Compare commits

..

16 Commits

Author SHA1 Message Date
Phillip Thelen cd8535a1c5 log language for registration events 2026-04-07 16:24:11 +02:00
Phillip Thelen f092a24ba8 || 2026-03-25 10:46:34 +01:00
Phillip Thelen 78f114182b remove only 2026-03-25 10:44:07 +01:00
Phillip Thelen 597ed0db87 fix tests 2026-03-25 10:36:40 +01:00
Phillip Thelen 8b13463c37 bump mongoose 2026-03-25 10:25:32 +01:00
Phillip Thelen 8348bff342 restore docker mongodb healthcheck 2026-03-25 10:09:20 +01:00
Phillip Thelen 80ee133466 don’t specify mongodb version directly 2026-03-25 10:06:54 +01:00
Phillip Thelen 23e96e431b correct check for finding authentication method 2026-03-25 10:06:42 +01:00
Kalista Payne fb3bac3493 fix(lint): no-unused-vars again 2026-03-24 17:20:43 -05:00
Kalista Payne 52a30a4d7e fix(lint): don't assign unused var 2026-03-24 17:12:19 -05:00
Phillip Thelen 01db89b259 remove unused code 2026-03-24 14:24:00 +01:00
Phillip Thelen 012592ae1c add tests 2026-03-24 14:19:04 +01:00
Phillip Thelen 0fe543b394 tests 2026-03-23 18:10:29 +01:00
Phillip Thelen 0e3e80bd08 track registration events 2026-03-23 17:41:07 +01:00
Phillip Thelen a64b190ac5 cleanup 2026-03-23 17:40:52 +01:00
Phillip Thelen 42d883c51e remove most of analytics code 2026-03-20 17:59:25 +01:00
115 changed files with 1159 additions and 3003 deletions
+1
View File
@@ -21,3 +21,4 @@ services:
timeout: 30s
start_period: 0s
retries: 30
+166 -143
View File
@@ -1,12 +1,12 @@
{
"name": "habitica",
"version": "5.47.0",
"version": "5.46.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "habitica",
"version": "5.47.0",
"version": "5.46.4",
"hasInstallScript": true,
"dependencies": {
"@babel/core": "^7.22.10",
@@ -57,7 +57,7 @@
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"mongoose": "^8.23.0",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
@@ -2624,6 +2624,19 @@
"node": ">=12.0.0"
}
},
"node_modules/@google-cloud/trace-agent/node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/@google-cloud/trace-agent/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -3052,9 +3065,9 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz",
"integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==",
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
@@ -6316,6 +6329,7 @@
"resolved": "https://registry.npmjs.org/bl/-/bl-2.2.1.tgz",
"integrity": "sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"readable-stream": "^2.3.5",
"safe-buffer": "^5.1.1"
@@ -6325,13 +6339,15 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/bl/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -6347,6 +6363,7 @@
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
@@ -11229,18 +11246,6 @@
"node": ">=12"
}
},
"node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11984,6 +11989,19 @@
"node": ">=12"
}
},
"node_modules/google-auth-library/node_modules/gcp-metadata": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz",
"integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^5.0.0",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=12"
}
},
"node_modules/google-auth-library/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -15557,128 +15575,14 @@
}
},
"node_modules/mongodb": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
"dev": true,
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.1.8",
"safe-buffer": "^5.1.2"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerDependenciesMeta": {
"aws4": {
"optional": true
},
"bson-ext": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"mongodb-extjson": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz",
"integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz",
"integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.1.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz",
"integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==",
"license": "MIT",
"dependencies": {
"tr46": "^5.0.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb/node_modules/bson": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
"dev": true,
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/mongoose": {
"version": "8.9.7",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.7.tgz",
"integrity": "sha512-mvNXmU0V8qZzMR/qoK2mjT4Ti2ALdtfS0teK+twxhlGkwzOD76V02/zWajTu2MJ7QyEmZe9OWvnJsIY0iAuX3Q==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.1",
"kareem": "2.6.3",
"mongodb": "~6.12.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/mongodb": {
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz",
"integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.9",
"bson": "^6.10.1",
"mongodb-connection-string-url": "^3.0.0"
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^6.10.4",
"mongodb-connection-string-url": "^3.0.2"
},
"engines": {
"node": ">=16.20.1"
@@ -15689,7 +15593,7 @@
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"snappy": "^7.3.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
@@ -15716,6 +15620,72 @@
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.2.tgz",
"integrity": "sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^14.1.0 || ^13.0.0"
}
},
"node_modules/mongodb-connection-string-url/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongodb-connection-string-url/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/mongodb-connection-string-url/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/mongoose": {
"version": "8.23.0",
"resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.23.0.tgz",
"integrity": "sha512-Bul4Ha6J8IqzFrb0B1xpVzkC3S0sk43dmLSnhFOn8eJlZiLwL5WO6cRymmjaADdCMjUcCpj2ce8hZI6O4ZFSug==",
"license": "MIT",
"dependencies": {
"bson": "^6.10.4",
"kareem": "2.6.3",
"mongodb": "~6.20.0",
"mpath": "0.9.0",
"mquery": "5.0.0",
"ms": "2.1.3",
"sift": "17.1.3"
},
"engines": {
"node": ">=16.20.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mongoose"
}
},
"node_modules/mongoose/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -15775,6 +15745,56 @@
"integrity": "sha512-jSTz73B/+pGTTvhu5Ym8xsG6+QqaWab53UXnXdNNlTijTdLvcHABCLJXudQiJxob5N1Mzr5EOSx5ziwn2sihPQ==",
"dev": true
},
"node_modules/monk/node_modules/bson": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/bson/-/bson-1.1.6.tgz",
"integrity": "sha512-EvVNVeGo4tHxwi8L6bPj3y3itEvStdwvvlojVxxbyYfoaxJ6keLgrTuKdyfEAszFK+H3olzBuafE0yoh0D1gdg==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=0.6.19"
}
},
"node_modules/monk/node_modules/mongodb": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.4.tgz",
"integrity": "sha512-K5q8aBqEXMwWdVNh94UQTwZ6BejVbFhh1uB6c5FKtPE9eUMZPUO3sRZdgIEcHSrAWmxzpG/FeODDKL388sqRmw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"bl": "^2.2.1",
"bson": "^1.1.4",
"denque": "^1.4.1",
"optional-require": "^1.1.8",
"safe-buffer": "^5.1.2"
},
"engines": {
"node": ">=4"
},
"optionalDependencies": {
"saslprep": "^1.0.0"
},
"peerDependenciesMeta": {
"aws4": {
"optional": true
},
"bson-ext": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"mongodb-extjson": {
"optional": true
},
"snappy": {
"optional": true
}
}
},
"node_modules/morgan": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
@@ -17137,10 +17157,11 @@
}
},
"node_modules/optional-require": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.8.tgz",
"integrity": "sha512-jq83qaUb0wNg9Krv1c5OQ+58EK+vHde6aBPzLvPPqJm89UQWsvSuFy9X/OSNJnFeSOKo7btE0n8Nl2+nE+z5nA==",
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/optional-require/-/optional-require-1.1.10.tgz",
"integrity": "sha512-0r3OB9EIQsP+a5HVATHq2ExIy2q/Vaffoo4IAikW1spCYswhLxqWQS0i3GwS3AdY/OIP4SWZHLGz8CMU558PGw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"require-at": "^1.0.6"
},
@@ -18622,6 +18643,7 @@
"resolved": "https://registry.npmjs.org/require-at/-/require-at-1.0.6.tgz",
"integrity": "sha512-7i1auJbMUrXEAZCOQ0VNJgmcT2VOKPRl2YGJwgpHpC9CE91Mv4/4UYIUm4chGJaI381ZDq1JUicFii64Hapd8g==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=4"
}
@@ -18920,6 +18942,7 @@
"resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.3.tgz",
"integrity": "sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
"sparse-bitfield": "^3.0.3"
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "habitica",
"description": "A habit tracker app which treats your goals like a Role Playing Game.",
"version": "5.47.0",
"version": "5.46.4",
"main": "./website/server/index.js",
"dependencies": {
"@babel/core": "^7.22.10",
@@ -52,7 +52,7 @@
"micromustache": "^8.0.3",
"moment": "^2.29.4",
"moment-recur": "git://github.com/HabitRPG/moment-recur.git#d3e8e6da0806f13b74dd2e4d7d9053e6a63db119",
"mongoose": "^8.9.5",
"mongoose": "^8.23.0",
"morgan": "^1.10.1",
"nan": "^2.25.0",
"nconf": "^0.12.1",
-560
View File
@@ -1,560 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import Amplitude from 'amplitude';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analyticsService', () => {
beforeEach(() => {
sandbox.stub(Amplitude.prototype, 'track').returns(Promise.resolve());
});
afterEach(() => {
sandbox.restore();
});
describe('#getServiceByEnvironment', () => {
it('returns mock methods when not in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
expect(analyticsService.getAnalyticsServiceByEnvironment())
.to.equal(analyticsService.mockAnalyticsService);
});
it('returns real methods when in production', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
expect(analyticsService.getAnalyticsServiceByEnvironment().track)
.to.equal(analyticsService.track);
expect(analyticsService.getAnalyticsServiceByEnvironment().trackPurchase)
.to.equal(analyticsService.trackPurchase);
});
});
describe('#track', () => {
let eventType; let
data;
beforeEach(() => {
eventType = 'Cron';
data = {
category: 'behavior',
uuid: 'unique-user-id',
resting: true,
cronCount: 5,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about event', () => analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
category: 'behavior',
resting: true,
cronCount: 5,
},
});
}));
it('sends english item name for gear if itemKey is provided', () => {
data.itemKey = 'headAccessory_special_foxEars';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Fox Ears',
},
});
});
});
it('sends english item name for egg if itemKey is provided', () => {
data.itemKey = 'Wolf';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Wolf Egg',
},
});
});
});
it('sends english item name for food if itemKey is provided', () => {
data.itemKey = 'Cake_Skeleton';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Bare Bones Cake',
},
});
});
});
it('sends english item name for hatching potion if itemKey is provided', () => {
data.itemKey = 'Golden';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Golden Hatching Potion',
},
});
});
});
it('sends english item name for quest if itemKey is provided', () => {
data.itemKey = 'atom1';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Attack of the Mundane, Part 1: Dish Disaster!',
},
});
});
});
it('sends english item name for purchased spell if itemKey is provided', () => {
data.itemKey = 'seafoam';
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
itemKey: data.itemKey,
itemName: 'Seafoam',
},
});
});
});
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
balance: 12,
loginIncentives: 1,
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.track(eventType, data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
balance: 12,
balanceGemAmount: 48,
loginIncentives: 1,
},
});
});
});
});
});
describe('#trackPurchase', () => {
let data;
beforeEach(() => {
data = {
uuid: 'user-id',
sku: 'paypal-checkout',
paymentMethod: 'PayPal',
itemPurchased: 'Gems',
purchaseValue: 8,
purchaseType: 'checkout',
gift: false,
quantity: 1,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
user: {
preferences: {
analyticsConsent: true,
},
},
};
});
context('Amplitude', () => {
it('calls out to amplitude', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledOnce;
}));
it('uses a dummy user id if none is provided', () => {
delete data.uuid;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_id: 'no-user-id-was-provided',
});
});
});
context('platform', () => {
it('logs web platform', () => {
data.headers = { 'x-client': 'habitica-web' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Web',
});
});
});
it('logs iOS platform', () => {
data.headers = { 'x-client': 'habitica-ios' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'iOS',
});
});
});
it('logs Android platform', () => {
data.headers = { 'x-client': 'habitica-android' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Android',
});
});
});
it('logs 3rd Party platform', () => {
data.headers = { 'x-client': 'some-third-party' };
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: '3rd Party',
});
});
});
it('logs unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
platform: 'Unknown',
});
});
});
});
context('Operating System', () => {
it('sets default', () => {
data.headers = {
'x-client': 'third-party',
'user-agent': 'foo',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Other',
os_version: '0',
});
});
});
it('sets iOS', () => {
data.headers = {
'x-client': 'habitica-ios',
'user-agent': 'Habitica/148 (iPhone; iOS 9.3; Scale/2.00)',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'iOS',
os_version: '9.3.0',
});
});
});
it('sets Android', () => {
data.headers = {
'x-client': 'habitica-android',
'user-agent': 'Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19',
};
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: 'Android',
os_version: '4.0.4',
});
});
});
it('sets Unknown if headers are not passed in', () => {
delete data.headers;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
os_name: undefined,
os_version: undefined,
});
});
});
});
it('sends details about purchase', () => analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
event_properties: {
gift: false,
itemPurchased: 'Gems',
paymentMethod: 'PayPal',
purchaseType: 'checkout',
quantity: 1,
sku: 'paypal-checkout',
},
});
}));
it('sends user data if provided', () => {
const stats = {
class: 'wizard', exp: 5, gp: 23, hp: 10, lvl: 4, mp: 30,
};
const user = {
stats,
contributor: { level: 1 },
purchased: { plan: { planId: 'foo-plan' } },
flags: { tour: { intro: -2 } },
habits: [{ _id: 'habit' }],
dailys: [{ _id: 'daily' }],
todos: [{ _id: 'todo' }],
rewards: [{ _id: 'reward' }],
preferences: {
analyticsConsent: true,
},
};
data.user = user;
return analyticsService.trackPurchase(data)
.then(() => {
expect(Amplitude.prototype.track).to.be.calledWithMatch({
user_properties: {
Class: 'wizard',
Experience: 5,
Gold: 23,
Health: 10,
Level: 4,
Mana: 30,
tutorialComplete: true,
'Number Of Tasks': {
habits: 1,
dailys: 1,
todos: 1,
rewards: 1,
},
contributorLevel: 1,
subscription: 'foo-plan',
},
});
});
});
});
});
describe('mockAnalyticsService', () => {
it('has stubbed track method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('track');
});
it('has stubbed trackPurchase method', () => {
expect(analyticsService.mockAnalyticsService).to.respondTo('trackPurchase');
});
});
});
+116 -136
View File
@@ -13,7 +13,6 @@ import { cron, cronWrapper } from '../../../../website/server/libs/cron';
import { model as User } from '../../../../website/server/models/user';
import * as Tasks from '../../../../website/server/models/task';
import common from '../../../../website/common';
import * as analytics from '../../../../website/server/libs/analyticsService';
import { model as Group } from '../../../../website/server/models/group';
const CRON_TIMEOUT_WAIT = new Date(5 * 60 * 1000).getTime();
@@ -41,20 +40,17 @@ describe('cron', async () => {
},
},
});
sinon.spy(analytics, 'track');
});
afterEach(async () => {
if (clock !== null) clock.restore();
analytics.track.restore();
});
it('updates user.preferences.timezoneOffsetAtLastCron', async () => {
const timezoneUtcOffsetFromUserPrefs = -1;
await cron({
user, tasksByType, daysMissed, analytics, timezoneUtcOffsetFromUserPrefs,
user, tasksByType, daysMissed, timezoneUtcOffsetFromUserPrefs,
});
expect(user.preferences.timezoneOffsetAtLastCron).to.equal(1);
@@ -63,7 +59,7 @@ describe('cron', async () => {
it('resets user.items.lastDrop.count', async () => {
user.items.lastDrop.count = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.items.lastDrop.count).to.equal(0);
});
@@ -71,26 +67,11 @@ describe('cron', async () => {
it('increments user cron count', async () => {
const cronCountBefore = user.flags.cronCount;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.flags.cronCount).to.be.greaterThan(cronCountBefore);
});
it('calls analytics', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
it('calls analytics when user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
});
expect(analytics.track.callCount).to.equal(1);
});
describe('end of the month perks', async () => {
beforeEach(async () => {
user.purchased.plan.customerId = 'subscribedId';
@@ -101,7 +82,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-12-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(2);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -112,7 +93,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = new Date('2018-11-11');
clock = sinon.useFakeTimers(new Date('2019-01-29'));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.mysteryItems.length).to.eql(4);
const filteredNotifications = user.notifications.filter(n => n.type === 'NEW_MYSTERY_ITEMS');
@@ -122,7 +103,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -131,7 +112,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
user.purchased.plan.dateUpdated = undefined;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -142,7 +123,7 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
@@ -150,7 +131,7 @@ describe('cron', async () => {
it('resets plan.dateUpdated on a new month', async () => {
const currentMonth = moment().startOf('month');
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(moment(user.purchased.plan.dateUpdated).startOf('month').isSame(currentMonth)).to.eql(true);
});
@@ -158,7 +139,7 @@ describe('cron', async () => {
it('increments plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(1);
});
@@ -166,7 +147,7 @@ describe('cron', async () => {
it('increments plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(1);
});
@@ -175,7 +156,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(2, 'months').toDate();
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(2);
});
@@ -184,7 +165,7 @@ describe('cron', async () => {
user.purchased.plan.dateUpdated = moment().subtract(3, 'months').toDate();
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(3);
});
@@ -196,7 +177,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(1);
@@ -206,7 +187,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -214,7 +195,7 @@ describe('cron', async () => {
it('does not reset plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.exist;
});
@@ -225,7 +206,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
@@ -264,7 +245,7 @@ describe('cron', async () => {
// Add 2 days so that we're sure we're not affected by any start-of-month effects
// e.g., from time zone oddness.
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(1);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -276,7 +257,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user1, tasksByType, daysMissed, analytics,
user: user1, tasksByType, daysMissed,
});
expect(user1.purchased.plan.consecutive.count).to.equal(10);
expect(user1.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -311,7 +292,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(1);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -323,7 +304,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3, tasksByType, daysMissed, analytics,
user: user3, tasksByType, daysMissed,
});
expect(user3.purchased.plan.consecutive.count).to.equal(10);
expect(user3.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -358,7 +339,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user6, tasksByType, daysMissed, analytics,
user: user6, tasksByType, daysMissed,
});
expect(user6.purchased.plan.consecutive.count).to.equal(1);
expect(user6.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -391,7 +372,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(1);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(2);
@@ -403,7 +384,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user12, tasksByType, daysMissed, analytics,
user: user12, tasksByType, daysMissed,
});
expect(user12.purchased.plan.consecutive.count).to.equal(10);
expect(user12.purchased.plan.consecutive.trinkets).to.equal(11);
@@ -439,7 +420,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
expect(user3g.purchased.plan.consecutive.count).to.equal(1);
expect(user3g.purchased.plan.cumulativeCount).to.equal(1);
@@ -452,7 +433,7 @@ describe('cron', async () => {
.add(2, 'days')
.toDate());
await cron({
user: user3g, tasksByType, daysMissed, analytics,
user: user3g, tasksByType, daysMissed,
});
// subscription has been erased by now
expect(user3g.purchased.plan.consecutive.count).to.equal(0);
@@ -471,7 +452,7 @@ describe('cron', async () => {
it('resets plan.gemsBought on a new month', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(0);
});
@@ -482,14 +463,14 @@ describe('cron', async () => {
user.purchased.plan.gemsBought = 10;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.gemsBought).to.equal(10);
});
it('does not reset plan.dateUpdated on a new month', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.dateUpdated).to.be.empty;
});
@@ -497,7 +478,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.count', async () => {
user.purchased.plan.consecutive.count = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.count).to.equal(0);
});
@@ -505,7 +486,7 @@ describe('cron', async () => {
it('does not increment plan.cumulativeCount', async () => {
user.purchased.plan.cumulativeCount = 0;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.cumulativeCount).to.equal(0);
});
@@ -513,7 +494,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.trinkets when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.trinkets).to.equal(0);
});
@@ -521,7 +502,7 @@ describe('cron', async () => {
it('does not increment plan.consecutive.gemCapExtra when user has reached a month that is a multiple of 3', async () => {
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(0);
});
@@ -530,7 +511,7 @@ describe('cron', async () => {
user.purchased.plan.consecutive.gemCapExtra = 26;
user.purchased.plan.consecutive.count = 5;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.consecutive.gemCapExtra).to.equal(26);
});
@@ -538,7 +519,7 @@ describe('cron', async () => {
it('does nothing to plan stats if we are before the last day of the cancelled month', async () => {
user.purchased.plan.dateTerminated = moment(new Date()).add({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.purchased.plan.customerId).to.not.exist;
});
@@ -564,7 +545,7 @@ describe('cron', async () => {
it('should make uncompleted todos redder', async () => {
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.be.lessThan(valueBefore);
});
@@ -573,7 +554,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
const valueBefore = tasksByType.todos[0].value;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.todos[0].value).to.equal(valueBefore);
});
@@ -582,7 +563,7 @@ describe('cron', async () => {
tasksByType.todos[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.todos).to.be.lengthOf(1);
@@ -608,7 +589,7 @@ describe('cron', async () => {
expect(user.tasksOrder.todos).to.be.lengthOf(3);
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
// user.tasksOrder.todos should be filtered while tasks by type remains unchanged
@@ -635,7 +616,7 @@ describe('cron', async () => {
const original = user.tasksOrder.todos; // Preserve the original order
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
let listsAreEqual = true;
@@ -675,7 +656,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.be.false;
});
@@ -686,7 +667,7 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].isDue).to.exist;
});
@@ -696,14 +677,14 @@ describe('cron', async () => {
tasksByType.dailys[0].everyX = 5;
tasksByType.dailys[0].startDate = moment().add(1, 'days').toDate();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].nextDue.length).to.eql(6);
});
it('should add history', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].history).to.be.lengthOf(1);
});
@@ -711,7 +692,7 @@ describe('cron', async () => {
it('should set tasks completed to false', async () => {
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -720,7 +701,7 @@ describe('cron', async () => {
user.preferences.sleep = true;
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].completed).to.be.false;
});
@@ -729,7 +710,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -739,7 +720,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].completed = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -749,7 +730,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: false });
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.dailys[0].checklist[0].completed).to.be.false;
});
@@ -759,7 +740,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.be.lessThan(hpBefore);
});
@@ -770,7 +751,7 @@ describe('cron', async () => {
const hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
});
@@ -784,7 +765,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -797,7 +778,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(hpBefore);
@@ -808,7 +789,7 @@ describe('cron', async () => {
let hpBefore = user.stats.hp;
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfFullyIncompleteDaily = hpBefore - user.stats.hp;
@@ -816,7 +797,7 @@ describe('cron', async () => {
tasksByType.dailys[0].checklist.push({ title: 'test', completed: true });
tasksByType.dailys[0].checklist.push({ title: 'test2', completed: false });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const hpDifferenceOfPartiallyIncompleteDaily = hpBefore - user.stats.hp;
@@ -829,7 +810,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
@@ -841,7 +822,7 @@ describe('cron', async () => {
tasksByType.dailys[0].startDate = moment(new Date()).subtract({ days: 1 });
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(0);
@@ -862,7 +843,7 @@ describe('cron', async () => {
tasksByType.dailys[1].frequency = 'daily';
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.hp).to.equal(48);
@@ -886,7 +867,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -897,7 +878,7 @@ describe('cron', async () => {
tasksByType.habits[0].up = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.be.lessThan(1);
@@ -909,7 +890,7 @@ describe('cron', async () => {
tasksByType.habits[0].down = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].value).to.equal(1);
@@ -928,7 +909,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -941,7 +922,7 @@ describe('cron', async () => {
tasksByType.habits[0].counterDown = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -955,7 +936,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -964,7 +945,7 @@ describe('cron', async () => {
// should reset
daysMissed = 8;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -988,7 +969,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1002,7 +983,7 @@ describe('cron', async () => {
// should reset after user CDS
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1026,7 +1007,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1036,7 +1017,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1060,7 +1041,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1084,7 +1065,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1098,7 +1079,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1107,7 +1088,7 @@ describe('cron', async () => {
// should reset
daysMissed = 32;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1132,7 +1113,7 @@ describe('cron', async () => {
// should reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1156,7 +1137,7 @@ describe('cron', async () => {
// should not reset
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(1);
@@ -1166,7 +1147,7 @@ describe('cron', async () => {
// should reset
daysMissed = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(tasksByType.habits[0].counterUp).to.equal(0);
@@ -1199,7 +1180,7 @@ describe('cron', async () => {
user.stats.lvl = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.history.exp).to.have.lengthOf(1);
@@ -1212,7 +1193,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(1);
@@ -1224,7 +1205,7 @@ describe('cron', async () => {
tasksByType.dailys[0].isDue = false;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.achievements.perfect).to.equal(0);
@@ -1238,7 +1219,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1256,7 +1237,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1280,7 +1261,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1307,7 +1288,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1333,7 +1314,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1360,7 +1341,7 @@ describe('cron', async () => {
};
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.equal(0);
@@ -1381,7 +1362,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1401,7 +1382,7 @@ describe('cron', async () => {
const previousBuffs = user.stats.buffs.toObject();
cronOverride({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.buffs.str).to.be.greaterThan(previousBuffs.str);
@@ -1420,7 +1401,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.be.greaterThan(mpBefore);
@@ -1436,7 +1417,7 @@ describe('cron', async () => {
tasksByType.dailys[0].completed = true;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(mpBefore);
@@ -1449,7 +1430,7 @@ describe('cron', async () => {
user.stats.mp = 120;
stubbedStatsComputed.returns(Object.assign(statsComputedRes, { maxMP: 100 }));
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.stats.mp).to.equal(common.statsComputed(user).maxMP);
@@ -1482,7 +1463,7 @@ describe('cron', async () => {
it('resets user progress', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.party.quest.progress.up).to.equal(0);
expect(user.party.quest.progress.down).to.equal(0);
@@ -1491,7 +1472,7 @@ describe('cron', async () => {
it('applies the user progress', async () => {
const progress = await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(progress.down).to.equal(-1);
});
@@ -1529,19 +1510,19 @@ describe('cron', async () => {
describe('login incentives', async () => {
it('increments incentive counter each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
user.lastCron = moment(new Date()).subtract({ days: 1 });
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
});
it('pushes a notification of the day\'s incentive each cron', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.notifications.length).to.eql(1);
expect(user.notifications[0].type).to.eql('LOGIN_INCENTIVE');
@@ -1549,13 +1530,13 @@ describe('cron', async () => {
it('replaces previous notifications', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
const filteredNotifications = user.notifications.filter(n => n.type === 'LOGIN_INCENTIVE');
@@ -1566,7 +1547,7 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if days are skipped in between', async () => {
daysMissed = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
@@ -1574,14 +1555,14 @@ describe('cron', async () => {
it('increments loginIncentives by 1 even if user is sleeping', async () => {
user.preferences.sleep = true;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
});
it('awards user bard robes if login incentive is 1', async () => {
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(1);
expect(user.items.gear.owned.armor_special_bardRobes).to.eql(true);
@@ -1591,7 +1572,7 @@ describe('cron', async () => {
it('awards user incentive backgrounds if login incentive is 2', async () => {
user.loginIncentives = 1;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(2);
expect(user.purchased.background.blue).to.eql(true);
@@ -1605,7 +1586,7 @@ describe('cron', async () => {
it('awards user Bard Hat if login incentive is 3', async () => {
user.loginIncentives = 2;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(3);
expect(user.items.gear.owned.head_special_bardHat).to.eql(true);
@@ -1615,7 +1596,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 4', async () => {
user.loginIncentives = 3;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(4);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1625,7 +1606,7 @@ describe('cron', async () => {
it('awards user a Chocolate, Meat and Pink Contton Candy if login incentive is 5', async () => {
user.loginIncentives = 4;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(5);
@@ -1639,7 +1620,7 @@ describe('cron', async () => {
it('awards user moon quest if login incentive is 7', async () => {
user.loginIncentives = 6;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(7);
expect(user.items.quests.moon1).to.eql(1);
@@ -1649,7 +1630,7 @@ describe('cron', async () => {
it('awards user RoyalPurple Hatching Potion if login incentive is 10', async () => {
user.loginIncentives = 9;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(10);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1659,7 +1640,7 @@ describe('cron', async () => {
it('awards user a Strawberry, Patato and Blue Contton Candy if login incentive is 14', async () => {
user.loginIncentives = 13;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(14);
@@ -1673,7 +1654,7 @@ describe('cron', async () => {
it('awards user a bard instrument if login incentive is 18', async () => {
user.loginIncentives = 17;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(18);
expect(user.items.gear.owned.weapon_special_bardInstrument).to.eql(true);
@@ -1683,7 +1664,7 @@ describe('cron', async () => {
it('awards user second moon quest if login incentive is 22', async () => {
user.loginIncentives = 21;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(22);
expect(user.items.quests.moon2).to.eql(1);
@@ -1693,7 +1674,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 26', async () => {
user.loginIncentives = 25;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(26);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1703,7 +1684,7 @@ describe('cron', async () => {
it('awards user Fish, Milk, Rotten Meat and Honey if login incentive is 30', async () => {
user.loginIncentives = 29;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(30);
@@ -1718,7 +1699,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 35', async () => {
user.loginIncentives = 34;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(35);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1728,7 +1709,7 @@ describe('cron', async () => {
it('awards user the third moon quest if login incentive is 40', async () => {
user.loginIncentives = 39;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(40);
expect(user.items.quests.moon3).to.eql(1);
@@ -1738,7 +1719,7 @@ describe('cron', async () => {
it('awards user a RoyalPurple hatching potion if login incentive is 45', async () => {
user.loginIncentives = 44;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(45);
expect(user.items.hatchingPotions.RoyalPurple).to.eql(1);
@@ -1748,7 +1729,7 @@ describe('cron', async () => {
it('awards user a saddle if login incentive is 50', async () => {
user.loginIncentives = 49;
await cron({
user, tasksByType, daysMissed, analytics,
user, tasksByType, daysMissed,
});
expect(user.loginIncentives).to.eql(50);
expect(user.items.food.Saddle).to.eql(1);
@@ -1766,7 +1747,6 @@ describe('cron wrapper', () => {
res = generateRes();
req = generateReq();
user = await res.locals.user.save();
res.analytics = analytics;
});
afterEach(() => {
+100
View File
@@ -0,0 +1,100 @@
import nconf from 'nconf';
import requireAgain from 'require-again';
import { model as User } from '../../../../website/server/models/user';
import { RegistrationEventModel } from '../../../../website/server/models/analytics/registrationEvent';
import { SubscriptionEventModel } from '../../../../website/server/models/analytics/subscriptionEvent';
describe('localAnalytics', () => {
let user;
let localAnalytics;
before(() => {
const nconfGetStub = sandbox.stub(nconf, 'get');
nconfGetStub.withArgs('ANALYTICS_DB').returns('analytics');
nconfGetStub.withArgs('DISABLE_LOCAL_ANALYTICS').returns(false);
localAnalytics = requireAgain('../../../../website/server/libs/localAnalytics');
});
beforeEach(async () => {
user = new User({
auth: {
local: {
username: 'username',
email: 'email@example.com',
},
},
registeredThrough: 'habitica-web',
});
});
describe('trackRegistrationEvent', () => {
afterEach(async () => {
await RegistrationEventModel.deleteMany({});
});
it('creates a registration event when a user registers', async () => {
user._id = '00000000-0000-0000-0000-000000000001';
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.1' });
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(1);
expect(registrationEvents[0]).to.have.property('userId', user._id);
expect(registrationEvents[0]).to.have.property('ipAddress', '127.0.0.1');
});
it('saves the correct data to the database', async () => {
user._id = '00000000-0000-0000-0000-000000000002';
user.auth.google = { id: 'abc', emails: [{ value: 'email@example.com' }] };
await localAnalytics.trackRegistrationEvent({ user, ipAddress: '127.0.0.2' });
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress', '127.0.0.2');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
});
describe('trackSubscriptionEvent', () => {
afterEach(async () => {
await SubscriptionEventModel.deleteMany({});
});
it('creates a subscription event when a user subscribes', async () => {
user._id = '00000000-0000-0000-0000-000000000003';
await localAnalytics.trackSubscriptionEvent({
eventType: 'subscribed',
user,
paymentMethod: 'stripe',
customerId: 'cus_123',
planId: 'plan_123',
});
const subscriptionEvents = await SubscriptionEventModel.find({ userId: user._id });
expect(subscriptionEvents).to.have.lengthOf(1);
expect(subscriptionEvents[0]).to.have.property('userId', user._id);
expect(subscriptionEvents[0]).to.have.property('eventType', 'subscribed');
expect(subscriptionEvents[0]).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvents[0]).to.have.property('customerId', 'cus_123');
expect(subscriptionEvents[0]).to.have.property('planId', 'plan_123');
});
it('creates a subscription event with cancellation reason when a user cancels', async () => {
user._id = '00000000-0000-0000-0000-000000000004';
await localAnalytics.trackSubscriptionEvent({
eventType: 'cancelled',
user,
paymentMethod: 'stripe',
customerId: 'cus_456',
planId: 'plan_456',
cancellationReason: 'No longer needed',
});
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('paymentMethod', 'stripe');
expect(subscriptionEvent).to.have.property('customerId', 'cus_456');
expect(subscriptionEvent).to.have.property('planId', 'plan_456');
expect(subscriptionEvent).to.have.property('cancellationReason', 'No longer needed');
});
});
});
+64 -47
View File
@@ -3,7 +3,6 @@ import moment from 'moment';
import * as sender from '../../../../../website/server/libs/email';
import common from '../../../../../website/common';
import api from '../../../../../website/server/libs/payments/payments';
import * as analytics from '../../../../../website/server/libs/analyticsService';
import * as notifications from '../../../../../website/server/libs/pushNotifications';
import { model as User } from '../../../../../website/server/models/user';
import { translate as t } from '../../../../helpers/api-integration/v3';
@@ -13,6 +12,7 @@ import {
import * as worldState from '../../../../../website/server/libs/worldState';
import { TransactionModel } from '../../../../../website/server/models/transaction';
import { REPEATING_EVENTS } from '../../../../../website/common/script/content/constants/events';
import { SubscriptionEventModel } from '../../../../../website/server/models/analytics/subscriptionEvent';
describe('payments/index', () => {
let user;
@@ -36,8 +36,6 @@ describe('payments/index', () => {
sandbox.stub(sender, 'sendTxn');
sandbox.stub(user, 'sendMessage');
sandbox.stub(analytics.mockAnalyticsService, 'trackPurchase');
sandbox.stub(analytics.mockAnalyticsService, 'track');
sandbox.stub(notifications, 'sendNotification');
data = {
@@ -97,6 +95,16 @@ describe('payments/index', () => {
expect(recipient.items.pets['Jackalope-RoyalPurple']).to.eql(5);
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: recipient._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'subscribed');
expect(subscriptionEvent).to.have.property('userId', recipient._id);
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('adds extra months to an existing subscription', async () => {
recipient.purchased.plan = plan;
@@ -298,28 +306,6 @@ describe('payments/index', () => {
expect(notifications.sendNotification).to.be.calledOnce;
});
it('tracks subscription purchase as gift', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: true,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('No Active Promotion', () => {
beforeEach(() => {
sinon.stub(worldState, 'getCurrentEventList').returns([]);
@@ -455,6 +441,16 @@ describe('payments/index', () => {
expect(user.purchased.plan.dateCreated).to.exist;
});
it('tracks subscription events', async () => {
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('ipAddress');
expect(subscriptionEvent).to.have.property('planId', 'basic_3mo');
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('sets plan.dateCreated if it did not previously exist', async () => {
expect(user.purchased.plan.dateCreated).to.not.exist;
@@ -543,29 +539,24 @@ describe('payments/index', () => {
expect(sender.sendTxn).to.be.calledWith(data.user, 'subscription-begins');
});
it('tracks subscription purchase', async () => {
await api.createSubscription(data);
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledOnce;
expect(analytics.mockAnalyticsService.trackPurchase).to.be.calledWith({
uuid: user._id,
groupId: undefined,
itemPurchased: 'Subscription',
sku: 'payment method-subscription',
purchaseType: 'subscribe',
paymentMethod: data.paymentMethod,
quantity: 1,
gift: false,
purchaseValue: 15,
firstPurchase: true,
headers: {
'x-client': 'habitica-web',
'user-agent': '',
},
});
});
context('Upgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_6mo';
data.updatedFrom = { key: 'basic_earned' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_6mo' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'upgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_earned to basic_6mo', async () => {
data.sub.key = 'basic_earned';
expect(user.purchased.plan.planId).to.not.exist;
@@ -608,6 +599,23 @@ describe('payments/index', () => {
});
context('Downgrades subscription', () => {
it('tracks subscription events', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
await api.createSubscription(data);
data.sub.key = 'basic_earned';
data.updatedFrom = { key: 'basic_6mo' };
await api.createSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id, planId: 'basic_earned' });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'downgraded');
expect(subscriptionEvent).to.have.property('userId', user._id);
expect(subscriptionEvent).to.have.property('paymentMethod', 'Payment Method');
});
it('from basic_6mo to basic_earned', async () => {
data.sub.key = 'basic_6mo';
expect(user.purchased.plan.planId).to.not.exist;
@@ -1136,6 +1144,15 @@ describe('payments/index', () => {
expect(daysTillTermination).to.be.within(29, 30); // 1 month +/- 1 days
});
it('tracks subscription events', async () => {
await api.cancelSubscription(data);
const subscriptionEvent = await SubscriptionEventModel.findOne({ userId: user._id });
expect(subscriptionEvent).to.exist;
expect(subscriptionEvent).to.have.property('eventType', 'cancelled');
expect(subscriptionEvent).to.have.property('userId', user._id);
});
it('adds extraMonths to dateTerminated value', async () => {
user.purchased.plan.extraMonths = 2;
@@ -1,50 +0,0 @@
/* eslint-disable global-require */
import nconf from 'nconf';
import requireAgain from 'require-again';
import {
generateRes,
generateReq,
generateNext,
} from '../../../helpers/api-unit.helper';
import * as analyticsService from '../../../../website/server/libs/analyticsService';
describe('analytics middleware', () => {
let res; let req; let
next;
const pathToAnalyticsMiddleware = '../../../../website/server/middlewares/analytics';
beforeEach(() => {
res = generateRes();
req = generateReq();
next = generateNext();
});
it('attaches analytics object to res', () => {
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics).to.exist;
});
it('attaches stubbed methods for non-prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(false);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.mockAnalyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.mockAnalyticsService.trackPurchase);
});
it('attaches real methods for prod environments', () => {
sandbox.stub(nconf, 'get').withArgs('IS_PROD').returns(true);
const attachAnalytics = requireAgain(pathToAnalyticsMiddleware).default;
attachAnalytics(req, res, next);
expect(res.analytics.track).to.eql(analyticsService.track);
expect(res.analytics.trackPurchase).to.eql(analyticsService.trackPurchase);
});
});
@@ -1,19 +0,0 @@
import {
generateUser,
requester,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /analytics/track/:eventName', () => {
it('calls res.analytics', async () => {
const user = await generateUser();
sandbox.spy(analytics, 'track');
const requestWithHeaders = requester(user, { 'x-client': 'habitica-web' });
await requestWithHeaders.post('/analytics/track/eventName', { data: 'example' }, { 'x-client': 'habitica-web' });
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('eventName', sandbox.match({ data: 'example' }));
sandbox.restore();
});
});
@@ -1,7 +1,6 @@
import {
generateUser,
} from '../../../../helpers/api-integration/v3';
import { mockAnalyticsService as analytics } from '../../../../../website/server/libs/analyticsService';
describe('POST /user/sleep', () => {
let user;
@@ -23,15 +22,4 @@ describe('POST /user/sleep', () => {
await user.sync();
expect(user.preferences.sleep).to.be.false;
});
it('sends sleep status to analytics service', async () => {
sandbox.spy(analytics, 'track');
await user.post('/user/sleep');
await user.sync();
expect(analytics.track).to.be.calledOnce;
expect(analytics.track).to.be.calledWith('sleep', sandbox.match.has('status', user.preferences.sleep));
sandbox.restore();
});
});
@@ -9,6 +9,7 @@ import {
} from '../../../../../helpers/api-integration/v3';
import { ApiUser } from '../../../../../helpers/api-integration/api-classes';
import { encrypt } from '../../../../../../website/server/libs/encryption';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
function generateRandomUserName () {
return (Date.now() + uuid()).substring(0, 20);
@@ -41,6 +42,25 @@ describe('POST /user/auth/local/register', () => {
expect(user.newUser).to.eql(true);
});
it('tracks a registration event', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
const password = 'password';
const user = await api.post('/user/auth/local/register', {
username,
email,
password,
confirmPassword: password,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: user._id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', user._id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'local');
});
it('registers a new user and sets verifiedUsername to true', async () => {
const username = generateRandomUserName();
const email = `${username}@example.com`;
@@ -7,6 +7,7 @@ import {
getProperty,
} from '../../../../../helpers/api-integration/v3';
import apiErrorMessages from '../../../../../../website/common/script/errors/apiErrorMessages';
import { RegistrationEventModel } from '../../../../../../website/server/models/analytics/registrationEvent';
describe('POST /user/auth/social', () => {
let api;
@@ -65,6 +66,19 @@ describe('POST /user/auth/social', () => {
await expect(getProperty('users', response.id, 'profile.name')).to.eventually.equal('a google user');
});
it('tracks a registration event', async () => {
const socialUser = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvent = await RegistrationEventModel.findOne({ userId: socialUser.id });
expect(registrationEvent).to.exist;
expect(registrationEvent).to.have.property('userId', socialUser.id);
expect(registrationEvent).to.have.property('ipAddress');
expect(registrationEvent).to.have.property('authenticationMethod', 'google');
});
it('includes sanitized version of provided username', async () => {
const response = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
@@ -231,6 +245,17 @@ describe('POST /user/auth/social', () => {
expect(response.newUser).to.be.false;
});
it('does not track a registration event for existing users', async () => {
const beforeEvents = await RegistrationEventModel.find({ userId: user._id });
await user.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
network,
});
const registrationEvents = await RegistrationEventModel.find({ userId: user._id });
expect(registrationEvents).to.have.lengthOf(beforeEvents.length);
});
it('does not log into other account if social auth already exists', async () => {
const registerResponse = await api.post(endpoint, {
authResponse: { access_token: randomAccessToken }, // eslint-disable-line camelcase
+1 -10
View File
@@ -13,7 +13,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buy', () => {
let user;
const analytics = { track () {} };
beforeEach(() => {
user = generateUser({
@@ -32,12 +31,6 @@ describe('shared.ops.buy', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('returns error when key is not provided', async () => {
@@ -51,10 +44,8 @@ describe('shared.ops.buy', () => {
it('buys health potion', async () => {
user.stats.hp = 30;
await buy(user, { params: { key: 'potion' } }, analytics);
await buy(user, { params: { key: 'potion' } });
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('adds equipment to inventory', async () => {
+3 -7
View File
@@ -29,10 +29,9 @@ describe('shared.ops.buyArmoire', () => {
const YIELD_EQUIPMENT = 0.5;
const YIELD_FOOD = 0.7;
const YIELD_EXP = 0.9;
const analytics = { track () {} };
async function buyArmoire (_user, _req, _analytics) {
const buyOp = new BuyArmoireOperation(_user, _req, _analytics);
async function buyArmoire (_user, _req) {
const buyOp = new BuyArmoireOperation(_user, _req);
return buyOp.purchase();
}
@@ -50,12 +49,10 @@ describe('shared.ops.buyArmoire', () => {
user.items.food = {};
sandbox.stub(randomValFns, 'trueRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
randomValFns.trueRandom.restore();
analytics.track.restore();
});
context('failure conditions', () => {
@@ -147,7 +144,7 @@ describe('shared.ops.buyArmoire', () => {
expect(_.size(user.items.gear.owned)).to.equal(2);
await buyArmoire(user, {}, analytics);
await buyArmoire(user, {});
expect(_.size(user.items.gear.owned)).to.equal(3);
@@ -155,7 +152,6 @@ describe('shared.ops.buyArmoire', () => {
expect(armoireCount).to.eql(_.size(getFullArmoire()) - 2);
expect(user.stats.gp).to.eql(100);
expect(analytics.track).to.be.calledTwice;
});
});
});
+3 -12
View File
@@ -1,6 +1,5 @@
/* eslint-disable camelcase */
import sinon from 'sinon'; // eslint-disable-line no-shadow
import {
generateUser,
} from '../../../helpers/common.helper';
@@ -11,15 +10,14 @@ import i18n from '../../../../website/common/script/i18n';
import { BuyGemOperation } from '../../../../website/common/script/ops/buy/buyGem';
import planGemLimits from '../../../../website/common/script/libs/planGemLimits';
async function buyGem (user, req, analytics) {
const buyOp = new BuyGemOperation(user, req, analytics);
async function buyGem (user, req) {
const buyOp = new BuyGemOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyGem', () => {
let user;
const analytics = { track () {} };
const goldPoints = 40;
const gemsBought = 40;
const userGemAmount = 10;
@@ -35,23 +33,16 @@ describe('shared.ops.buyGem', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Gems', () => {
it('purchases gems', async () => {
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } }, analytics);
const [, message] = await buyGem(user, { params: { type: 'gems', key: 'gem' } });
expect(message).to.equal(i18n.t('plusGem', { count: 1 }));
expect(user.balance).to.equal(userGemAmount + 0.25);
expect(user.purchased.plan.gemsBought).to.equal(1);
expect(user.stats.gp).to.equal(goldPoints - planGemLimits.convRate);
expect(analytics.track).to.be.calledOnce;
});
it('purchases gems with a different language than the default', async () => {
+3 -10
View File
@@ -10,10 +10,9 @@ import i18n from '../../../../website/common/script/i18n';
describe('shared.ops.buyHealthPotion', () => {
let user;
const analytics = { track () {} };
async function buyHealthPotion (_user, _req, _analytics) {
const buyOp = new BuyHealthPotionOperation(_user, _req, _analytics);
async function buyHealthPotion (_user, _req) {
const buyOp = new BuyHealthPotionOperation(_user, _req);
return buyOp.purchase();
}
@@ -32,19 +31,13 @@ describe('shared.ops.buyHealthPotion', () => {
},
stats: { gp: 200 },
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('Potion', () => {
it('recovers 15 hp', async () => {
user.stats.hp = 30;
await buyHealthPotion(user, {}, analytics);
await buyHealthPotion(user, {});
expect(user.stats.hp).to.eql(45);
expect(analytics.track).to.be.calledOnce;
});
it('does not increase hp above 50', async () => {
+5 -9
View File
@@ -13,15 +13,14 @@ import {
import i18n from '../../../../website/common/script/i18n';
import { errorMessage } from '../../../../website/common/script/libs/errorMessage';
async function buyGear (user, req, analytics) {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
async function buyGear (user, req) {
const buyOp = new BuyMarketGearOperation(user, req);
return buyOp.purchase();
}
describe('shared.ops.buyMarketGear', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -47,14 +46,12 @@ describe('shared.ops.buyMarketGear', () => {
sinon.stub(shared, 'randomVal');
sinon.stub(shared.onboarding, 'checkOnboardingStatus');
sinon.stub(shared.fns, 'predictableRandom');
sinon.stub(analytics, 'track');
});
afterEach(() => {
shared.randomVal.restore();
shared.fns.predictableRandom.restore();
shared.onboarding.checkOnboardingStatus.restore();
analytics.track.restore();
if (clock) {
clock.restore();
@@ -65,7 +62,7 @@ describe('shared.ops.buyMarketGear', () => {
it('adds equipment to inventory', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.items.gear.owned).to.eql({
weapon_warrior_0: true,
@@ -92,13 +89,12 @@ describe('shared.ops.buyMarketGear', () => {
eyewear_special_whiteHalfMoon: true,
eyewear_special_yellowHalfMoon: true,
});
expect(analytics.track).to.be.calledOnce;
});
it('adds the onboarding achievement to the user and checks the onboarding status', async () => {
user.stats.gp = 31;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.be.calledOnce;
expect(user.addAchievement).to.be.calledWith('purchasedEquipment');
@@ -111,7 +107,7 @@ describe('shared.ops.buyMarketGear', () => {
user.stats.gp = 31;
user.achievements.purchasedEquipment = true;
await buyGear(user, { params: { key: 'armor_warrior_1' } }, analytics);
await buyGear(user, { params: { key: 'armor_warrior_1' } });
expect(user.addAchievement).to.not.be.called;
});
+2 -5
View File
@@ -14,7 +14,6 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyMysterySet', () => {
let user;
const analytics = { track () {} };
let clock;
beforeEach(() => {
@@ -27,11 +26,9 @@ describe('shared.ops.buyMysterySet', () => {
},
},
});
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -93,7 +90,7 @@ describe('shared.ops.buyMysterySet', () => {
context('successful purchases', () => {
it('buys Steampunk Accessories Set', async () => {
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '301404' } }, analytics);
await buyMysterySet(user, { params: { key: '301404' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('weapon_warrior_0', true);
@@ -106,7 +103,7 @@ describe('shared.ops.buyMysterySet', () => {
it('buys mystery set if it is available', async () => {
clock = sinon.useFakeTimers(new Date('2024-01-16'));
user.purchased.plan.consecutive.trinkets = 1;
await buyMysterySet(user, { params: { key: '201601' } }, analytics);
await buyMysterySet(user, { params: { key: '201601' } });
expect(user.purchased.plan.consecutive.trinkets).to.eql(0);
expect(user.items.gear.owned).to.have.property('shield_mystery_201601', true);
+2 -5
View File
@@ -12,10 +12,9 @@ describe('shared.ops.buyQuestGems', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGemOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGemOperation(_user, _req);
return buyOp.purchase();
}
@@ -25,13 +24,11 @@ describe('shared.ops.buyQuestGems', () => {
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sinon.useFakeTimers(new Date('2024-01-16'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
+8 -17
View File
@@ -12,21 +12,15 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buyQuest', () => {
let user;
const analytics = { track () {} };
async function buyQuest (_user, _req, _analytics) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req, _analytics);
async function buyQuest (_user, _req) {
const buyOp = new BuyQuestWithGoldOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
it('buys a Quest scroll', async () => {
@@ -35,12 +29,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 1,
});
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('if a user\'s count of a quest scroll is negative, it will be reset to 0 before incrementing when they buy a new one.', async () => {
@@ -49,10 +42,9 @@ describe('shared.ops.buyQuest', () => {
user.items.quests[key] = -1;
await buyQuest(user, {
params: { key },
}, analytics);
});
expect(user.items.quests[key]).to.equal(1);
expect(user.stats.gp).to.equal(5);
expect(analytics.track).to.be.calledOnce;
});
it('buys a Quest scroll with the right quantity if a string is passed for quantity', async () => {
@@ -61,13 +53,13 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress1',
},
}, analytics);
});
await buyQuest(user, {
params: {
key: 'dilatoryDistress1',
},
quantity: '3',
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress1: 4,
@@ -82,7 +74,7 @@ describe('shared.ops.buyQuest', () => {
key: 'dilatoryDistress1',
},
quantity: 'a',
}, analytics);
});
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -187,12 +179,11 @@ describe('shared.ops.buyQuest', () => {
params: {
key: 'dilatoryDistress3',
},
}, analytics);
});
expect(user.items.quests).to.eql({
dilatoryDistress3: 1,
});
expect(user.stats.gp).to.equal(100);
expect(analytics.track).to.be.calledOnce;
});
});
+5 -11
View File
@@ -14,20 +14,17 @@ import { errorMessage } from '../../../../website/common/script/libs/errorMessag
describe('shared.ops.buySpecialSpell', () => {
let user;
let clock;
const analytics = { track () {} };
async function buySpecialSpell (_user, _req, _analytics) {
const buyOp = new BuySpellOperation(_user, _req, _analytics);
async function buySpecialSpell (_user, _req) {
const buyOp = new BuySpellOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
if (clock) {
clock.restore();
}
@@ -78,7 +75,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'thankyou',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.thankyou).to.equal(1);
@@ -89,7 +86,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('buys a limited card when it is available', async () => {
@@ -101,7 +97,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'nye',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.nye).to.equal(1);
@@ -112,7 +108,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the card is not currently available', async () => {
@@ -140,7 +135,7 @@ describe('shared.ops.buySpecialSpell', () => {
params: {
key: 'seafoam',
},
}, analytics);
});
expect(user.stats.gp).to.equal(1);
expect(user.items.special.seafoam).to.equal(1);
@@ -151,7 +146,6 @@ describe('shared.ops.buySpecialSpell', () => {
expect(message).to.equal(i18n.t('messageBought', {
itemText: item.text(),
}));
expect(analytics.track).to.be.calledOnce;
});
it('throws an error if the spell is not currently available', async () => {
+3 -10
View File
@@ -13,21 +13,15 @@ import { BuyHourglassMountOperation } from '../../../../website/common/script/op
describe('common.ops.hourglassPurchase', () => {
let user;
const analytics = { track () {} };
async function buyMount (_user, _req, _analytics) {
const buyOp = new BuyHourglassMountOperation(_user, _req, _analytics);
async function buyMount (_user, _req) {
const buyOp = new BuyHourglassMountOperation(_user, _req);
return buyOp.purchase();
}
beforeEach(() => {
user = generateUser();
sinon.stub(analytics, 'track');
});
afterEach(() => {
analytics.track.restore();
});
context('failure conditions', () => {
@@ -131,12 +125,11 @@ describe('common.ops.hourglassPurchase', () => {
it('buys a pet', async () => {
user.purchased.plan.consecutive.trinkets = 2;
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } }, analytics);
const [, message] = await hourglassPurchase(user, { params: { type: 'pets', key: 'MantisShrimp-Base' } });
expect(message).to.eql(i18n.t('hourglassPurchase'));
expect(user.purchased.plan.consecutive.trinkets).to.eql(1);
expect(user.items.pets).to.eql({ 'MantisShrimp-Base': 5 });
expect(analytics.track).to.be.calledOnce;
});
it('buys a mount', async () => {
+4 -8
View File
@@ -17,20 +17,17 @@ describe('shared.ops.purchase', () => {
let user;
let clock;
const goldPoints = 40;
const analytics = { track () {} };
before(() => {
user = generateUser({ 'stats.class': 'rogue' });
});
beforeEach(() => {
sinon.stub(analytics, 'track');
sinon.spy(pinnedGearUtils, 'removeItemByPath');
clock = sandbox.useFakeTimers(new Date('2024-01-10'));
});
afterEach(() => {
analytics.track.restore();
pinnedGearUtils.removeItemByPath.restore();
clock.restore();
});
@@ -187,11 +184,10 @@ describe('shared.ops.purchase', () => {
const type = 'eggs';
const key = 'Wolf';
await purchase(user, { params: { type, key } }, analytics);
await purchase(user, { params: { type, key } });
expect(user.items[type][key]).to.equal(1);
expect(pinnedGearUtils.removeItemByPath.notCalled).to.equal(true);
expect(analytics.track).to.be.calledOnce;
});
it('purchases hatchingPotions', async () => {
@@ -332,7 +328,7 @@ describe('shared.ops.purchase', () => {
const key = 'Wolf';
try {
await purchase(user, { params: { type, key }, quantity: 'jamboree' }, analytics);
await purchase(user, { params: { type, key }, quantity: 'jamboree' });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -345,7 +341,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: -2 }, analytics);
await purchase(user, { params: { type, key }, quantity: -2 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -358,7 +354,7 @@ describe('shared.ops.purchase', () => {
user.balance = 10;
try {
await purchase(user, { params: { type, key }, quantity: 2.9 }, analytics);
await purchase(user, { params: { type, key }, quantity: 2.9 });
} catch (err) {
expect(err).to.be.an.instanceof(BadRequest);
expect(err.message).to.equal(i18n.t('invalidQuantity'));
@@ -40,7 +40,6 @@ function _requestMaker (user, method, additionalSets = {}) {
|| route.indexOf('/paypal') === 0
|| route.indexOf('/amazon') === 0
|| route.indexOf('/stripe') === 0
|| route.indexOf('/analytics') === 0
) {
url += `${route}`;
} else {
@@ -198,7 +198,6 @@ import dailyIcon from '@/assets/svg/daily.svg?raw';
import todoIcon from '@/assets/svg/todo.svg?raw';
import rewardIcon from '@/assets/svg/reward.svg?raw';
import * as Analytics from '@/libs/analytics';
import { mapState } from '@/libs/store';
export default {
@@ -438,14 +437,6 @@ export default {
return false;
},
changeMirrorPreference (newVal) {
Analytics.track({
eventName: 'mirror tasks',
eventAction: 'mirror tasks',
eventCategory: 'behavior',
hitType: 'event',
mirror: newVal,
group: this.group._id,
}, { trackOnClient: true });
const groupsToMirror = this.user.preferences.tasks.mirrorGroupTasks || [];
if (newVal) { // we're turning copy ON for this group
groupsToMirror.push(this.group._id);
@@ -240,7 +240,6 @@
<script>
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import notifications from '@/mixins/notifications';
import closeX from '../ui/closeX';
@@ -276,11 +275,6 @@ export default {
this.$store.state.party.data = party;
this.user.party._id = party._id;
Analytics.updateUser({
partyID: party._id,
partySize: 1,
});
this.$root.$emit('bv::hide::modal', 'create-party-modal');
await this.$router.push('/party');
},
@@ -314,7 +314,6 @@ import extend from 'lodash/extend';
import groupUtilities from '@/mixins/groupsUtilities';
import styleHelper from '@/mixins/styleHelper';
import { mapGetters } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import participantListModal from './participantListModal';
import groupFormModal from './groupFormModal';
import groupGemsModal from '@/components/groups/groupGemsModal';
@@ -560,7 +559,6 @@ export default {
if (this.isParty) {
data.type = 'party';
Analytics.updateUser({ partySize: null, partyID: null });
this.$store.state.partyMembers = [];
}
@@ -334,7 +334,6 @@ import orderBy from 'lodash/orderBy';
import * as quests from '@/../../common/script/content/quests';
import getItemInfo from '@/../../common/script/libs/getItemInfo';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import navigationBack from '@/assets/svg/navigation_back.svg?raw';
import questDialogContent from '../shops/quests/questDialogContent';
@@ -421,11 +420,6 @@ export default {
async questInit () {
this.loading = true;
Analytics.updateUser({
partyID: this.group._id,
partySize: this.group.memberCount,
});
const groupId = this.group._id || this.user.party._id;
const key = this.selectedQuest;
@@ -123,7 +123,6 @@
<script>
import orderBy from 'lodash/orderBy';
import * as Analytics from '@/libs/analytics';
import { mapGetters, mapActions } from '@/libs/store';
import MemberDetails from '../memberDetails';
import createPartyModal from '../groups/createPartyModal';
@@ -236,22 +235,8 @@ export default {
},
async createOrInviteParty () {
if (this.user.party._id) {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Find Party Members',
});
this.$router.push('/looking-for-party');
} else {
await Analytics.track({
eventName: 'Header Party CTA',
eventAction: 'Header Party CTA',
eventCategory: 'behavior',
hitType: 'event',
state: 'Get Started',
});
this.$root.$emit('bv::show::modal', 'create-party-modal');
}
},
@@ -114,7 +114,6 @@ import { mapState } from '@/libs/store';
import notifications from '@/mixins/notifications';
import guide from '@/mixins/guide';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
import yesterdailyModal from './tasks/yesterdailyModal';
import newStuff from './news/modal';
@@ -648,15 +647,6 @@ export default {
// Reset daily analytics actions
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 0);
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 0);
} else {
// Note a failed cron event, for our records and investigation
Analytics.track({
eventName: 'cron failed',
eventAction: 'cron failed',
eventCategory: 'behavior',
hitType: 'event',
responseCode: response.status,
}, { trackOnClient: true });
}
// Sync
@@ -433,9 +433,6 @@ import lockableLabel from '@/components/tasks/modal-controls/lockableLabel';
import notificationsMixin from '@/mixins/notifications';
import paymentsMixin from '@/mixins/payments';
// analytics
import * as Analytics from '@/libs/analytics';
export default {
components: {
selectTranslatedArray,
@@ -536,16 +533,6 @@ export default {
this.close();
},
submit () {
if (this.paymentData.group && !this.paymentData.newGroup) {
Analytics.track({
hitType: 'event',
eventName: 'group plan upgrade',
eventAction: 'group plan upgrade',
eventCategory: 'behavior',
demographics: this.upgradedGroup.demographics,
type: this.paymentData.group.type,
}, { trackOnClient: true });
}
this.paymentData = {};
this.$root.$emit('bv::hide::modal', 'payments-success-modal');
},
-11
View File
@@ -6,7 +6,6 @@ import { mapState } from '@/libs/store';
import encodeParams from '@/libs/encodeParams';
import notificationsMixin from '@/mixins/notifications';
import { CONSTANTS, setLocalSetting } from '@/libs/userlocalManager';
import * as Analytics from '@/libs/analytics';
const STRIPE_PUB_KEY = import.meta.env.STRIPE_PUB_KEY;
@@ -207,16 +206,6 @@ export default {
alert(`Error while redirecting to Stripe: ${checkoutSessionResult.error.message}`);
throw checkoutSessionResult.error;
}
if (paymentType === 'groupPlan') {
Analytics.track({
hitType: 'event',
eventName: 'group plan create',
eventAction: 'group plan create',
eventCategory: 'behavior',
demographics: appState.newGroup.demographics,
type: appState.newGroup.type,
}, { trackOnClient: true });
}
} catch (err) {
console.error('Error while redirecting to Stripe', err); // eslint-disable-line
alert(`Error while redirecting to Stripe: ${err.message}`);
-10
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import scoreTask from '@/../../common/script/ops/scoreTask';
import notifications from './notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export default {
@@ -58,15 +57,6 @@ export default {
const tasksScoredCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT);
if (!tasksScoredCount || tasksScoredCount < 2) {
Analytics.track({
eventName: 'task scored',
eventAction: 'task scored',
eventCategory: 'behavior',
hitType: 'event',
uuid: user._id,
taskType: task.type,
direction,
}, { trackOnClient: true });
if (!tasksScoredCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_SCORED_COUNT, 1);
} else {
-2
View File
@@ -130,7 +130,6 @@ import PrivacyBanner from '@/components/header/banners/privacy';
import AppFooter from '@/components/appFooter';
import notificationsDisplay from '@/components/notifications';
import { mapState } from '@/libs/store';
import * as Analytics from '@/libs/analytics';
import BuyModal from '@/components/shops/buyModal.vue';
import SelectMembersModal from '@/components/selectMembersModal.vue';
import notifications from '@/mixins/notifications';
@@ -276,7 +275,6 @@ export default {
}
}
Analytics.updateUser();
return this.loadAllTranslations();
}).then(() => {
this.$store.state.isUserLoaded = true;
-10
View File
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import * as Analytics from '@/libs/analytics';
import getStore from '@/store';
import handleRedirect from './handleRedirect';
@@ -318,15 +317,6 @@ router.beforeEach(async (to, from, next) => {
router.app.$root.$emit('update-party');
}
if (to.name === 'lookingForParty') {
Analytics.track({
hitType: 'event',
eventName: 'View Find Members',
eventAction: 'View Find Members',
eventCategory: 'behavior',
}, { trackOnClient: true });
}
// Redirect old guild urls
if (to.hash.indexOf('#/options/groups/guilds/') !== -1) {
const splits = to.hash.split('/');
-8
View File
@@ -1,6 +1,5 @@
import axios from 'axios';
import Vue from 'vue';
import * as Analytics from '@/libs/analytics';
export async function getChat (store, payload) {
const response = await axios.get(`/api/v4/groups/${payload.groupId}/chat?limit=400`);
@@ -17,13 +16,6 @@ export async function postChat (store, payload) {
url += `?previousMsg=${payload.previousMsg}`;
}
if (group.type === 'party') {
Analytics.updateUser({
partyID: group.id,
partySize: group.memberCount,
});
}
const response = await axios.post(url, {
message: payload.message,
});
@@ -1,7 +1,6 @@
import axios from 'axios';
import omit from 'lodash/omit';
import findIndex from 'lodash/findIndex';
import * as Analytics from '@/libs/analytics';
import { loadAsyncResource } from '@/libs/asyncResource';
export async function getPublicGuilds (store, payload) {
@@ -74,7 +73,6 @@ export async function join (store, payload) {
if (invitationI !== -1) invitations.parties.splice(invitationI, 1);
user.party._id = groupId;
Analytics.updateUser({ partyID: groupId });
// load the party members so that they get shown in the header
store.dispatch('party:getMembers');
}
@@ -18,7 +18,6 @@ import * as shops from './shops';
import * as snackbars from './snackbars';
import * as worldState from './worldState';
import * as news from './news';
import * as analytics from './analytics';
import * as faq from './faq';
import * as blockers from './blockers';
@@ -44,7 +43,6 @@ const actions = flattenAndNamespace({
snackbars,
worldState,
news,
analytics,
faq,
blockers,
});
@@ -1,26 +1,6 @@
import axios from 'axios';
import * as Analytics from '@/libs/analytics';
// export async function initQuest (store) {
// }
export async function sendAction (store, payload) { // eslint-disable-line import/prefer-default-export, max-len
// @TODO: Maybe move this to server
let partyData = {};
if (store.state.party && store.state.party.data) {
partyData = {
partyID: store.state.party.data._id,
partySize: store.state.party.data.memberCount,
};
} else {
partyData = {
partyID: store.state.user.data.party._id,
partySize: store.state.partyMembers.data.length,
};
}
Analytics.updateUser(partyData);
const response = await axios.post(`/api/v4/groups/${payload.groupId}/${payload.action}`);
// @TODO: Update user?
-10
View File
@@ -3,7 +3,6 @@ import Vue from 'vue';
import compact from 'lodash/compact';
import omit from 'lodash/omit';
import { loadAsyncResource } from '@/libs/asyncResource';
import * as Analytics from '@/libs/analytics';
import { CONSTANTS, getLocalSetting, setLocalSetting } from '@/libs/userlocalManager';
export function fetchUserTasks (store, options = {}) {
@@ -112,15 +111,6 @@ export async function create (store, createdTask) {
}
const tasksCreatedCount = getLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT);
if (!tasksCreatedCount || tasksCreatedCount < 2) {
const uuid = store.state.user.data._id;
Analytics.track({
eventName: 'task created',
eventAction: 'task created',
eventCategory: 'behavior',
hitType: 'event',
uuid,
taskType: taskRes.type,
}, { trackOnClient: true });
if (!tasksCreatedCount) {
setLocalSetting(CONSTANTS.keyConstants.TASKS_CREATED_COUNT, 1);
} else {
-4
View File
@@ -159,10 +159,6 @@ export default defineConfig({
target: DEV_BASE_URL,
changeOrigin: true,
},
'^/analytics': {
target: DEV_BASE_URL,
changeOrigin: true,
},
}
}
})
+82 -82
View File
@@ -3,9 +3,9 @@
"onwards": "¡Adelante!",
"levelup": "Al cumplir tus objetivos en la vida real, ¡has subido de nivel y has recuperado toda tu salud!",
"reachedLevel": "Has Alcanzado el Nivel <%= level %>",
"achievementLostMasterclasser": "Completista de Aventuras: Serie Arquimaestra",
"achievementLostMasterclasser": "Completador de Aventuras: Serie Arquimaestra",
"achievementLostMasterclasserText": "¡Ha completado las dieciséis misiones en la Serie Arquimaestra y resuelto el misterio de la Arquimaestra Perdida!",
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Arquimaestra y resolviste el misterio de la Arquimaestra Perdida!",
"achievementLostMasterclasserModalText": "¡Completaste las dieciséis misiones en la Serie Maestro de Clases y resolviste el misterio de la Arquimaestra Perdida!",
"achievementMindOverMatter": "La mente sobre la materia",
"achievementMindOverMatterText": "Ha completado las misiones de la Roca, el Limo y el Hilo.",
"achievementMindOverMatterModalText": "¡Completaste las misiones de mascotas de la Roca, el Limo y el Hilo!",
@@ -24,106 +24,106 @@
"achievementAridAuthorityText": "Por domar todas las Monturas Desérticas.",
"achievementAridAuthority": "Autoridad Árida",
"achievementDustDevilModalText": "¡Conseguiste todas las Mascotas Desérticas!",
"achievementDustDevilText": "Ha conseguido todas las Mascotas Desérticas.",
"achievementDustDevilText": "Por conseguir todas las Mascotas Desérticas.",
"achievementDustDevil": "Demonio de Polvo",
"achievementMonsterMagus": "Monstruo Magus",
"achievementUndeadUndertakerModalText": "¡Has domado todas las Monturas Zombi!",
"achievementUndeadUndertakerText": "Ha domado todas las Monturas Zombi.",
"achievementUndeadUndertakerText": "Por domar todas las Monturas Zombie.",
"achievementUndeadUndertaker": "Sepulturero de Muertos Vivientes",
"achievementMonsterMagusModalText": "¡Has conseguido todas las Mascotas Zombi!",
"achievementMonsterMagusText": "Ha conseguido todas las Mascotas Zombi.",
"achievementMonsterMagusText": "Por conseguir todas las Mascotas Zombi.",
"achievementPartyOn": "¡Tu equipo llegó a 4 miembros!",
"achievementPartyUp": "¡Formaste un equipo con alguien más!",
"achievementPearlyProModalText": "¡Has domado todas las Monturas Blancas!",
"achievementPearlyProText": "Ha domado todas las Monturas Blancas.",
"achievementPearlyProModalText": "¡Has domado todas las Mascotas Blancas!",
"achievementPearlyProText": "Ha domado Todas las Mascotas Blancas.",
"achievementPrimedForPaintingModalText": "¡Has conseguido todas las Mascotas Blancas!",
"achievementPrimedForPaintingText": "Ha conseguido todas las Mascotas Blancas.",
"achievementPrimedForPainting": "Preparado para Pintar",
"achievementPrimedForPainting": "Preparado para pintar",
"hideAchievements": "Ocultar <%= category %>",
"showAllAchievements": "Mostrar Todos <%= category %>",
"onboardingCompleteDesc": "Has ganado <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> por completar la lista.",
"showAllAchievements": "Mostrar todo <%= category %>",
"onboardingCompleteDesc": "Has ganado <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> por completar la lista.",
"earnedAchievement": "¡Has conseguido un logro!",
"viewAchievements": "Ver Logros",
"letsGetStarted": "¡Comencemos!",
"onboardingProgress": "<%= percentage %> % de progreso",
"gettingStartedDesc": "¡Completa estas tareas de introducción y ganarás <strong>5 Logros</strong> y <strong class=\"gold-amount\">100 de Oro</strong> una vez hayas terminado!",
"gettingStartedDesc": "¡Completa estas tareas de incorporación y ganarás <strong>5 logros</strong> y <strong class=\"gold-amount\">100 de oro</strong> una vez hayas terminado!",
"achievementCreatedTaskText": "Creó su primera tarea.",
"achievementCreatedTask": "Crea tu primera tarea",
"achievementFedPet": "Alimenta a una Mascota",
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las Mascotas pueden ser selectivas",
"achievementFedPet": "Alimenta a una mascota",
"achievementFedPetModalText": "Hay muchos tipos diferentes de alimentos, pero las mascotas pueden ser exigentes",
"achievementHatchedPetModalText": "Dirígete a tu inventario y prueba a combinar una poción de eclosión y un huevo",
"achievementCompletedTaskText": "Ha completado su primera tarea.",
"achievementPurchasedEquipmentModalText": "El Equipamiento es una forma de personalizar tu avatar y mejorar tus Estadísticas",
"achievementPurchasedEquipment": "Compra una pieza de Equipamiento",
"achievementCompletedTaskModalText": "Marca como completada cualquier tarea para conseguir recompensas",
"achievementCompletedTaskText": "Has completado tu primera tarea.",
"achievementPurchasedEquipmentModalText": "El equipamiento es una forma de personalizar tu avatar y mejorar tus estadísticas",
"achievementPurchasedEquipment": "Compra una pieza de equipamiento",
"achievementCompletedTaskModalText": "Marca alguna de tus tareas para conseguir recompensas",
"achievementCompletedTask": "Completa una tarea",
"achievementCreatedTaskModalText": "Añade una tarea para algo que te gustaría cumplir esta semana",
"achievementFedPetText": "Alimentó a su primera mascota.",
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipamento.",
"achievementPurchasedEquipmentText": "Adquirió su primera pieza de equipo.",
"achievementHatchedPetText": "Eclosionó su primera mascota.",
"achievementHatchedPet": "Eclosiona una Mascota",
"achievementHatchedPet": "Eclosiona una mascota",
"achievementPearlyPro": "Coleccionista de Perlas",
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de Algodón de Azúcar Rosa!",
"achievementTickledPinkText": "Ha conseguido todas las mascotas de Algodón de Azúcar Rosa.",
"achievementTickledPinkModalText": "¡Has conseguido todas las mascotas de algodón de azúcar rosa!",
"achievementTickledPinkText": "Ha conseguido todas las mascotas de algodón de azúcar rosa.",
"achievementTickledPink": "cosquillas rosa",
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción de eclosión y un huevo!",
"foundNewItemsCTA": "¡Dirígete a tu Inventario e intenta combinar tu nueva poción para incubar y un huevo!",
"foundNewItemsExplanation": "Completar tareas te da la oportunidad de encontrar objetos, como Huevos, Pociones de Eclosión y Alimento para Mascotas.",
"foundNewItems": "¡Encontraste nuevos artículos!",
"onboardingCompleteDescSmall": "¡Si quieres aún más, mira tus Logros y empieza a coleccionarlos!",
"yourProgress": "Tu Progreso",
"onboardingComplete": "¡Has completado tus tareas de introducción!",
"achievementRosyOutlookModalText": "¡Has domado todas las Monturas de Algodón de Azúcar Rosa!",
"achievementRosyOutlookText": "Ha domado todas las Monturas de Algodón de Azúcar Rosa.",
"achievementRosyOutlook": "Perspectiva Optimista",
"achievementBareNecessities": "Necesidades Básicas",
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña!",
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas Escarabajo, Mariposa, Caracol y Araña.",
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas Mono, Perezoso y Brote!",
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas Mono, Perezoso y Brote.",
"onboardingComplete": "¡Has completado tus tareas de incorporación!",
"achievementRosyOutlookModalText": "¡Has domado todas las monturas de algodón de azúcar rosa!",
"achievementRosyOutlookText": "Ha domado todas las monturas de algodón de azúcar rosa.",
"achievementRosyOutlook": "Perspectiva optimista",
"achievementBareNecessities": "Necesidades básicas",
"achievementBugBonanzaModalText": "¡Has completado las misiones de las mascotas escarabajo, mariposa, caracol y araña!",
"achievementBugBonanzaText": "Ha completado las misiones de las mascotas escarabajo, mariposa, caracol y araña.",
"achievementBareNecessitiesModalText": "¡Has completado las misiones de las mascotas mono, perezoso y brote!",
"achievementBareNecessitiesText": "Ha completado las misiones de las mascotas mono, perezoso y brote.",
"achievementBugBonanza": "Bonanza insectil",
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas Ajolote, Rana e Hipopótamo!",
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas Ajolote, Rana, e Hipopótamo.",
"achievementFreshwaterFriends": "Amigos de Agua Dulce",
"achievementFreshwaterFriendsModalText": "¡Has completado las misiones de las mascotas ajolote, rana e hipopótamo!",
"achievementFreshwaterFriendsText": "Ha completado las misiones de las mascotas ajolote, rana, e hipopótamo.",
"achievementFreshwaterFriends": "Amigos de agua dulce",
"achievementAllThatGlittersModalText": "¡Has domado todas las Monturas Doradas!",
"achievementAllThatGlittersText": "Ha domado todas las Monturas Doradas.",
"achievementGoodAsGoldModalText": "¡Has conseguido todas las Mascotas Doradas!",
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas Doradas.",
"achievementGoodAsGold": "Corazón de Oro",
"achievementGoodAsGoldText": "Ha conseguido todas las Mascotas doradas.",
"achievementGoodAsGold": "Más Bueno que el Pan",
"yourRewards": "Tus Recompensas",
"achievementAllThatGlitters": "Todo lo que Brilla",
"achievementBoneCollector": "Coleccionista de Huesos",
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas Esqueleto.",
"achievementAllThatGlitters": "Todo lo que brilla",
"achievementBoneCollector": "Coleccionista de huesos",
"achievementBoneCollectorText": "Ha conseguido todas las Mascotas esqueléticas.",
"achievementSeeingRed": "Rojo de Ira",
"achievementSkeletonCrewModalText": "¡Has domado todas las Monturas Esqueleto!",
"achievementSkeletonCrewText": "Ha domado todas las Monturas Esqueleto.",
"achievementSkeletonCrew": "Ejército Huesudo",
"achievementBoneCollectorModalText": "¡Has conseguido todas las Mascotas Esqueleto!",
"achievementSeeingRedText": "Ha conseguido todas las Mascotas Rojas.",
"achievementSeeingRedModalText": "¡Has conseguido todas las Mascotas Rojas!",
"achievementRedLetterDayModalText": "¡Has domado todas las Monturas Rojas!",
"achievementRedLetterDayText": "Ha domado todas las Monturas Rojas.",
"achievementSkeletonCrewModalText": "¡Has domado todas las monturas esqueléticas!",
"achievementSkeletonCrewText": "Ha domado todas las monturas esqueléticas.",
"achievementSkeletonCrew": "Equipo esquelético",
"achievementBoneCollectorModalText": "¡Has conseguido todas las mascotas esqueléticas!",
"achievementSeeingRedText": "Ha conseguido todas las Mascotas rojas.",
"achievementSeeingRedModalText": "¡Has conseguido todas las mascotas rojas!",
"achievementRedLetterDayModalText": "¡Has domado todas las monturas rojas!",
"achievementRedLetterDayText": "Ha domado todas las monturas rojas.",
"achievementRedLetterDay": "Día señalado",
"achievementLegendaryBestiaryModalText": "¡Has conseguido todas las mascotas míticas!",
"achievementLegendaryBestiaryText": "¡Ha eclosionado todos los colores estándar de todas las mascotas míticas: Dragón, Cerdo Volador, Grifo, Serpiente Marina y Unicornio!",
"achievementLegendaryBestiary": "Bestiario Legendario",
"achievementLegendaryBestiary": "Bestiario legendario",
"achievementSeasonalSpecialistModalText": "¡Completaste todas las misiones estacionales!",
"achievementSeasonalSpecialistText": "Ha completado todas las misiones estacionales de Primavera e Invierno: ¡Búsqueda de Huevos, Santa Trampero, y Encuentra al Cachorro!",
"achievementSeasonalSpecialist": "Especialista Estacional",
"achievementVioletsAreBlue": "Las Violetas son Azules",
"achievementVioletsAreBlue": "Las violetas son azules",
"achievementWildBlueYonderModalText": "¡Has domado todas las monturas de Algodon de Azúcar Azul!",
"achievementWildBlueYonderText": "Ha domado todas las monturas de Algodon de Azúcar Azul.",
"achievementWildBlueYonder": "La Salvaje y Azul Lejanía",
"achievementWildBlueYonder": "La salvaje y azul lejanía",
"achievementVioletsAreBlueModalText": "¡Has conseguido todas las mascotas de Algodon de Azúcar Azul!",
"achievementVioletsAreBlueText": "Ha conseguido todas las mascotas de Algodon de Azúcar Azul.",
"achievementDomesticatedText": "¡Ha eclosionado todos los colores de mascotas domésticas: Hurón, Cobaya, Gallo, Cerdo Volador, Rata, Conejito, Caballo y Vaca!",
"achievementDomesticated": "I-A-I-A-O",
"achievementDomesticatedText": "¡Has criado todos los colores de mascotas domésticas: hurón, cobaya, gallo, cerdo volador, rata, conejito, caballo y vaca!",
"achievementDomesticated": "E-I-E-I-O",
"achievementDomesticatedModalText": "¡Has conseguido todas las mascotas domesticadas!",
"achievementShadyCustomerModalText": "¡Has conseguido todas las Mascotas Sombrías!",
"achievementShadeOfItAll": "A la Sombra de Todo",
"achievementShadeOfItAllText": "Ha domado todas las Monturas Sombrías.",
"achievementShadeOfItAllModalText": "¡Has domado todas las Monturas Sombrías!",
"achievementShadyCustomerText": "Ha conseguido todas las Mascotas Sombrías.",
"achievementShadyCustomer": "Cliente Sombrío",
"achievementShadyCustomerModalText": "¡Has conseguido todas las mascotas sombrías!",
"achievementShadeOfItAll": "La sombra de todo ello",
"achievementShadeOfItAllText": "Ha domado todas las monturas sombrías.",
"achievementShadeOfItAllModalText": "¡Has domado todas las monturas sombrías!",
"achievementShadyCustomerText": "Ha conseguido todas las mascotas sombrías.",
"achievementShadyCustomer": "Cliente sombrío",
"achievementZodiacZookeeper": "Cuidador del Zodiaco",
"achievementZodiacZookeeperText": "¡Ha eclosionado todas las mascotas del zodíaco de color básico: Rata, Vaca, Conejo, Serpiente, Caballo, Oveja, Mono, Gallo, Lobo, Tigre, Cerdo Volador y Dragón!",
"achievementZodiacZookeeperModalText": "¡Has conseguido todas las mascotas del zodíaco!",
@@ -131,39 +131,39 @@
"achievementBirdsOfAFeatherModalText": "¡Has conseguido todas las mascotas voladoras!",
"achievementBirdsOfAFeather": "Aves de Pluma",
"achievementReptacularRumbleModalText": "¡Has coleccionado todas las mascotas reptiles!",
"achievementReptacularRumbleText": "¡Ha eclosionado todos los colores estándar de las mascotas reptiles: Caimán, Pterodáctilo, Serpiente, Triceratops, Tortuga, Tiranosaurio Rex y Velociraptor!",
"achievementGroupsBeta2022": "Probador Beta Interactivo",
"achievementGroupsBeta2022Text": "Tú y tu grupo proporcionaron comentarios invaluables para ayudar a probar Habitica.",
"achievementReptacularRumbleText": "!Has incubado todos los colores estándar de las mascotas reptiles: caimán, pterodáctilo, serpiente, triceratops, tortuga, tiranosaurio rex y velociraptor!",
"achievementGroupsBeta2022": "Probador Beta interactivo",
"achievementGroupsBeta2022Text": "Tú y tu grupo habéis proporcionado comentarios invaluables para ayudar a probar Habitica.",
"achievementGroupsBeta2022ModalText": "¡Usted y sus grupos ayudaron a Habitica probando y proporcionando comentarios!",
"achievementReptacularRumble": "Rumble reptacular",
"achievementWoodlandWizard": "Mago del Bosque",
"achievementWoodlandWizardModalText": "¡Has conseguido todas las mascotas del bosque!",
"achievementWoodlandWizardText": "¡Ha eclosionado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Brote!",
"achievementBoneToPick": "Un Hueso Duro de Roer",
"achievementBoneToPickText": "¡Ha eclosionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
"achievementWoodlandWizard": "Mago del bosque",
"achievementWoodlandWizardModalText": "¡Has recogido todas las mascotas del bosque!",
"achievementWoodlandWizardText": "Ha incubado todos los colores estándar de las criaturas del bosque: Tejón, Oso, Ciervo, Zorro, Rana, Erizo, Búho, Caracol, Ardilla y Treeling!",
"achievementBoneToPick": "Un hueso duro de roer",
"achievementBoneToPickText": "¡Ha eclosionado todas las mascotas Clásicas y de Misión Esqueléticas!",
"achievementPolarPro": "Experto Polar",
"achievementPolarProModalText": "¡Has conseguido todas las Mascotas Polares!",
"achievementBoneToPickModalText": "¡Has coleccionado todas las Mascotas Esqueleto, Clásicas y de Misión!",
"achievementPolarProModalText": "¡Has coleccionado todas las mascotas Polares!",
"achievementBoneToPickModalText": "¡Has coleccionado todas las mascotas clásicas y de misiones esqueléticas!",
"achievementPolarProText": "¡Ha eclosionado todos los colores estándar para mascotas Polares: Osos, Zorros, Pinguinos, Ballenas y Lobos!",
"achievementPlantParent": "Cuidador de Plantas",
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Brote!",
"achievementPlantParentModalText": "¡Has conseguido todas las Mascotas Planta!",
"achievementPlantParentText": "¡Ha eclosionado todos los colores estándar de mascotas vegetales: Cactus y Árbolito!",
"achievementPlantParentModalText": "¡Has coleccionado todas las Mascotas Planta!",
"achievementDinosaurDynasty": "Dinastía de Dinosaurios",
"achievementDinosaurDynastyModalText": "¡Has conseguido todas las mascotas ave y dinosaurio!",
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas ave y dinosaurio: Halcón, Búho, Loro, Pavo Real, Pingüino, Gallo, Pterodáctilo, Tiranosaurio rex, Triceratops y Velociraptor!",
"achievementDinosaurDynastyModalText": "¡Has recogido todas las mascotas de pájaros y dinosaurios!",
"achievementDinosaurDynastyText": "Ha eclosionado todos los colores estándar de mascotas, de aves y dinosaurios: halcón, búho, loro, pavo real, pingüino, gallo, pterodáctilo, tiranosaurio rex, triceratops y velociraptor!",
"achievementRoughRider": "Jinete tosco",
"achievementRoughRiderText": "¡Ha eclosionado todos los colores estándar de mascotas y monturas incómodas: Cactus, Erizo y Piedra!",
"achievementBonelessBossModalText": Has conseguido todas las mascotas invertebradas!",
"achievementBonelessBossText": "¡Ha eclosionado todos los colores estándar de mascotas invertebradas: Escarabajo, Mariposa, Calamar, Nudibranquio, Pulpo, Caracol y Araña!",
"achievementBonelessBoss": "Jefe Deshuesado",
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: Armadillo, Cactus, Zorro, Rana, Serpiente y Araña!",
"achievementBonelessBossModalText": Tienes todas las mascotas invertebradas en tu colección!",
"achievementBonelessBossText": "Ha eclosionado todos los colores estándar de mascotas invertebradas: escarabajo, mariposa calamar, nudibranquio, pulpo, caracol y araña!",
"achievementBonelessBoss": "Jefe deshuesado",
"achievementDuneBuddyText": "¡Ha eclosionado todos los colores estándar de mascotas de clima desértico: armadillo, cactus, zorro, rana, serpiente y araña!",
"achievementDuneBuddy": "Amigo de médano",
"achievementDuneBuddyModalText": "¡Has conseguido todas las mascotas de desierto!",
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de mascotas y monturas incómodas!",
"achievementRodentRuler": "Rey Roedor",
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
"achievementRodentRulerModalText": "¡Has conseguido todos las mascotas roedores!",
"achievementCats": "Señora de los Gatos",
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
"achievementRoughRiderModalText": "¡Has conseguido todos los colores básicos de las mascotas y monturas incómodas!",
"achievementRodentRuler": "Gobernante Roedor",
"achievementRodentRulerText": "¡Ha eclosionado todos los colores estándar de las mascotas roedores: Conejillo de Indias, Rata y Ardilla!",
"achievementRodentRulerModalText": "¡Has conseguido todos las roedores mascota!",
"achievementCats": "Pastor de Gatos",
"achievementCatsText": "¡Ha eclosionado todos los colores estándar de las mascotas felinas: Guepardo, León, Tigre Dientes de Sable y Tigre!",
"achievementCatsModalText": "¡Has conseguido todas las mascotas felinas!"
}
+114 -125
View File
@@ -1,203 +1,203 @@
{
"backgrounds": "Fondos",
"background": "Fondo",
"backgroundShop": "Tienda de Fondos",
"noBackground": "Ningún Fondo Seleccionado",
"backgroundShop": "Tienda de fondos",
"noBackground": "Ningún fondo seleccionado",
"backgrounds062014": "1.ª serie: publicada en junio de 2014",
"backgroundBeachText": "Playa",
"backgroundBeachNotes": "Relájate en una cálida playa.",
"backgroundFairyRingText": "Anillo de Hadas",
"backgroundFairyRingText": "Anillo de hadas",
"backgroundFairyRingNotes": "Baila en un anillo de hadas.",
"backgroundForestText": "Bosque",
"backgroundForestNotes": "Pasea por un bosque estival.",
"backgrounds072014": "2.ª serie: publicada en julio de 2014",
"backgroundCoralReefText": "Arrecife de Coral",
"backgroundCoralReefText": "Arrecife de coral",
"backgroundCoralReefNotes": "Nada en un arrecife de coral.",
"backgroundOpenWatersText": "Aguas Abiertas",
"backgroundOpenWatersText": "Aguas abiertas",
"backgroundOpenWatersNotes": "Disfruta de las aguas abiertas.",
"backgroundSeafarerShipText": "Bajel de Marineros",
"backgroundSeafarerShipNotes": "Navega a bordo de un Barco Marinero.",
"backgroundSeafarerShipText": "Bajel de marineros",
"backgroundSeafarerShipNotes": "Navega a bordo de un barco marinero.",
"backgrounds082014": "3.ª serie: publicada en agosto de 2014",
"backgroundCloudsText": "Nubes",
"backgroundCloudsNotes": "Planea entre las nubes.",
"backgroundDustyCanyonsText": "Cañón Polvoriento",
"backgroundDustyCanyonsText": "Cañón polvoriento",
"backgroundDustyCanyonsNotes": "Pasea por un cañón polvoriento.",
"backgroundVolcanoText": "Volcán",
"backgroundVolcanoNotes": "Entra en calor dentro de un volcán.",
"backgrounds092014": "4.ª serie: publicada en septiembre de 2014",
"backgroundThunderstormText": "Tormenta Eléctrica",
"backgroundThunderstormText": "Tormenta eléctrica",
"backgroundThunderstormNotes": "Conduce rayos en la tormenta eléctrica.",
"backgroundAutumnForestText": "Bosque Otoñal",
"backgroundAutumnForestText": "Bosque otoñal",
"backgroundAutumnForestNotes": "Pasea por un bosque otoñal.",
"backgroundHarvestFieldsText": "Campos de Cultivo",
"backgroundHarvestFieldsText": "Campos de cultivo",
"backgroundHarvestFieldsNotes": "Labra tus campos de cultivo.",
"backgrounds102014": "5.ª serie: publicada en octubre de 2014",
"backgroundGraveyardText": "Cementerio",
"backgroundGraveyardNotes": "Visita un espeluznante cementerio.",
"backgroundHauntedHouseText": "Casa Encantada",
"backgroundHauntedHouseText": "Casa encantada",
"backgroundHauntedHouseNotes": "Entra a hurtadillas en una casa encantada.",
"backgroundPumpkinPatchText": "Campo de Calabazas",
"backgroundPumpkinPatchNotes": "Talla tus calabazas en este campo de calabazas.",
"backgroundPumpkinPatchText": "Terreno de calabazas",
"backgroundPumpkinPatchNotes": "Talla tu calabaza en este terreno.",
"backgrounds112014": "6.ª serie: publicada en noviembre de 2014",
"backgroundHarvestFeastText": "Festín de la Cosecha",
"backgroundHarvestFeastNotes": "Disfruta del Banquete de la Cosecha.",
"backgroundStarrySkiesText": "Cielos Estrellados",
"backgroundHarvestFeastText": "Festín de la cosecha",
"backgroundHarvestFeastNotes": "Disfruta del banquete de la cosecha.",
"backgroundStarrySkiesText": "Cielos estrellados",
"backgroundStarrySkiesNotes": "Contempla los cielos repletos de estrellas.",
"backgroundSunsetMeadowText": "Atardecer en la Pradera",
"backgroundSunsetMeadowText": "Atardecer en la pradera",
"backgroundSunsetMeadowNotes": "Admira un atardecer en la pradera.",
"backgrounds122014": "7.ª serie: publicada en diciembre de 2014",
"backgroundIcebergText": "Témpano de Hielo",
"backgroundIcebergNotes": "Flota a la deriva sobre un Témpano de Hielo.",
"backgroundTwinklyLightsText": "Luces Brillantes de Invierno",
"backgroundIcebergText": "Iceberg",
"backgroundIcebergNotes": "Flota a la deriva sobre un iceberg.",
"backgroundTwinklyLightsText": "Luces brillantes de invierno",
"backgroundTwinklyLightsNotes": "Camina entre árboles engalanados con luces festivas.",
"backgroundSouthPoleText": "Polo Sur",
"backgroundSouthPoleNotes": "Visita el gélido Polo Sur.",
"backgrounds012015": "8.ª serie: publicada en enero de 2015",
"backgroundIceCaveText": "Cueva de Hielo",
"backgroundIceCaveNotes": "Desciende y adéntrate en una Cueva de Hielo.",
"backgroundFrigidPeakText": "Cima Glacial",
"backgroundIceCaveText": "Cueva de hielo",
"backgroundIceCaveNotes": "Desciende y adéntrate en una cueva de hielo.",
"backgroundFrigidPeakText": "Cima glacial",
"backgroundFrigidPeakNotes": "Escala una cima glacial.",
"backgroundSnowyPinesText": "Pinos Nevados",
"backgroundSnowyPinesText": "Pinos nevados",
"backgroundSnowyPinesNotes": "Refúgiate entre pinos nevados.",
"backgrounds022015": "9.ª serie: publicada en febrero de 2015",
"backgroundBlacksmithyText": "Forja",
"backgroundBlacksmithyNotes": "Trabaja en la forja.",
"backgroundCrystalCaveText": "Cueva de Cristal",
"backgroundCrystalCaveText": "Cueva de cristal",
"backgroundCrystalCaveNotes": "Explora una cueva de cristal.",
"backgroundDistantCastleText": "Castillo Distante",
"backgroundDistantCastleText": "Castillo distante",
"backgroundDistantCastleNotes": "Defiende un castillo distante.",
"backgrounds032015": "10.ª serie: publicada en marzo de 2015",
"backgroundSpringRainText": "Lluvia Primaveral",
"backgroundSpringRainText": "Lluvia primaveral",
"backgroundSpringRainNotes": "Baila bajo la lluvia de primavera.",
"backgroundStainedGlassText": "Vidriera",
"backgroundStainedGlassNotes": "Contempla las vidrieras.",
"backgroundRollingHillsText": "Colinas Ondulantes",
"backgroundRollingHillsText": "Colinas ondulantes",
"backgroundRollingHillsNotes": "Corretea por las colinas ondulantes.",
"backgrounds042015": "11.ª serie: publicada en abril de 2015",
"backgroundCherryTreesText": "Cerezos",
"backgroundCherryTreesNotes": "Admira los cerezos en flor.",
"backgroundFloralMeadowText": "Prado Floreciente",
"backgroundFloralMeadowText": "Prado floreciente",
"backgroundFloralMeadowNotes": "Ve de pícnic a un prado floreciente.",
"backgroundGumdropLandText": "País de las Gominolas",
"backgroundGumdropLandNotes": "Mordisquea el paisaje del País de las Gominolas.",
"backgrounds052015": "12.ª serie: publicada en mayo de 2015",
"backgroundMarbleTempleText": "Templo de Mármol",
"backgroundMarbleTempleText": "Templo de mármol",
"backgroundMarbleTempleNotes": "Posa delante de un templo de mármol.",
"backgroundMountainLakeText": "Lago de Montaña",
"backgroundMountainLakeText": "Lago de montaña",
"backgroundMountainLakeNotes": "Atrévete a sumergir la punta del pie en este lago de montaña.",
"backgroundPagodasText": "Pagodas",
"backgroundPagodasNotes": "Sube a lo alto de las Pagodas.",
"backgroundPagodasNotes": "Sube a lo alto de las pagodas.",
"backgrounds062015": "13.ª serie: publicada en junio de 2015",
"backgroundDriftingRaftText": "Balsa a la Deriva",
"backgroundDriftingRaftText": "Balsa a la deriva",
"backgroundDriftingRaftNotes": "Rema sobre una balsa a la deriva.",
"backgroundShimmeryBubblesText": "Burbujas Relucientes",
"backgroundShimmeryBubblesText": "Burbujas relucientes",
"backgroundShimmeryBubblesNotes": "Flota a través de un mar de burbujas relucientes.",
"backgroundIslandWaterfallsText": "Cascadas Isleñas",
"backgroundIslandWaterfallsText": "Cascadas isleñas",
"backgroundIslandWaterfallsNotes": "Haz un pícnic junto a las cascadas de esta isla.",
"backgrounds072015": "14.ª serie: publicada en julio de 2015",
"backgroundDilatoryRuinsText": "Ruinas de Dilatoria",
"backgroundDilatoryRuinsNotes": "Sumérgete en las ruinas de Dilatoria.",
"backgroundGiantWaveText": "Ola Gigante",
"backgroundGiantWaveText": "Ola gigante",
"backgroundGiantWaveNotes": "¡Surfea una ola gigante!",
"backgroundSunkenShipText": "Barco Hundido",
"backgroundSunkenShipText": "Barco hundido",
"backgroundSunkenShipNotes": "Explora un barco hundido.",
"backgrounds082015": "15.ª serie: publicada en agosto de 2015",
"backgroundPyramidsText": "Pirámides",
"backgroundPyramidsNotes": "Admira las pirámides.",
"backgroundSunsetSavannahText": "Ocaso en la Sabana",
"backgroundSunsetSavannahText": "Ocaso en la sabana",
"backgroundSunsetSavannahNotes": "Acecha a tus presas al atardecer en la sabana.",
"backgroundTwinklyPartyLightsText": "Luces Parpadeantes de Fiesta",
"backgroundTwinklyPartyLightsText": "Luces parpadeantes de fiesta",
"backgroundTwinklyPartyLightsNotes": "¡Baila bajo las luces festivas centelleantes!",
"backgrounds092015": "16.ª serie: publicada en septiembre de 2015",
"backgroundMarketText": "Mercado de Habitica",
"backgroundMarketNotes": "Compra en el Mercado de Habitica.",
"backgroundMarketNotes": "Compra en el mercado de Habitica.",
"backgroundStableText": "Establo de Habitica",
"backgroundStableNotes": "Cuida a tus monturas en el Establo de Habitica.",
"backgroundStableNotes": "Cuida a tus monturas en el establo de Habitica.",
"backgroundTavernText": "Taberna de Habitica",
"backgroundTavernNotes": "Visita la Taberna de Habitica.",
"backgrounds102015": "17.ª serie: publicada en octubre de 2015",
"backgroundHarvestMoonText": "Luna de Cosecha",
"backgroundHarvestMoonText": "Luna de cosecha",
"backgroundHarvestMoonNotes": "Ríete a carcajadas bajo la luna de cosecha.",
"backgroundSlimySwampText": "Pantano Lodoso",
"backgroundSlimySwampText": "Pantano lodoso",
"backgroundSlimySwampNotes": "Cruza con esfuerzo el pantano lodoso.",
"backgroundSwarmingDarknessText": "Criaturas de la Oscuridad",
"backgroundSwarmingDarknessText": "Criaturas de la oscuridad",
"backgroundSwarmingDarknessNotes": "Tiembla entre las criaturas de la oscuridad.",
"backgrounds112015": "18.ª serie: publicada en noviembre de 2015",
"backgroundFloatingIslandsText": "Islas Flotantes",
"backgroundFloatingIslandsText": "Islas flotantes",
"backgroundFloatingIslandsNotes": "Salta entre las islas flotantes.",
"backgroundNightDunesText": "Dunas Nocturnas",
"backgroundNightDunesText": "Dunas nocturnas",
"backgroundNightDunesNotes": "Camina tranquilamente por las dunas nocturnas.",
"backgroundSunsetOasisText": "Oasis al Atardecer",
"backgroundSunsetOasisText": "Oasis al atardecer",
"backgroundSunsetOasisNotes": "Disfruta del oasis al atardecer.",
"backgrounds122015": "19.ª serie: publicada en diciembre de 2015",
"backgroundAlpineSlopesText": "Laderas Alpinas",
"backgroundAlpineSlopesText": "Laderas alpinas",
"backgroundAlpineSlopesNotes": "Esquía en las laderas alpinas.",
"backgroundSnowySunriseText": "Amanecer Nevado",
"backgroundSnowySunriseText": "Amanecer nevado",
"backgroundSnowySunriseNotes": "Contempla el amanecer nevado.",
"backgroundWinterTownText": "Pueblo Invernal",
"backgroundWinterTownText": "Pueblo invernal",
"backgroundWinterTownNotes": "Camina deprisa por el pueblo invernal.",
"backgrounds012016": "20.ª serie: publicada en enero de 2016",
"backgroundFrozenLakeText": "Lago Congelado",
"backgroundFrozenLakeText": "Lago congelado",
"backgroundFrozenLakeNotes": "Patina sobre un lago congelado.",
"backgroundSnowmanArmyText": "Ejército de Muñecos de Nieve",
"backgroundSnowmanArmyText": "Ejército de muñecos de nieve",
"backgroundSnowmanArmyNotes": "Lidera un ejército de muñecos de nieve.",
"backgroundWinterNightText": "Noche de Invierno",
"backgroundWinterNightText": "Noche de invierno",
"backgroundWinterNightNotes": "Mira las estrellas de una noche de invierno.",
"backgrounds022016": "21.ª serie: publicada en febrero de 2016",
"backgroundBambooForestText": "Bosque de Bambú",
"backgroundBambooForestText": "Bosque de bambú",
"backgroundBambooForestNotes": "Pasea por el bosque de bambú.",
"backgroundCozyLibraryText": "Biblioteca Acogedora",
"backgroundCozyLibraryText": "Biblioteca acogedora",
"backgroundCozyLibraryNotes": "Lee en esta acogedora biblioteca.",
"backgroundGrandStaircaseText": "Gran Escalinata",
"backgroundGrandStaircaseText": "Gran escalinata",
"backgroundGrandStaircaseNotes": "Desciende por la gran escalinata.",
"backgrounds032016": "22.ª serie: publicada en marzo de 2016",
"backgroundDeepMineText": "Mina Profunda",
"backgroundDeepMineText": "Mina profunda",
"backgroundDeepMineNotes": "Encuentra metales preciosos en esta mina profunda.",
"backgroundRainforestText": "Selva Tropical",
"backgroundRainforestText": "Selva tropical",
"backgroundRainforestNotes": "Adéntrate en la selva tropical.",
"backgroundStoneCircleText": "Crómlech",
"backgroundStoneCircleNotes": "Lanza hechizos en este crómlech.",
"backgrounds042016": "23.ª serie: publicada en abril de 2016",
"backgroundArcheryRangeText": "Campo de Tiro con Arco",
"backgroundArcheryRangeText": "Campo de tiro con arco",
"backgroundArcheryRangeNotes": "Practica en este campo de tiro con arco.",
"backgroundGiantFlowersText": "Flores Gigantes",
"backgroundGiantFlowersText": "Flores gigantes",
"backgroundGiantFlowersNotes": "Diviértete sobre estas gigantescas flores.",
"backgroundRainbowsEndText": "Final del Arcoíris",
"backgroundRainbowsEndText": "Final del arcoíris",
"backgroundRainbowsEndNotes": "Encuentra oro al final del arcoíris.",
"backgrounds052016": "24.ª serie: publicada en mayo de 2016",
"backgroundBeehiveText": "Colmena",
"backgroundBeehiveNotes": "Zumba y baila en una colmena.",
"backgroundGazeboText": "Kiosko",
"backgroundGazeboNotes": "Pelea en un kiosko.",
"backgroundTreeRootsText": "Raíces de Árbol",
"backgroundTreeRootsText": "Raíces de árbol",
"backgroundTreeRootsNotes": "Explora las raíces del árbol.",
"backgrounds062016": "25.ª serie: publicada en junio de 2016",
"backgroundLighthouseShoreText": "Costa con Faro",
"backgroundLighthouseShoreText": "Costa con faro",
"backgroundLighthouseShoreNotes": "Pasea por la costa junto al faro.",
"backgroundLilypadText": "Nenúfar",
"backgroundLilypadNotes": "Salta sobre un nenúfar.",
"backgroundWaterfallRockText": "Roca de Cascada",
"backgroundWaterfallRockText": "Roca de cascada",
"backgroundWaterfallRockNotes": "Chapotea junto a la roca de la cascada.",
"backgrounds072016": "26.ª serie: publicada en julio de 2016",
"backgroundAquariumText": "Acuario",
"backgroundAquariumNotes": "Sube y baja dentro de este acuario.",
"backgroundDeepSeaText": "Mar Profundo",
"backgroundDeepSeaText": "Profundidades del mar",
"backgroundDeepSeaNotes": "Bucea hasta las profundidades del mar.",
"backgroundDilatoryCastleText": "Castillo de Dilatoria",
"backgroundDilatoryCastleNotes": "Nada junto al Castillo de Dilatoria.",
"backgrounds082016": "27.ª serie: publicada en agosto de 2016",
"backgroundIdyllicCabinText": "Cabaña Idílica",
"backgroundIdyllicCabinNotes": "Retírate a una cabaña idílica.",
"backgroundMountainPyramidText": "Pirámide de Montaña",
"backgroundIdyllicCabinText": "Cabaña bucólica",
"backgroundIdyllicCabinNotes": "Retírate a una cabaña bucólica.",
"backgroundMountainPyramidText": "Pirámide de montaña",
"backgroundMountainPyramidNotes": "Sube los incontables peldaños de esta pirámide de montaña.",
"backgroundStormyShipText": "Barco Tormentoso",
"backgroundStormyShipText": "Barco tormentoso",
"backgroundStormyShipNotes": "Agárrate fuerte contra viento y marea a bordo de un barco tormentoso.",
"backgrounds092016": "28.ª serie: publicada en septiembre de 2016",
"backgroundCornfieldsText": "Campos de Maíz",
"backgroundCornfieldsText": "Campos de maíz",
"backgroundCornfieldsNotes": "Disfruta de un bonito día en los campos de maíz.",
"backgroundFarmhouseText": "Casa de Granja",
"backgroundFarmhouseText": "Casa de granja",
"backgroundFarmhouseNotes": "Saluda a los animales de camino a la casa de la granja.",
"backgroundOrchardText": "Huerto de Árboles Frutales",
"backgroundOrchardText": "Huerto de árboles frutales",
"backgroundOrchardNotes": "Coge fruta madura en el huerto.",
"backgrounds102016": "29.ª serie: publicada en octubre de 2016",
"backgroundSpiderWebText": "Telaraña",
@@ -207,14 +207,14 @@
"backgroundRainyCityText": "Ciudad Lluviosa",
"backgroundRainyCityNotes": "Chapotea a través de una Ciudad Lluviosa.",
"backgrounds112016": "30.ª serie: publicada en noviembre de 2016",
"backgroundMidnightCloudsText": "Nubes de Medianoche",
"backgroundMidnightCloudsNotes": "Vuela a través de las nubes de medianoche.",
"backgroundStormyRooftopsText": "Techos Tormentosos",
"backgroundStormyRooftopsNotes": "Deslízate a través de los techos tormentosos.",
"backgroundMidnightCloudsText": "Media noche nublada",
"backgroundMidnightCloudsNotes": "Vuela a través de la Media noche nublada.",
"backgroundStormyRooftopsText": "Tempestuoso Techo",
"backgroundStormyRooftopsNotes": "Deslízate a través del Tempestuoso Techo.",
"backgroundWindyAutumnText": "Otoño Ventoso",
"backgroundWindyAutumnNotes": "Persigue las hojas durante el otoño ventoso.",
"backgroundWindyAutumnNotes": "Caza las hojas durante el Otoño Ventoso.",
"incentiveBackgrounds": "Fondos Estándar",
"backgroundVioletText": "Violeta",
"backgroundVioletText": "violeta",
"backgroundVioletNotes": "Un vibrante fondo violeta.",
"backgroundBlueText": "Azul",
"backgroundBlueNotes": "Un fondo básico azul.",
@@ -228,16 +228,16 @@
"backgroundYellowNotes": "Un agradable fondo amarillo.",
"backgrounds122016": "31.ª serie: publicada en diciembre de 2016",
"backgroundShimmeringIcePrismText": "Prismas de Hielo Relucientes",
"backgroundShimmeringIcePrismNotes": "Baila junto a los prismas de hielo relucientes.",
"backgroundShimmeringIcePrismNotes": "Baila junto a los Prismas de Hielo Relucientes.",
"backgroundWinterFireworksText": "Fuegos Artificiales de Invierno",
"backgroundWinterFireworksNotes": "Lanza los Fuegos Artificiales de Invierno.",
"backgroundWinterStorefrontText": "Tienda Invernal",
"backgroundWinterStorefrontNotes": "Compra regalos en la Tienda Invernal.",
"backgrounds012017": "32.ª serie: publicada en enero de 2017",
"backgroundBlizzardText": "Tormenta de Nieve",
"backgroundBlizzardNotes": "Enfréntate a una terrible tormenta de nieve.",
"backgroundBlizzardNotes": "Enfréntate a una terrible Tormenta de Nieve.",
"backgroundSparklingSnowflakeText": "Copo de Nieve Chispeante",
"backgroundSparklingSnowflakeNotes": "Deslízate en un copo de nieve chispeante.",
"backgroundSparklingSnowflakeNotes": "Deslízate en un Copo de Nieve Chispeante.",
"backgroundStoikalmVolcanoesText": "Volcanes Stoïkalm",
"backgroundStoikalmVolcanoesNotes": "Explora los Volcanes Stoïkalm.",
"backgrounds022017": "33.ª serie: publicada en febrero de 2017",
@@ -246,10 +246,10 @@
"backgroundTreasureRoomText": "Sala del Tesoro",
"backgroundTreasureRoomNotes": "Disfruta de la riqueza de la Sala del Tesoro.",
"backgroundWeddingArchText": "Arco de Boda",
"backgroundWeddingArchNotes": "Posa bajo el arco de boda.",
"backgroundWeddingArchNotes": "Posa bajo el Arco de Boda.",
"backgrounds032017": "34.ª serie: publicada en marzo de 2017",
"backgroundMagicBeanstalkText": "Tallo de Judía Mágico",
"backgroundMagicBeanstalkNotes": "Sube por un tallo de judía mágico.",
"backgroundMagicBeanstalkNotes": "Sube por un Tallo de Judía Mágico.",
"backgroundMeanderingCaveText": "Cuerva Serpenteante",
"backgroundMeanderingCaveNotes": "Explora la Cueva Serpenteante.",
"backgroundMistiflyingCircusText": "Desconcertante Circo Volador",
@@ -260,47 +260,47 @@
"backgroundGiantBirdhouseText": "Casa de Pájaros Gigante",
"backgroundGiantBirdhouseNotes": "Posarse sobre la Casa de Pájaros Gigante.",
"backgroundMistShroudedMountainText": "Montaña Envuelta en Niebla",
"backgroundMistShroudedMountainNotes": "Escala la montaña envuelta en niebla.",
"backgroundMistShroudedMountainNotes": "Escala la Montaña Envuelta en Niebla.",
"backgrounds052017": "36.ª serie: publicada en mayo de 2017",
"backgroundGuardianStatuesText": "Estatuas Guardianes",
"backgroundGuardianStatuesNotes": "Quédate en vigilia frente a Estatuas Guardianes.",
"backgroundHabitCityStreetsText": "Calles de la Ciudad de los Hábitos",
"backgroundHabitCityStreetsNotes": "Explora las Calles de la Ciudad de los Hábitos.",
"backgroundOnATreeBranchText": "Sobre Una Rama de un Árbol",
"backgroundOnATreeBranchNotes": "Pósate sobre una rama de un árbol.",
"backgroundOnATreeBranchNotes": "Pósate sobre una Rama de un Árbol.",
"backgrounds062017": "37.ª serie: publicada en junio de 2017",
"backgroundBuriedTreasureText": "Tesoro Enterrado",
"backgroundBuriedTreasureNotes": "Desentierra el tesoro enterrado.",
"backgroundBuriedTreasureNotes": "Desentierra el Tesoro Enterrado.",
"backgroundOceanSunriseText": "Amanecer Oceánico",
"backgroundOceanSunriseNotes": "Admira el amanecer oceánico.",
"backgroundOceanSunriseNotes": "Admira el Amanecer Oceánico.",
"backgroundSandcastleText": "Castillo de Arena",
"backgroundSandcastleNotes": "Rige sobre el castillo de arena.",
"backgroundSandcastleNotes": "Rige sobre el Castillo de Arena.",
"backgrounds072017": "38.ª serie: publicada en julio de 2017",
"backgroundGiantSeashellText": "Concha Gigante",
"backgroundGiantSeashellNotes": "Reposa en la concha gigante.",
"backgroundGiantSeashellNotes": "Reposa en la Concha Gigante.",
"backgroundKelpForestText": "Bosque de Algas Marinas",
"backgroundKelpForestNotes": "Nada a través del bosque de algas marinas.",
"backgroundKelpForestNotes": "Nada a través del Bosque de Algas Marinas.",
"backgroundMidnightLakeText": "Lago Medianoche",
"backgroundMidnightLakeNotes": "Descansa junto al Lago Medianoche.",
"backgrounds082017": "39.ª serie: publicada en agosto de 2017",
"backgroundBackOfGiantBeastText": "Espalda de una Bestia Gigante",
"backgroundBackOfGiantBeastNotes": "Cabalga en la espalda de una bestia gigante.",
"backgroundBackOfGiantBeastNotes": "Cabalga en la Espalda de una Bestia Gigante.",
"backgroundDesertDunesText": "Dunas del Desierto",
"backgroundDesertDunesNotes": "Explora valientemente las dunas del desierto.",
"backgroundDesertDunesNotes": "Explora valientemente las Dunas del Desierto.",
"backgroundSummerFireworksText": "Fuegos Artificiales de Verano",
"backgroundSummerFireworksNotes": "¡Celebra el Día del Nombramiento de Habitica con Fuegos Artificiales de Verano!",
"backgrounds092017": "40.ª serie: publicada en septiembre de 2017",
"backgroundBesideWellText": "Al lado de un Pozo",
"backgroundBesideWellNotes": "Paseo junto a un pozo.",
"backgroundBesideWellNotes": "Paseo junto a un Pozo.",
"backgroundGardenShedText": "Cobertizo de Jardín",
"backgroundGardenShedNotes": "Trabaja en un cobertizo de jardín.",
"backgroundGardenShedNotes": "Trabajo en un Cobertizo de Jardín.",
"backgroundPixelistsWorkshopText": "Taller de Pixelist",
"backgroundPixelistsWorkshopNotes": "Crea obras maestras en el Taller de Pixelist.",
"backgrounds102017": "41.ª serie: publicada en octubre de 2017",
"backgroundMagicalCandlesText": "Velas Mágicas",
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de velas mágicas.",
"backgroundMagicalCandlesNotes": "Déjate acariciar por la luz de Velas Mágicas.",
"backgroundSpookyHotelText": "Hotel Escalofriante",
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un hotel escalofriante.",
"backgroundSpookyHotelNotes": "Infíltrate por el vestíbulo de un Hotel Escalofriante.",
"backgroundTarPitsText": "Pozos de Alquitrán",
"backgroundTarPitsNotes": "Pasa de puntillas por los Pozos de Alquitrán.",
"backgrounds112017": "42.ª serie: publicada en noviembre de 2017",
@@ -314,16 +314,16 @@
"backgroundCrosscountrySkiTrailText": "Ruta de Ski Todo-Terreno",
"backgroundCrosscountrySkiTrailNotes": "Planea sobre la Ruta de Ski Todo-Terreno.",
"backgroundStarryWinterNightText": "Noche de Invierno Estrellada",
"backgroundStarryWinterNightNotes": "Admira una noche de invierno estrellada.",
"backgroundStarryWinterNightNotes": "Noche de Invierno Estrellada.",
"backgroundToymakersWorkshopText": "Taller del Fabricante de Juguetes",
"backgroundToymakersWorkshopNotes": "Disfrutar las maravillas del Taller del Fabricante de Juguetes.",
"backgrounds012018": "44.ª serie: publicada en enero de 2018",
"backgroundAuroraText": "Aurora",
"backgroundAuroraNotes": "Disfruta del brillo invernal de la aurora.",
"backgroundAuroraNotes": "Disfrutar del brillo invernal de la Aurora.",
"backgroundDrivingASleighText": "Trineo",
"backgroundDrivingASleighNotes": "Conduce un trineo sobre terreno nevado.",
"backgroundDrivingASleighNotes": "Conduce un Trineo sobre terreno nevado.",
"backgroundFlyingOverIcySteppesText": "Estepas heladas",
"backgroundFlyingOverIcySteppesNotes": "Vuela sobre las estepas heladas.",
"backgroundFlyingOverIcySteppesNotes": "Volar sobre las Estepas Heladas.",
"backgrounds022018": "45.ª serie: publicada en febrero de 2018",
"backgroundChessboardLandText": "Tierra de Tablero de Ajedrez",
"backgroundChessboardLandNotes": "Juega una partida en la Tierra de Tablero de Ajedrez.",
@@ -333,16 +333,16 @@
"backgroundRoseGardenNotes": "Pasear por el Jardín de Rosas.",
"backgrounds032018": "45.ª serie: publicada en marzo de 2018",
"backgroundGorgeousGreenhouseText": "Espléndido Invernadero",
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en un espléndido invernadero.",
"backgroundGorgeousGreenhouseNotes": "Camina entre la flora que se cuida en el Espléndido Invernadero.",
"backgroundElegantBalconyText": "Elegante Balcón",
"backgroundElegantBalconyNotes": "Admira el paisaje desde un elegante balcón.",
"backgroundElegantBalconyNotes": "Admira el paisaje desde un Elegante Balcón.",
"backgroundDrivingACoachText": "Conduciendo una Carroza",
"backgroundDrivingACoachNotes": "Disfruta conducir una carroza por campos de flores.",
"backgroundDrivingACoachNotes": "Disfruta Conducir una Carroza por campos de flores.",
"backgrounds042018": "47.ª serie: publicada en abril de 2018",
"backgroundTulipGardenText": "Jardín de Tulipanes",
"backgroundTulipGardenNotes": "Pasa de puntillas por un jardín de tulipanes.",
"backgroundTulipGardenNotes": "Pasa de puntillas por un Jardín de Tulipanes.",
"backgroundFlyingOverWildflowerFieldText": "Campo de Flores silvestres",
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un campo de flores silvestres.",
"backgroundFlyingOverWildflowerFieldNotes": "Elévate por encima de un Campo de Flores silvestres.",
"backgroundFlyingOverAncientForestText": "Bosque Antiguo",
"backgroundFlyingOverAncientForestNotes": "Vuela por encima de las copas de los árboles de un bosque antiguo.",
"backgrounds052018": "48.ª serie: publicada en mayo de 2018",
@@ -355,19 +355,19 @@
"backgrounds062018": "49.ª serie: publicada en junio de 2018",
"backgroundDocksText": "Muelle",
"backgroundDocksNotes": "Pescado de lo alto del muelle.",
"backgroundRowboatText": "Bote de Remos",
"backgroundRowboatText": "Bote de remos",
"backgroundRowboatNotes": "Canta en un bote de remos.",
"backgroundPirateFlagText": "Bandera Pirata",
"backgroundPirateFlagText": "Bandera pirata",
"backgroundPirateFlagNotes": "Cuelga una temible bandera pirata.",
"backgrounds072018": "50.ª serie: publicada en julio de 2018",
"backgroundDarkDeepText": "Oscuras Profundidades",
"backgroundDarkDeepText": "Oscuras profundidades",
"backgroundDarkDeepNotes": "Nada en las oscuras profundidades entre bichos bioluminiscentes.",
"backgroundDilatoryCityText": "Ciudad de Dilatoria",
"backgroundDilatoryCityNotes": "Deambula a través de la ciudad submarina de Dilatoria.",
"backgroundTidePoolText": "Poza de Marea",
"backgroundTidePoolText": "Poza de marea",
"backgroundTidePoolNotes": "Observa la vida del océano cerca de una poza de marea.",
"backgrounds082018": "51.ª serie: publicada en agosto de 2018",
"backgroundTrainingGroundsText": "Campos de Entrenamiento",
"backgroundTrainingGroundsText": "Campos de entrenamiento",
"backgroundTrainingGroundsNotes": "Practica en los campos de entrenamiento.",
"backgroundFlyingOverRockyCanyonText": "Cañón Rocoso",
"backgroundFlyingOverRockyCanyonNotes": "Contempla un escenario que quita el aliento mientras vuelas sobre un Cañón rocoso.",
@@ -375,9 +375,9 @@
"backgroundBridgeNotes": "Cruza un Puente encantador.",
"backgrounds092018": "52.ª serie: publicada en septiembre de 2018",
"backgroundApplePickingText": "Colecta de Manzanas",
"backgroundApplePickingNotes": "Ve a recolectar manzanas y trae a casa unas cuantas.",
"backgroundApplePickingNotes": "Ve a Recolectar Manzanas y trae a casa unas cuantas.",
"backgroundGiantBookText": "Libro Gigante",
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un libro gigante.",
"backgroundGiantBookNotes": "Lee mientras paseas por las páginas de un Libro Gigante.",
"backgroundCozyBarnText": "Granero Confortable",
"backgroundCozyBarnNotes": "Relájate con tus mascotas y monturas en su Confortable Granero.",
"backgrounds102018": "53.ª serie: publicada en octubre de 2018",
@@ -930,16 +930,5 @@
"backgroundWinterDesertWithSaguarosNotes": "Exalta tus sentidos entre los Cactus en Desierto Invernal.",
"backgrounds022026": "CONJUNTO 141: Publicado en Febrero 2026",
"backgroundElegantPalaceText": "Palacio Elegante",
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante.",
"backgrounds032026": "142.ª serie: publicada en marzo de 2026",
"backgroundWaterfallWithRainbowText": "Cascada y Arcoíris",
"backgroundWaterfallWithRainbowNotes": "Admira la belleza cautivadora de una cascada con un arcoíris.",
"backgrounds042026": "143.ª serie: publicada en abril de 2026",
"backgroundRidingACometText": "Montando un Cometa",
"backgroundRidingACometNotes": "¡Viaja a través del espacio mientras montas un cometa!",
"backgrounds052026": "144.ª serie: publicada en mayo de 2026",
"backgroundElvenCitadelText": "Ciudadela Élfica",
"backgroundElvenCitadelNotes": "Toma un pintoresto recorrido en una ciudadela élfica.",
"backgroundOnAStrangePlanetText": "En un Extraño Planeta",
"backgroundOnAStrangePlanetNotes": "Aventúrate allá donde ningun Habiticano ha viajado antes: hacia un extraño planeta."
"backgroundElegantPalaceNotes": "Quédate obnubilado por las coloridas salas del Palacio Elegante."
}
+15 -69
View File
@@ -3,14 +3,14 @@
"equipmentType": "Tipo",
"klass": "Clase",
"groupBy": "Agrupar por <%= type %>",
"classBonus": "(Este equipamiento es de tu clase por lo que gana un multiplicador adicional de 1.5 a sus atributos)",
"classArmor": "Armadura de Clase",
"featuredset": "Conjunto Destacado: <%= name %>",
"mysterySets": "Conjuntos Misteriosos",
"classBonus": "(Este equipamiento es de tu clase por lo que gana un multiplicador de 1,5 a sus atributos)",
"classArmor": "Armadura de clase",
"featuredset": "Conjunto destacado: <%= name %>",
"mysterySets": "Conjuntos misteriosos",
"gearNotOwned": "No tienes este objeto.",
"noGearItemsOfType": "No tienes ninguno de estos.",
"classLockedItem": "Este equipamiento solo está disponible para una clase específica. ¡A partir del nivel 10, puedes cambiar tu clase pulsando: Icono de Usuario > Ajustes > Creación de personaje!",
"tierLockedItem": "Este objeto solo estará disponible una vez hayas comprado los anteriores en orden. ¡Sigue trabajando duro para llegar hasta él!",
"classLockedItem": "Este equipamiento solo está disponible para una clase específica. ¡A partir del nivel 10, cambia tu clase pulsando: Icono de Usuario > Ajustes > Creación de personaje!",
"tierLockedItem": "Este objeto solo estará disponible una vez hayas comprado los anteriores de la secuencia. ¡Sigue trabajando duro para llegar hasta él!",
"sortByType": "Tipo",
"sortByPrice": "Precio",
"sortByCon": "CON",
@@ -18,16 +18,16 @@
"sortByStr": "FUE",
"sortByInt": "INT",
"weapon": "arma",
"weaponCapitalized": "Objeto Mano Dominante",
"weaponBase0Text": "Sin Arma",
"weaponBase0Notes": "Sin Arma.",
"weaponWarrior0Text": "Espada de Entrenamiento",
"weaponCapitalized": "Objeto de la mano dominante",
"weaponBase0Text": "Sin arma",
"weaponBase0Notes": "Sin arma.",
"weaponWarrior0Text": "Espada de entrenamiento",
"weaponWarrior0Notes": "Arma de práctica. No otorga ningún beneficio.",
"weaponWarrior1Text": "Espada",
"weaponWarrior1Notes": "Espada común de un soldado. Aumenta la Fuerza en <%= str %>.",
"weaponWarrior2Text": "Hacha",
"weaponWarrior2Notes": "Arma cortante de doble filo. Aumenta la fuerza en <%= str %>.",
"weaponWarrior3Text": "Lucero del Alba",
"weaponWarrior3Text": "Lucero del alba",
"weaponWarrior3Notes": "Maza pesada con brutales espinas. Aumenta la Fuerza en <%= str %>.",
"weaponWarrior4Text": "Espada de Zafiro",
"weaponWarrior4Notes": "Espada cuyos filos cortan como el viento del norte. Aumenta la Fuerza en <%= str %>.",
@@ -3108,9 +3108,9 @@
"headSpecialSummer2024MageNotes": "Este sombrero se mece suavemente en las corrientes del océano a la vez que te asiste para canalizar tu gran sabiduría. Aumenta la percepcion en <%= per %>. Equipamiento de edición limitada Verano 2024.",
"headArmoireCorsairsBandanaNotes": "Este pañuelo badana es esencial ya sea que desees mantener tu cabeza protegida de alguna gaviota que volando sobre ella suelte la carga o por que no quieras que la tripulación vea que estas sudando nerviosamente y decida amotinarse. Eso si, añade una cuenta decorativa cada vez que completes una de tus aventuras. Aumenta la Inteligencia en <%= int %>. Armario Encantado: Conjunto de Corsario (Artículo 2 de 3)",
"shieldSpecialSummer2024WarriorNotes": "Jajaja, para todos aquellos que dicen que no eres capaz de alcanzar tus objetivos, mirad: ¡decídselo a mi mano, quiero decir, jeje, aleta! Aumenta la Constitución en <%= con %>. Equipamiento de edición limitada Verano 2024.",
"gearItemsCompleted": "¡Ya tienes todo el equipamiento de <%= klass %>! Nuevo equipamiento se publica durante las Galas estaciónales.",
"gearItemsCompleted": "¡Ya tienes todo el equipamiento de <%= klass %>! Se publicará nuevo equipamiento en las Galas estaciónales.",
"moreArmoireGearAvailable": "¡Hasta entonces, hay aún <%= armoireCount %> piezas de equipamiento que puedes encontrar en el Armario Encantado!",
"moreArmoireGearComing": El Armario Encantado también obtiene objetos nuevos cada mes!",
"moreArmoireGearComing": También en el Armario Encantado tienes nuevos objetos cada mes!",
"weaponSpecialSummer2024RogueText": "Tridente de Nudibranquio",
"weaponSpecialSummer2024MageText": "Varita de Anémona de Mar",
"weaponSpecialSummer2024MageNotes": "Estos tentáculos terroríficos poseen la habilidad de disipar, desviar y dirigir la magia indistintamente. Aumenta la inteligencia en <%= int %> y la percepción en <%= per %>. Equipamiento de edición limitada Verano 2024.",
@@ -3484,7 +3484,7 @@
"headMystery202512Text": "Yelmo de Galleta Ganadora",
"headMystery202512Notes": "Pan de jengibre forjado con números hechizos arcanos que acabaron con la cordura de sus conjuradores, protección mística ¡siempre y cuando no te lo comas! No otorga ningún beneficio. Artículo de Suscriptor Diciembre 2025.",
"headMystery202602Text": "Orejas de Zorro del Sakura",
"headMystery202602Notes": "Con estas orejas tu oído se afilará tanto que podrás escuchar el brote de las flores en las ramas de los árboles al acercarse la primavera. No otorga ningún beneficio. Artículo de Suscriptor de febrero 2026.",
"headMystery202602Notes": " Tu percepción de los sonidos será tan aguzada debido a estas orejas, que podrás escuchar no solo una mano aplaudiendo sola, si no también el florecimiento de las hermosas florecillas cuando la primavera se vaya acercando, florecilla. No otorga ningún beneficio. Artículo de Suscriptor Febrero 2026.",
"headArmoireLoneCowpokeHatText": "Sombrero de Vaquero Solitario",
"shieldSpecialWinter2026WarriorText": "Escudo Escarcha",
"shieldSpecialWinter2026HealerText": "Explosión Estelar",
@@ -3495,59 +3495,5 @@
"backMystery202601Text": "Símbolo Invernal",
"backMystery202602Text": "Las Cinco Colas de Sakura",
"backArmoireHarpsichordText": "Clavicémbalo",
"backArmoireHarpsichordNotes": "¡Duing! ¡Ding! Reúne a tu equipo ya sea para un almuerzo campestre o un picnic, algo totalmente diferente a lo anterior, y una vez allí deleita sus pabellones auditivos mientras interpretas Highway to Hell en tu clavicémbalo. Aumenta la Percepción y la Inteligencia en <%= attrs %> cada uno.Armario Encantado: Conjunto Instrumentos Musicales 2 (Artículo 1 de 3)",
"weaponSpecialSpring2026WarriorText": "Florete de la Poderosa Rana",
"weaponSpecialSpring2026RogueText": "Rama Primaveral",
"weaponSpecialSpring2026HealerText": "Bastón de Campanilla de Invierno",
"weaponSpecialSpring2026MageText": "Parasol Palo de Mayo",
"weaponSpecialSpring2026MageNotes": "Se acerca una ocasión para celebrar, ¡y con este bonito palo-parasol estarás preparado! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026WarriorText": "Armadura Rana",
"armorSpecialSpring2026WarriorNotes": "Salta a la acción tan pronto como la nieve empieza a derretirse. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026RogueText": "Armadura de Corteza de Abedul",
"armorSpecialSpring2026RogueNotes": "Resiste las inevitables lluvias primaverales y las brisas ligeras. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorSpecialSpring2026HealerText": "Vestido de Campanilla de Invierno",
"armorSpecialSpring2026MageText": "Traje de Bailarín del Palo de Mayo",
"armorSpecialSpring2026MageNotes": "Llega preparado para bailar, hacer un picnic y disfrutar del clima cálido que trae la primavera. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorMystery202604Text": "Traje Espacial del Audaz Astronauta",
"armorMystery202604Notes": "Un pequeño paso para tus Tareas Pendientes, ¡un gran salto para tu satisfacción personal! No otorga ningún beneficio. Artículo de Suscriptor de abril de 2026.",
"armorArmoireHandstandOutfitText": "Parado de manos",
"armorArmoireHandstandOutfitNotes": "Las cosas definitvamente se ven de otra manera cuando estás boca abajo, no es así? Si te sientes atrapado, ¡es tiempo de una nueva perspectiva! Aumenta la Percepción en <%= per %>. Armario Encantado: Conjunto Parado de Manos (Artículo 1 de 1).",
"headSpecialSpring2026RogueText": "Casco Rama Primaveral",
"headSpecialSpring2026HealerText": "Casco de Campanilla de Invierno",
"headSpecialSpring2026HealerNotes": "Haz una esperanzadora declaración con estos hermosos, resistentes pétalos. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026MageText": "Corona de Flor de Mayo",
"headMystery202603Text": "Sombrero de Mago de las Glicinias",
"headMystery202604Text": "Casco del Audaz Astronauta",
"headArmoireFloppyYellowHatText": "Sombrero Flexible Amarillo",
"headArmoireVerdantArmingCapText": "Cofia Acolchada del Paje Verdoso",
"shieldSpecialSpring2026RogueText": "Rama Primaveral",
"shieldSpecialSpring2026RogueNotes": "Alcanza tus límites con estas ramas. También sirven como rascador de espalda en caso de necesitarlo. Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldSpecialSpring2026HealerText": "Hoja de Campanilla de Invierno",
"shieldMystery202605Text": "Escudo del Anochecer",
"shieldArmoireSoftYellowPillowText": "Almohada Amarillo Suave",
"shieldArmoireVerdantBannerText": "Estandarte del Paje Verde",
"shieldArmoireVerdantBannerNotes": "¡Ondea alto tu estandarte para indicar a tus amigos que es hora de reunirse! Aumenta la Inteligencia en <%= int %>. Armario Encantado: Conjunto del Paje Verdoso (Artículo 2 de 2).",
"backMystery202605Text": "Nimbus del Anochecer",
"weaponSpecialSpring2026RogueNotes": "Se acerca una gran oportunidad para crecer, ¡y con estas ramas en ciernes estarás preparado! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026WarriorNotes": "Puede que se presente una oportunidad para batirse en duelo en cualquier momento, ¡y con este formidable florete estarás preparado! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026HealerNotes": "Se presenta una oportunidad para empezar de nuevo con un fresco comienzo, ¡y con este espléndido bastón estarás preparado! Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponMystery202603Text": "Bastón de Mago de las Glicinias",
"weaponMystery202603Notes": "¡Lanza hechizos para calentar el aire primaveral y favorecer la floración! No otorga ningún beneficio. Artículo de Suscriptor de marzo de 2026.",
"armorSpecialSpring2026HealerNotes": "Deslízate con gracia desde un invierno frío y oscuro hasta una primavera gloriosa. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"armorArmoireSoftYellowSuitText": "Traje Amarillo Suave",
"armorArmoireSoftYellowSuitNotes": "El amarillo es un color energético. Ponte esto a la hora de dormir, y te despertarás con el Sol, listo para enfrentarte a un día atareado. Aumenta la Constitución y la Fuerza en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 2 de 3).",
"headSpecialSpring2026WarriorText": "Casco del Guerrero Rana",
"headSpecialSpring2026WarriorNotes": "Las ranas son famosas por su resistencia a la corrupción. ¡Este casco te otorgará sus nobles cualidades! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026RogueNotes": "Haz una notable declaración con ramitas y brotes creciendo de forma salvaje en todas direcciones. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headSpecialSpring2026MageNotes": "Haz una alegre declaración con brillantes flores rodeando tu cabeza. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada de Primavera 2026.",
"headMystery202603Notes": "Este coqueto sombrero no solo potencia tus habilidades mágicas, ¡sino que además tiene un adorable aroma primaveral! No otorga ningún beneficio. Artículo de Suscriptor de marzo 2026.",
"headMystery202604Notes": "En el espacio, nadie puede oirte completando tus Tareas Pendientes. ¡Pero la verdadera recompensa es la satisfacción personal! No otorga ningún beneficio. Artículo de Suscriptor de abril 2026.",
"backMystery202605Notes": "Un halo resplandeciente de luz de luna y estrellas para iluminar la noche más oscura. No confiere ningún beneficio. Artículo de Suscriptor de mayo 2026.",
"headArmoireFloppyYellowHatNotes": "Muchos encantamientos han sido cosidos a este simple sombrero, confiriéndole un juvenil color amarillo. Aumenta todas las estadísticas en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 1 de 3).",
"headArmoireVerdantArmingCapNotes": "Esta cómoda, acolchada cofia te prepara para la batalla y te ayuda a resistir cualquier cosa pesada que se te presente. Aumenta la Percepción y la Constitución en <%= attrs %>. Armario Encantado: Conjunto del Paje Verdoso (Artículo 1 de 2).",
"shieldSpecialSpring2026WarriorText": "Candelabro del Guerrero Rana",
"shieldSpecialSpring2026WarriorNotes": "Este candelabro no sólo puede iluminar tu camino; también puedes usarlo para derretir cualquier resto de nieve y hielo. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldSpecialSpring2026HealerNotes": "Crea una brisa suave con este ventilador a medida que suben las temperaturas. También sirve como bolígrafo en caso de necesitarlo. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Primavera 2026.",
"shieldMystery202605Notes": "Deja que la brillante luz de luna te proteja de los peligros en la oscuridad. No otorga ningún beneficio. Artículo de Suscriptor de mayo 2026.",
"shieldArmoireSoftYellowPillowNotes": "El guerrero experimentado lleva una almohada en cada expedición. Crece y brilla mientras consolidas todo lo aprendido en aventuras pasadas… incluso mientras duermes la siesta. Aumenta la Inteligencia y la Percepción en <%= attrs %>. Armario Encantado: Conjunto Ropa de Casa Amarilla (Artículo 3 de 3)."
"backArmoireHarpsichordNotes": "¡Duing! ¡Ding! Reúne a tu equipo ya sea para un almuerzo campestre o un picnic, algo totalmente diferente a lo anterior, y una vez allí deleita sus pabellones auditivos mientras interpretas Highway to Hell en tu clavicémbalo. Aumenta la Percepción y la Inteligencia en <%= attrs %> cada uno.Armario Encantado: Conjunto Instrumentos Musicales 2 (Artículo 1 de 3)"
}
+1 -13
View File
@@ -428,17 +428,5 @@
"groupTeacher": "Usándolo para dar clase",
"groupParentChildren": "Usándolo con mi familia",
"groupPlanBillingFYI": "Las suscripciones de Planes de Grupo se renuevan automáticamente a no ser que la canceles al menos 24 horas antes del fin del periodo en curso. Puedes cancelarla desde la pestaña Facturación de Grupo en tu Plan de Grupo. Se te cargará dentro de las 24 horas en las que renueva la suscripción y basado en el número de miembros de tu grupo en el Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, habrá un cargo adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación.",
"groupPlanBillingFYIShort": "Las suscripciones de Plan de Grupo se renuevan automáticamente a no ser que se cancelen 24 horas antes del fin del periodo actual. Se te cargará dentro de las 24 horas antes de la renovación de la suscripción, basado en el número de miembros de tu Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, se te aplicará un pago adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación.",
"createNewGroup": "Crear un Nuevo Grupo",
"upgradeExistingGroup": "Mejorar un Grupo Existente",
"chooseAnOption": "Elegir una Opción",
"yourParty": "Tu Equipo",
"previouslyUpgradedGroup": "Grupo previamente mejorado",
"inviteOthersForAdditional": "Invita a otros a tu Grupo por un adicional de",
"perMember": "por miembro",
"oneMember": "1 miembro",
"membersCount": "<%= count %> miembros",
"pendingCount": "(<%= count %> pendiente)",
"upgradeCancelsPendingInvites": "Mejorar tu Equipo cancelará todas las invitaciones pendientes",
"additionalMembersProrated": "Miembros adicionales invitados durante el mes se añadirán al total del siguiente ciclo de facturación como cargo prorrateado."
"groupPlanBillingFYIShort": "Las suscripciones de Plan de Grupo se renuevan automáticamente a no ser que se cancelen 24 horas antes del fin del periodo actual. Se te cargará dentro de las 24 horas antes de la renovación de la suscripción, basado en el número de miembros de tu Plan de Grupo en ese mismo momento. Si añades miembros entre períodos de pago, se te aplicará un pago adicional prorrateado por los beneficios obtenidos en tu siguiente ciclo de facturación."
}
+20 -24
View File
@@ -267,28 +267,24 @@
"fall2024SpaceInvaderHealerSet": "Conjunto de Invasor del Espacio (Sanador)",
"fall2024BlackCatRogueSet": "Conjunto Gato Negro (Pícaro)",
"fall2024FieryImpWarriorSet": "Conjunto de Balrog Menor (Guerrero)",
"winter2025StringLightsHealerSet": "Conjunto Tira de Luces (Sanador)",
"winter2025SnowRogueSet": "Conjunto Muñeco de Nieve (Pícaro)",
"winter2025MooseWarriorSet": "Conjunto Alce (Guerrero)",
"winter2025AuroraMageSet": "Conjunto Aurora (Mago)",
"spring2025CrystalPointRogueSet": "Conjunto Punta de Cristal (Pícaro)",
"spring2025PlumeriaHealerSet": "Conjunto Plumeria (Sanador)",
"spring2025MantisMageSet": "Conjunto Mantis (Mago)",
"spring2025SunshineWarriorSet": "Conjunto Rayo de Sol (Guerrero)",
"summer2025ScallopWarriorSet": "Conjunto Vieira (Guerrero)",
"summer2025SquidRogueSet": "Conjunto Calamar (Pícaro)",
"summer2025SeaAngelHealerSet": "Conjunto Ángel de Mar (Sanador)",
"summer2025FairyWrasseMageSet": "Conjunto Pez Lábrido Hada (Mago)",
"fall2025SasquatchWarriorSet": "Conjunto Pie Grande (Guerrero)",
"fall2025SkeletonRogueSet": "Conjunto Esqueleto (Pícaro)",
"fall2025KoboldHealerSet": "Conjunto Kobold (Sanador)",
"fall2025MaskedGhostMageSet": "Conjunto Fantasma Enmascarado (Mago)",
"winter2026RimeReaperWarriorSet": "Conjunto Destripador Escarcha (Guerrero)",
"winter2026SkiRogueSet": "Conjunto Esquí (Pícaro)",
"winter2026PolarBearHealerSet": "Conjunto Oso Polar (Sanador)",
"winter2026MidwinterCandleMageSet": "Conjunto Vela Invernal (Mago)",
"spring2026FrogWarriorSet": "Conjunto Rana (Guerrero)",
"spring2026BranchRogueSet": "Conjunto Rama Primaveral (Pícaro)",
"spring2026SnowdropHealerSet": "Conjunto Campanilla de Invierno (Sanador)",
"spring2026MaypoleMageSet": "Conjunto Palo de Mayo (Mago)"
"winter2025StringLightsHealerSet": "Conjunto de Sanador Tira de Luces",
"winter2025SnowRogueSet": "Conjunto Pícaro Muñeco de Nieve",
"winter2025MooseWarriorSet": "Conjunto de Guerrero Alce",
"winter2025AuroraMageSet": "Conjunto de Mago de la Aurora",
"spring2025CrystalPointRogueSet": "Conjunto Pícaro Puntas de Cristal",
"spring2025PlumeriaHealerSet": "Conjunto Sanador Flor Plumaria",
"spring2025MantisMageSet": "Conjunto Mago Mantis",
"spring2025SunshineWarriorSet": "Conjunto Guerrero Brillo Solar",
"summer2025ScallopWarriorSet": "Conjunto Guerrero Vieira",
"summer2025SquidRogueSet": "Conjunto Pícaro Calamar",
"summer2025SeaAngelHealerSet": "Conjunto Sanador Ángel de Mar",
"summer2025FairyWrasseMageSet": "Conjunto Mago Pez Lábrido Hada",
"fall2025SasquatchWarriorSet": "Conjunto de Guerrero Bigfoot",
"fall2025SkeletonRogueSet": "Conjunto de Esqueleto Pícaro",
"fall2025KoboldHealerSet": "Conjunto de Sanador Kobold",
"fall2025MaskedGhostMageSet": "Conjunto de Mago Fantasma Enmascarado",
"winter2026RimeReaperWarriorSet": "Conjunto de Guerrero Destripador Escarcha",
"winter2026SkiRogueSet": "Conjunto de Esquiador Pícaro",
"winter2026PolarBearHealerSet": "Conjunto de Sanador Oso Polar",
"winter2026MidwinterCandleMageSet": "Conjunto de Mago Vela Invernal"
}
+9 -18
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "Tiranosaurio Esqueleto",
"questTRexUndeadRageTitle": "Sanación de Esqueleto",
"questTRexUndeadRageDescription": "Esta barra se llena cuando no completas tus tareas Diarias. Cuando esté completa, ¡el Tiranosaurio Esqueleto va a sanar un 30 por ciento de su salud restante!",
"questTRexUndeadRageEffect": "¡Tiranosaurio Esqueleto usa SANACIÓN DE ESQUELETO!\n\n¡El monstruo deja salir un rugido sobrenatural, y algunos de sus huesos dañados vuelven a unirse!",
"questTRexUndeadRageEffect": "`¡Tiranosaurio Esqueleto usa SANACIÓN DE ESQUELETO!`\n\n¡El monstruo deja salir un rugido sobrenatural, y algunos de sus huesos dañados vuelven a unirse!",
"questTRexDropTRexEgg": "Tiranosaurio (Huevo)",
"questTRexUnlockText": "Desbloquea la compra de huevos de tiranosaurio en el Mercado",
"questRockText": "Escapa de la Cueva Viviente",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "Enjambre de Carabelas Aquaticas",
"questDilatoryDistress2RageTitle": "Regeneración de Enjambre",
"questDilatoryDistress2RageDescription": "Regeneración de Enjambre: Esta barra se va llenando cuando no completas tus Diarias. Cuando se llene completamente, ¡el Enjambre de Calaveras Acuáticas sanará el 30% de su salud restante!",
"questDilatoryDistress2RageEffect": "¡El Enjambre de Calaveras Acuáticas usa REGENERACIÓN DE ENJAMBRE!\n\n¡Incentivadas por sus victorias, más calaveras salen de la grieta, fortaleciendo al enjambre!",
"questDilatoryDistress2RageEffect": "`¡El Enjambre de Calaveras Acuáticas usa REGENERACIÓN DE ENJAMBRE!`\n\n¡Incentivadas por sus victorias, más calaveras salen de la grieta, fortaleciendo al enjambre!",
"questDilatoryDistress2DropSkeletonPotion": "Poción de Eclosión Esquelética",
"questDilatoryDistress2DropCottonCandyBluePotion": "Poción de Eclosión Algodón de Azúcar Azul",
"questDilatoryDistress2DropHeadgear": "Diadema de coral de fuego (equipo de cabeza)",
@@ -437,7 +437,7 @@
"questStoikalmCalamity1Boss": "Enjambre de Calaveras Terrestres",
"questStoikalmCalamity1RageTitle": "Reaparición del Enjambre",
"questStoikalmCalamity1RageDescription": "Regeneración de Enjambre: Esta barra se va llenando cuando no completas tus Diarias. Cuando se llene completamente, ¡el Enjambre de Calaveras Terrestres sanará el 30% o de su salud restante!",
"questStoikalmCalamity1RageEffect": "¡El Enjambre de Calaveras de Tierra usa REGENERACIÓN DE ENJAMBRE!\n\n¡Más calaveras salen de la tierra, chasqueando sus dientes en el frío!",
"questStoikalmCalamity1RageEffect": "`¡El Enjambre de Calaveras de Tierra usa REGENERACIÓN DE ENJAMBRE!`\n\n¡Más calaveras salen de la tierra, chasqueando sus dientes en el frío!",
"questStoikalmCalamity1DropSkeletonPotion": "Poción de eclosión de esqueleto",
"questStoikalmCalamity1DropDesertPotion": "Poción de eclosión del desierto",
"questStoikalmCalamity1DropArmor": "Armadura de jinete de mamut",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "Enjambre de Calaveras Aéreas",
"questMayhemMistiflying1RageTitle": "Reaparición del Enjambre",
"questMayhemMistiflying1RageDescription": "Reaparición del Enjambre: Esta barra se llena cuando no completas tus Tareas Diarias. Cuando está llena, ¡el Enjambre de Calaveras Aéreas recuperará el 30% de su salud restante!",
"questMayhemMistiflying1RageEffect": "¡El Enjambre de Calaveras Aéreas utiliza REAPARICIÓN DEL ENJAMBRE!\n\n¡Envalentonadas por sus victorias, más calaveras aparecen girando de entre las nubes!",
"questMayhemMistiflying1RageEffect": "`El Enjambre de Calaveras Aéreas utiliza REAPARICIÓN DEL ENJAMBRE`\n\n¡Envalentonadas por sus victorias, más calaveras aparecen girando de entre las nubes!",
"questMayhemMistiflying1DropSkeletonPotion": "Poción de Eclosión Esquelética",
"questMayhemMistiflying1DropWhitePotion": "Poción de Eclosión Blanca",
"questMayhemMistiflying1DropArmor": "Túnicas del Mensajero Picaresco Arcoiris (Armadura)",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "Anti'zinnya",
"questLostMasterclasser4RageTitle": "Vacío Sifónico",
"questLostMasterclasser4RageDescription": "Vacío Sifónico: Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, Anti'zinnya eliminará el Maná del Equipo!",
"questLostMasterclasser4RageEffect": "¡'Anti'zinnya usa VACÍO SIFÓNICO! ¡En una inversión del hechizo Oleada Etérea, sientes que tu magia se escurre hacia la oscuridad!",
"questLostMasterclasser4RageEffect": "'Anti'zinnya usa VACÍO SIFÓNICO! ¡En una inversión del hechizo Oleada Etérea, sientes que tu magia se escurre entre la oscuridad!",
"questLostMasterclasser4DropBackAccessory": "Capa de Éter (Accesorio en la Espalda)",
"questLostMasterclasser4DropWeapon": "Cristales de Éter (Arma de dos manos)",
"questLostMasterclasser4DropMount": "Montura Invisible de Éter",
@@ -693,7 +693,7 @@
"jungleBuddiesText": "Paquete de misiones Colegas de la Jungla",
"questWaffleUnlockText": "Desbloquea la compra de la poción de eclosión de confitería en el Mercado",
"questWaffleDropDessertPotion": "Poción de eclosión de confitería",
"questWaffleRageEffect": "¡Tortita Terrible usa CIENAGA DE ARCE! ¡Un sirope pegajoso de savia ralentiza tus golpes y hechizos! Daño pendiente reducido.",
"questWaffleRageEffect": "`¡Tortita Terrible usa CIENAGA DE ARCE!` ¡Un sirope pegajoso de savia ralentiza tus golpes y hechizos! Daño pendiente reducido.",
"questWaffleRageDescription": "Cienaga de Arce: Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, la Tortita Terrible restará del daño pendiente que los miembros del Equipo hayan acumulado!",
"questBlackPearlUnlockText": "Desbloquea Pociones de Eclosión de Perla Negra para comprar en el Mercado",
"questBlackPearlDropBlackPearlPotion": "Poción de Eclosión de Perla Negra",
@@ -749,7 +749,7 @@
"questVirtualPetNotes": "Es una tranquila y agradable mañana de primavera en Habitica, una semana después de un memorable Día de los Inocentes. Tú y @Beffymaroo estáis en los establos atendiendo a vuestras mascotas (¡quienes todavía están un poco confundidas por el tiempo que pasaron virtualmente!).<br><br>A lo lejos escuchas un estruendo y un pitido, suave al principio pero aumentando en volumen como si estuviera cada vez más cerca. Aparece una forma de huevo en el horizonte y, a medida que se acerca, con un pitido cada vez más fuerte, ¡ves que es una mascota virtual gigantesca!<br><br>“Oh, no”, exclama @Beffymaroo, “Creo que Santo Inocente dejó asuntos pendientes con este tipo grande aquí, ¡parece querer atención!”<br><br>La mascota virtual emite un pitido enfadado, lanzando una rabieta virtual y gritando cada vez más cerca.",
"questVirtualPetBoss": "Tamagotchi",
"questVirtualPetRageTitle": "El pitido",
"questVirtualPetRageEffect": "¡Wotchimon usa Pitido Molesto!\" ¡Wotchimon emite un pitido molesto y su barra de felicidad desaparece repentinamente! Daño pendiente reducido.",
"questVirtualPetRageEffect": "\"¡Wotchimon usa un pitido molesto!\" ¡Wotchimon emite un pitido molesto y su barra de felicidad desaparece repentinamente! Daño pendiente reducido.",
"questVirtualPetRageDescription": "Esta barra se llena cuando no completas tus Diarios. ¡Cuando esté lleno, Wotchimon eliminará algunos de los daños causados de tu grupo!",
"questVirtualPetDropVirtualPetPotion": "Poción virtual para incubar mascotas",
"questVirtualPetText": "El Caos Virtual con Santo Inocente: El Pitido",
@@ -760,7 +760,7 @@
"questPinkMarbleBoss": "Cupido",
"questPinkMarbleRageTitle": "Ponche Rosa",
"questPinkMarbleRageDescription": "Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando esté llena, Cupido eliminará algunos daños causados por tu grupo!",
"questPinkMarbleRageEffect": "¡Cupido usa Ponche Rosa! ¡No ha sido nada afectuoso! Tus compañeros de equipo están desconcertados. El daño pendiente se ha reducido.",
"questPinkMarbleRageEffect": "\"¡Cupido usa Ponche Rosa!\" ¡No ha sido nada afectuoso! Tus compañeros de equipo están desconcertados. El daño pendiente se ha reducido.",
"questPinkMarbleDropPinkMarblePotion": "Poción de eclosión de mármol rosa",
"questPinkMarbleUnlockText": "Desbloquea Poción de eclosión de mármol rosa para comprarla en el Mercado.",
"questFungiNotes": "Ha sido una primavera lluviosa y la tierra alrededor de los establos está esponjosa y húmeda. Te das cuenta que bastantes setas han crecido cerca de las paredes de madera y en las vallas. Hay una niebla que impide que el sol brille con fuerza y esto crea un paisaje muy desalentador. <br><br>Entre la espesura de la bruma ves la silueta del Bromista de Abril, pero no parece que esté en su habitual estado divertido y saltarín.<br><br>”Esperaba traeros algunas divertidas pociones de eclosión de Setas Mágicas para que pudierais disfrutar de vuestras amigas setas desde mi día especial y para siempre” dice, con una expresión alarmantemente seria. “Pero esta fría niebla me está afectando, me hace sentirme demasiado cansado y triste y hace que mi magia no funcione.”<br><br>”Oh no, siento escuchar eso,” le dices, notando tu también tu propio estado de ánimo muy afectado y apagado. “Esta extraña niebla esta haciendo que el día se vuelva sombrío. Me pregunto cuál será su procedencia...”<br><br>Un estruendo apenas audible resuena por entre los campos y puedes ver una silueta emergiendo de la bruma. La sensación de peligro que notas se hace más intensa al ver una gigantesca criatura parecida a una seta con cara de pocos amigos, y la bruma parece que procede de ella.<br><br>”Vale,” dice el Bromista, “Creo que este colega micológico puede ser el origen de nuestra súbita tristeza. Veamos si una buena trifulca nos ameniza el día mientras le abrimos otra sonrisa al bicho.”",
@@ -860,14 +860,5 @@
"questOpalCollectMercuryRunes": "Runa de Mercurio",
"questOpalCollectOpalGems": "Gema de Ópalo",
"questOpalDropOpalPotion": "Poción de eclosión Ópalo",
"questOpalUnlockText": "Desbloquea pociones de eclosión Ópalo para comprarlas en la Tienda",
"questAlienText": "Invasión de los Ladrones de Motivación",
"questAlienNotes": "Han sido días extraños en Habitica. El gran platillo volador aún flota cerca de los Campos Florecientes. Zumba extrañamente. ¿Por qué sigue ahí? El Día de los Santos Inocentes ha pasado, y el tiempo de protagonismo del Maestro de los Pícaros ha terminado.<br><br>Deambulas hacia la luz de la nave espacial. Podrías echarle un vistazo y adentrarte un poco, de paso.<br><br>Mientras te acercas, ves al Bromista de Abril, con aspecto algo sombrío. Su rostro luce verdoso a la luz del rayo de la nave.<br><br>\"'¡Era mi plan conseguir algunas pociones para todos, un pequeño regalo para que todos puedan disfrutar de sus pequeños amigos extraterrestres de nuevo! Pero simplemente no puedo reunir el valor... Creo que sé por qué\", dice el Bromista, asintiendo hacia el rayo.<br><br>Pequeños símbolos están siendo absorbidos por la nave. ¡Son todas tus tareas marcadas! No me extraña que tu motivación haya estado tan baja.<br><br>”¡Nuestra motivación está siendo abducida!” exclamas. “¡Tenemos que rescatarla antes de que acabe perdida en el espacio profundo!”<br><br>El Bromista sonríe. “¡Concentra tus pensamientos en las tareas que sabes que debes terminar! Yo haré el resto con un poco de magia.”",
"questAlienBoss": "Ladrón de Ánimo, el Extraterrestre",
"questAlienRageTitle": "Impedimento Intergaláctico",
"questAlienRageEffect": "¡Ladrón de Ánimo usa Impedimento Intergaláctico! Has retrocedido a través del hiperespacio. ¡Tu oponente recupera PV!",
"questAlienDropAlienPotion": "Poción de eclosión alien",
"questAlienUnlockText": "Desbloque la Poción de Eclosión Alien para comprar en el Mercado",
"questAlienRageDescription": "Esta barra se llena cuando no completas tus Tareas Diarias. ¡Cuando está llena, el Extraterrestre te desanimará recuperando parte de su Salud!",
"questAlienCompletion": "Has logrado recuperar la motivación robada con tu determinación y el poder mágico del Bromista. Mientras sientes regresar tus fuerzas, el OVNI desciende y una rampa emerge lentamente junto con una criatura grande, verde y de un solo ojo. Aunque de aspecto extraño, no parece amenazante.<br><br>“Parece que fuimos un poco demasiado lejos al intentar obtener un poco de ánimo extra de tu hermosa ciudad”, dice. “Disculpas por eso, y fantástico trabajo al recuperarlo. ¡El aura extra de tus esfuerzos, de hecho, cargó el motor de la nave lo suficiente como para llevarnos a casa! Por favor, toma esto en señal de agradecimiento.”<br><br>“¡Oh, pociones!”, dice el Bromista, “¡qué encantador, y qué conveniente para mí que las tengas todas listas!”"
"questOpalUnlockText": "Desbloquea pociones de eclosión Ópalo para comprarlas en la Tienda"
}
+3 -10
View File
@@ -1,8 +1,8 @@
{
"rebirthNew": "Renacimiento: ¡Nueva aventura disponible!",
"rebirthUnlock": "¡Has desbloqueado el Renacimiento! Este objeto especial del Mercado te permite empezar un juego nuevo en el nivel 1, manteniendo tus tareas, logros, mascotas, y más. Úsalo para comenzar una nueva vida en Habitica por si sientes que ya lo lograste todo, o para experimentar nuevas funciones con la perspectiva de un personaje nuevo.",
"rebirthAchievement": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> vez, y el nivel más alto que has conseguido <strong><%= level %></strong>.",
"rebirthAchievement100": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> veces, y el nivel más alto que has conseguido es <strong>100</strong> o superior.",
"rebirthAchievement": "¡Has comenzado una nueva aventura! Este es tu renacimiento número <%= number %>, y el nivel más alto que has conseguido es <%= level %>. Para apilar este logro, ¡empieza tu siguiente aventura después de haber conseguido un nivel más alto!",
"rebirthAchievement100": "¡Has comenzado una nueva aventura! Este es el Renacimiento <%= number %> para ti, y el Nivel más alto que has conseguido es 100 o superior. Para acumular este Logro, ¡empieza tu aventura siguiente después de haber conseguido al menos el nivel 100!",
"rebirthBegan": "Comienza una nueva aventura",
"rebirthText": "Has comenzado <%= rebirths %> nuevas aventuras",
"rebirthOrb": "Usó un Orbe de Renacimiento para comenzar de nuevo después de alcanzar el Nivel <%= level %>.",
@@ -11,12 +11,5 @@
"rebirthPop": "Reinicia instantáneamente tu personaje como un Guerrero de Nivel 1, manteniendo logros, colecciones y equipamiento. Tus tareas y su historial se mantendrán, pero volverán a ser amarillas. Tus rachas desaparecerán excepto por tareas pertenecientes a Desafíos y Planes Grupales activos. Tu Oro, Experiencia, Maná y los efectos de todas tus habilidades desaparecerán. Todo esto tendrá efecto inmediato.",
"rebirthName": "Esfera de Renacimiento",
"rebirthComplete": "¡Has vuelto a nacer!",
"nextFreeRebirth": "<strong><%= days %> días</strong> hasta la Orbe de Renacimiento <strong>GRATIS</strong>",
"rebirthUnlockedOrb": "¡Una nueva aventura está disponible!",
"rebirthNewAchievement": "Nuevo Logro",
"rebirthNewAdventure": "¡Una nueva aventura comienza!",
"rebirthAchievementPlural": "Has utilizado el Orbe del Renacimiento <strong><%= number %></strong> veces, y el nivel más alto que has conseguido <strong><%= level %></strong>.",
"rebirthStackInfo": "Este logro se acumulará cada vez que uses el Orbe del Renacimiento.",
"rebirthUnlockedNewItem": "Orbe del Renacimiento Desbloqueado",
"rebirthUnlockedDesc": "¡Usa el Orbe del Renacimiento para darle nuevo significado a tu aventura en Habitica cuando sientas que ya has logrado todo! Vuelve a empezar desde el nivel 1 conservando tus tareas, Logros y Mascotas con este objeto especial que encontrarás en el Mercado."
"nextFreeRebirth": "<strong><%= days %> días</strong> hasta la Orbe de Renacimiento <strong>GRATIS</strong>"
}
+1 -5
View File
@@ -273,9 +273,5 @@
"mysterySet202512": "Conjunto de Galleta Ganadora",
"mysterySet202601": "Conjunto Égida de Invierno",
"mysterySet202602": "Conjunto Zorro del Sakura",
"subscriptionBillingFYI": "Las suscripciones se renuevan automáticamente a no ser que se cancelen con 24 horas de antelación antes de que finalice el periodo en curso. Puedes modificar tu suscripción desde la pestaña Suscripción en los ajustes. Se te cargará a tu cuenta dentro de las 24 horas de la fecha de renovación, al mismo precio que pagaste inicialmente.",
"mysterySet202603": "Conjunto de Mago de las Glicinias",
"mysterySet202604": "Conjunto Astronauta Audaz",
"mysterySet202605": "Conjunto Nimbus del Anochecer",
"subscriptionBillingFYIShort": "Las suscripciones se renuevan automáticamente a menos que las canceles al menos 24 horas antes de que finalice el período actual. Se te cobrará el importe correspondiente en las 24 horas siguientes a la fecha de renovación, al mismo precio que pagaste inicialmente."
"subscriptionBillingFYI": "Las suscripciones se renuevan automáticamente a no ser que se cancelen con 24 horas de antelación antes de que finalice el periodo en curso. Puedes modificar tu suscripción desde la pestaña Suscripción en los ajustes. Se te cargará a tu cuenta dentro de las 24 horas de la fecha de renovación, al mismo precio que pagaste inicialmente."
}
@@ -932,7 +932,7 @@
"backgroundElvenCitadelText": "ciudadela élfica",
"backgroundElvenCitadelNotes": "Tome un viaje escénico a una ciudadela élfica.",
"backgrounds122025": "Lote 139: Lanzamiento en diciembre de 2025",
"backgroundNighttimeStreetWithShopsText": "Calle Nocturna con Tiendas",
"backgroundNighttimeStreetWithShopsText": "Calle nocturna con tiendas",
"backgroundNighttimeStreetWithShopsNotes": "Disfrutre del calido resplandor de una calle nocturna con tiendas.",
"backgrounds012026": "Lote 140: Lanzamiento enero de 2026",
"backgroundWinterDesertWithSaguarosText": "Desierto invernal con saguaros",
+1 -1
View File
@@ -172,7 +172,7 @@
"editProfile": "Editar Perfil",
"challengesWon": "Desafíos Ganados",
"questsCompleted": "Misiones Completadas",
"headAccess": "Accesorio para la Cabeza.",
"headAccess": "Accesorío para la Cabeza.",
"backAccess": "Accesorio para la Espalda.",
"bodyAccess": "Accesorio para el Cuerpo.",
"mainHand": "Mano Principal",
@@ -8,7 +8,7 @@
"commGuideHeadingInteractions": "Interacciones en Habitica",
"commGuidePara015": "Habitica tiene algunos espacios donde puedes interactuar con otros jugadores. Estos incluyen contextos privados (mensajes privados y mensajería de Grupo) además de la característica de Buscar Equipo y Desafíos.",
"commGuidePara016": "Al navegar por los componentes sociales en Habitica, hay algunas reglas generales para mantener a todos seguros y felices.",
"commGuideList02A": "<strong>Respétense unos a otros</strong>. Sean cortéses, amables, amigables y serviciales. Recuerda: los Habiticanos vienen de todo tipo de contextos y han tenido experiencias muy diferentes.",
"commGuideList02A": "<strong>Respétense unos a los otros</strong>. Sean cortéses, amables , amigables y serviciales. Recuerda: los Habiticanos vienen de todo tipo de contextos y han tenido experiencias muy diferentes.",
"commGuideList02C": "<strong>No publiques imágenes o textos que sean violentos, amenazantes, sexualmente explícitos o sugestivos, o que promuevan la discriminación, intolerancia, racismo, sexismo, odio, acoso o daño hacia cualquier individuo o grupo</strong>. Ni siquiera como una broma o meme. Esto incluye tanto insultos como declaraciones. No todos tienen el mismo sentido del humor, por lo que algo que tú consideres como broma podría ser hiriente para otros.",
"commGuideList02D": "<strong>Mantén las discusiones apropiadas para todas las edades.</strong> Esto significa evitar temas de adultos en espacios públicos. Tenemos muchos Habiticanos jóvenes que usan el sitio, y gente que viene de muchos contextos diferentes. Queremos que nuestra comunidad sea tan cómoda e incluyente como sea posible.",
"commGuideList02E": "<strong>Evita las obscenidades. Esto incluye groserías leves, basadas en la religión, que pueden ser aceptables en otros lugares y groserías abreviadas o camufladas.</strong> Tenemos gente de todos los contextos religiosos y culturales, y queremos asegurarnos de que todos ellos se sientan cómodos en los espacios públicos. <strong>Si un moderador o miembro del personal te dice que un término no está permitido en Habitica, incluso si no te diste cuenta de que el término era problemático, esa decisión es definitiva.</strong> Además, los insultos serán tratados de manera muy severa, ya que también son una violación a los Términos de Servicio.",
@@ -22,7 +22,7 @@
"commGuidePara050": "Abrumadoramente, los Habiticanos se ayudan los unos a los otros, son respetuosos y trabajan para que toda la comunidad sea divertida y amigable. Sin embargo, una vez cada muerte de obispo, algo que un Habiticano hace puede violar una de las pautas mencionadas. Cuando esto ocurra, los Mods actuarán de la manera que crean necesaria para mantener a Habitica segura y agradable para todos.",
"commGuidePara051": "<strong>Hay varios tipos de infracciones, y se tratan dependiendo de su gravedad</strong>. Estas no son listas exhaustivas, y los Mods pueden tomar decisiones en temas que no están cubiertos aquí a su discreción. Los Mods tendrán en cuenta el contexto al evaluar las infracciones.",
"commGuideHeadingSevereInfractions": "Infracciones graves",
"commGuidePara052": "Las Infracciones Severas afectan de forma considerable la seguridad de la comunidad y a los usuarios de Habitica, y por lo tanto tienen consecuencias severas.",
"commGuidePara052": "Infracciones severas dañan de forma importante la seguridad de la comunidad y a los usuarios de Habitica, y por lo tanto tienen consecuencias severas.",
"commGuidePara053": "Los siguientes son ejemplos de algunas infracciones severas. Esta no es una lista completa.",
"commGuideList05A": "Violación de los Términos y condiciones",
"commGuideList05B": "Comentarios de Odio/imágenes de Odio, Acoso, Ciber-bullying, Mensajes ofensivos, y Provocaciones",
@@ -81,6 +81,6 @@
"commGuideList01A": "Nuestros Términos y Condiciones se aplican en Desafíos, Equipos, perfiles de jugador y mensajes privados.",
"commGuideList02M": "No pidas gemas, suscripciones o membresía en Planes de Grupo. Esto no está permitido en la Taberna, espacios de chat públicos o privados, ni en mensajes privados. Si recibes mensajes solicitando artículos de pago, por favor márcalos para reportarlos. Pedir gemas o suscripciones repetida o intensamente, especialmente después de una advertencia, puede resultar en la suspensión de tu cuenta.",
"commGuideList05H": "Intentos severos o repetidos de defraudar o presionar a otros jugadores por artículos de dinero real",
"commGuideList02N": "<strong>Denuncia cualquier cosa que veas que rompa estas Reglas o nuestros Términos de Servicio.</strong> Puedes reportar los mensajes directamente o notificar a los moderadores a través de <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> por violaciones en perfiles o Desafíos. Lo manejaremos lo más rápido posible. Puedes contactarnos en tu idioma nativo si te es más fácil: quizá tengamos que usar el Traductor de Google, pero queremos que te sientas cómodo al contactarnos si tienes un problema.",
"commGuideList02H": "<strong>Todos los Nombres Públicos y @nombresdeusuario deben cumplir con los Términos de Servicio</strong>. Para cambiar tu Nombre Público y/o tu @nombredeusuario: en móvil ve a Menu > Ajustes > Cuenta. En el sitio web, ve a Ajustes desde el ícono de usuario en la barra de navegación en la parte superior."
"commGuideList02N": "<strong>Denuncia cualquier cosa que veas que rompe estas Pautas o nuestros Términos de Servicio.</strong> Puedes reportar los mensajes directamente o notificar a los Moderadores a través de <a href='mailto:admin@habitica.com' target='_blank'>admin@habitica.com</a> por violaciones en perfiles o Desafíos. Lo veremos lo mas rápido posible. Puedes contactarnos en tu lenguaje nativo si es mas fácil para tí: Tal vez tengamos que usar el Traductor de Google, pero queremos que te sientas cómodo en contactarnos si tienes un problema.",
"commGuideList02H": "<strong>Todos los Nombres Públicos y @nombresdeusuario deben cumplir con los Términos de Servicio</strong>. Para cambiar tu Nombre Público y/o tu @nombredeusuario: en móvil ve a Menu > Ajustes > Cuenta. En la web, ve a Ajustes desde el ícono de usuario en la navegación en la parte superior."
}
+60 -111
View File
@@ -140,9 +140,9 @@
"weaponSpecialFallRogueText": "Estaca de Plata",
"weaponSpecialFallRogueNotes": "Elimina muertos vivientes. También otorga un bono contra hombres lobo, porque nunca se puede ser demasiado cuidadoso. Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialFallWarriorText": "Garra Acaparadora de Ciencia",
"weaponSpecialFallWarriorNotes": "Esta garra acaparadora es lo más avanzado en tecnología. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialFallWarriorNotes": "Esta garra acaparadora es lo más avanzado en tecnología. Incrementa la Fuerza en <%= str %>;. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialFallMageText": "Escoba Mágica",
"weaponSpecialFallMageNotes": "¡Esta escoba encantada vuela más rápido que un dragón! Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialFallMageNotes": "¡Esta escoba encantada vuela más rápido que un dragón! Incrementa la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialFallHealerText": "Varita de Escarabajo",
"weaponSpecialFallHealerNotes": "El escarabajo en esta vara mágica protege y cura a quien la empuña. Incrementa la Inteligencia por <%= int %>. Equipamiento de Edición Limitada de Otoño 2014.",
"weaponSpecialWinter2015RogueText": "Pincho de Hielo",
@@ -172,11 +172,11 @@
"weaponSpecialFall2015RogueText": "Hacha de Bati-Batalla",
"weaponSpecialFall2015RogueNotes": "Las Pendientes Aterradoras se encogen de miedo ante el batido de esta hacha. Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015WarriorText": "Tabla de Madera",
"weaponSpecialFall2015WarriorNotes": "Excelente para elevar cosas en los maizales y/o abofetear tareas. Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015WarriorNotes": "Excelente para elevar cosas en los maizales y/o abofetear a tus tareas. Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015MageText": "Hilo Encantado",
"weaponSpecialFall2015MageNotes": "¡Una poderosa Bruja de la Aguja puede controlar este hilo encantado sin siquiera tocarlo! Incrementa la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015MageNotes": "¡Una poderosa Bruja de la Aguja puede controlar este hilo encantado sin siquiera tocarlo! Incrementa la Inteligencia por <%= int %> y la Percepción por <%= per %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015HealerText": "Poción de Cieno de Pantano",
"weaponSpecialFall2015HealerNotes": "¡Preparada a la perfección! Ahora sólo tienes que convencerte de beberla. Incrementa la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialFall2015HealerNotes": "¡Preparada a la perfección! Ahora sólo tienes que convencerte de beberla. Incrementa la Inteligencia por <%= int %>. Equipamiento de Edición Limitada de Otoño 2015.",
"weaponSpecialWinter2016RogueText": "Taza de Chocolate",
"weaponSpecialWinter2016RogueNotes": "¿Bebida caliente o proyectil ardiente? Tú decides... Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Invierno 2015-2016.",
"weaponSpecialWinter2016WarriorText": "Pala Robusta",
@@ -202,13 +202,13 @@
"weaponSpecialSummer2016HealerText": "Tridente sanador",
"weaponSpecialSummer2016HealerNotes": "Una espina daña, la otra cura. Incrementa la inteligencia en <%= int %>. Equipamiento de Edición Limitada de Verano 2016.",
"weaponSpecialFall2016RogueText": "Daga de mordida de araña",
"weaponSpecialFall2016RogueNotes": "¡Siente el dolor de la picadura de la araña! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016RogueNotes": "¡Siente el dolor de la picadura de la araña! Incrementa la Fuerza por <%= str %>. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016WarriorText": "Raices que atacan",
"weaponSpecialFall2016WarriorNotes": "¡Ataca tus tareas con estas retorcidas raíces! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016WarriorNotes": "!Ataca tus tareas con estas retorcidas raíces! Incrementa la Fuerza por <%= str %>. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016MageText": "Orbe Siniestro",
"weaponSpecialFall2016MageNotes": "No le pidas a este orbe que te revele tu futuro... Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016MageNotes": "No le pidas a este orbe que te revele tu futuro... Incrementa la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016HealerText": "Serpiente Venenosa",
"weaponSpecialFall2016HealerNotes": "Una mordida daña y otra mordida cura. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialFall2016HealerNotes": "Una mordida daña y otra mordida cura. Incrementa la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Otoño 2016.",
"weaponSpecialWinter2017RogueText": "Hacha de Hielo",
"weaponSpecialWinter2017RogueNotes": "¡Esta hacha es buenísima para el ataque, defensa y escalada en hielo! Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Invierno 2016-2017.",
"weaponSpecialWinter2017WarriorText": "Palo del Poder",
@@ -234,19 +234,19 @@
"weaponSpecialSummer2017HealerText": "Varita de Perla",
"weaponSpecialSummer2017HealerNotes": "Un solo toque de esta varita con punta de perla calma todas las heridas. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Verano 2017.",
"weaponSpecialFall2017RogueText": "Mazo de Manzana Caramelizada",
"weaponSpecialFall2017RogueNotes": "¡Derrota a tus enemigos con dulzura! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017RogueNotes": "¡Derrota a tus enemigos con dulzura! Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017WarriorText": "Lanza de Maíz Dulce",
"weaponSpecialFall2017WarriorNotes": "Todos tus enemigos se acobardarán ante esta lanza de apariencia dulce, independientemente de si son fantasmas, monstruos o tareas Pendientes en rojo. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017WarriorNotes": "Todos tus enemigos se acobardarán ante esta lanza de apariencia dulce, independientemente de si son fantasmas, monstruos o tareas Pendientes rojas. Incrementa la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017MageText": "Báculo Escalofriante",
"weaponSpecialFall2017MageNotes": "Los ojos de la calavera incandescente en este báculo irradian magia y misterio. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017MageNotes": "Los ojos de la calavera incandescente en este báculo irradian magia y misterio. Incrementa Ia Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada Otoño 2017.",
"weaponSpecialFall2017HealerText": "Candelabro Terrorífico",
"weaponSpecialFall2017HealerNotes": "Esta luz disipa el miedo y les hace saber a los otros que tú estás ahí para ayudar. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2017.",
"weaponSpecialFall2017HealerNotes": "Esta luz disipa el miedo y les hace saber a los otros que tú estás ahí para ayudar. Incrementa la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Otoño 2017.",
"weaponSpecialWinter2018RogueText": "Gancho de menta",
"weaponSpecialWinter2018RogueNotes": "Perfecto para escalar paredes o para distraer a tus oponentes con golosinas muy, muy dulces. Aumenta la Fuerza en <%= str %>. Equipo de Invierno de Edición Limitada 2017-2018.",
"weaponSpecialWinter2018WarriorText": "Martillo de Proa Festivo",
"weaponSpecialWinter2018WarriorNotes": "¡La chispeante apariencia de esta brillante arma deslumbrará a tus enemigos mientras la blandes! Aumenta la Fuerza en <%= str %>. Equipo de Invierno de Edición Limitada 2017-2018.",
"weaponSpecialWinter2018MageText": "Confeti Festivo",
"weaponSpecialWinter2018MageNotes": "¡La magiay el brilloestán en el aire! Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Invierno 2017-2018.",
"weaponSpecialWinter2018MageNotes": "¡La magia--y el brillo--están en el aire! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipo de Invierno de Edición Limitada 2017-2018.",
"weaponSpecialWinter2018HealerText": "Varita de Muérdago",
"weaponSpecialWinter2018HealerNotes": "¡Esta bola de muérdago seguramente encantará y deleitará a los transeúntes! Aumenta la Inteligencia en <%= int %>. Equipo de Invierno de Edición Limitada 2017-2018.",
"weaponSpecialSpring2018RogueText": "Totora Boyante",
@@ -266,13 +266,13 @@
"weaponSpecialSummer2018HealerText": "Tridente de Sirena Monarca",
"weaponSpecialSummer2018HealerNotes": "Con un gesto benévolo, ordenas que el agua curativa fluya en ondas a través de tus dominios. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada Verano 2018.",
"weaponSpecialFall2018RogueText": "Vial de Claridad",
"weaponSpecialFall2018RogueNotes": "Cuando necesites volver a la realidad, cuando necesites un pequeño empujón para tomar la decisión correcta, respira hondo y toma un sorbo. ¡Todo estará bien! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2018.",
"weaponSpecialFall2018RogueNotes": "Cuando necesitas volver a la realidad, cuando necesitas un pequeño empuje para hacer la decisión correcta, respira hondo y toma un sorbo. ¡Todo estará bien! Aumenta la Fuerza en <%= str %>. Equipo de Otoño de 2018, Edición Limitada.",
"weaponSpecialFall2018WarriorText": "Látigo de Minos",
"weaponSpecialFall2018WarriorNotes": "No es suficientemente largo para usarlo como cuerda de guía en un laberinto. Bueno, tal vez en un laberinto muy pequeño. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2018.",
"weaponSpecialFall2018WarriorNotes": "No es suficientemente largo para usarlo como cuerda de guía en un laberinto. Bueno, tal vez en un laberinto muy pequeño. Aumenta la Fuerza en <%= str %>. Equipo de Otoño 2018, Edición Limitada.",
"weaponSpecialFall2018MageText": "Bastón de Dulzura",
"weaponSpecialFall2018MageNotes": "¡Esta no es una chupeta cualquiera! El orbe brillante de azúcar mágica que corona este bastón tiene el poder de hacer que los buenos hábitos se adhieran a ti. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2018.",
"weaponSpecialFall2018MageNotes": "¡Esta no es una chupeta cualquiera! El orbe brillante de azúcar mágica que corona este bastón tiene el poder de hacer que los buenos hábitos se adhieran a ti. Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipo de Otoño 2018, Edición Limitada.",
"weaponSpecialFall2018HealerText": "Bastón Hambriento",
"weaponSpecialFall2018HealerNotes": "Sólo mantén este bastón alimentado y concederá Bendiciones. Si olvidas alimentarlo, mantén los dedos fuera de su alcance. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2018.",
"weaponSpecialFall2018HealerNotes": "Sólo mantén este bastón alimentado y concederá Bendiciones. Si olvidas alimentarlo, mantén los dedos fuera de su alcance. Aumenta la Inteligencia en <%= int %>. Equipo de Otoño 2018, Edición Limitada.",
"weaponSpecialWinter2019RogueText": "Ramo de Flores de Pascua",
"weaponSpecialWinter2019RogueNotes": "Usa este festivo ramo para camuflarte mejor ¡O para regalarlo generosamente y alegrarle el día a un amigo! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Invierno 2018-2019.",
"weaponSpecialWinter2019WarriorText": "Alabarda Copo de Nieve",
@@ -326,7 +326,7 @@
"weaponArmoireBasicLongbowText": "Arco Largo Básico",
"weaponArmoireBasicLongbowNotes": "Un arco útil de segunda mano. Incrementa la Fuerza por <%= str %>. Armario Encantado: Conjunto Básico de Arquero (Artículo 1 de 3).",
"weaponArmoireHabiticanDiplomaText": "Diploma Habiticano",
"weaponArmoireHabiticanDiplomaNotes": "Un certificado por un logro significante; ¡bien hecho! Incrementa la Inteligencia por <%= int %>. Armario Encantado: Conjunto Graduado (Artículo 1 de 3).",
"weaponArmoireHabiticanDiplomaNotes": "Un certificado por un logro significante -- ¡Bien hecho! Incrementa la Inteligencia por <%= int %>. Armario Encantado: Conjunto de Graduado (Artículo 1 de 3).",
"weaponArmoireSandySpadeText": "Pala arenosa",
"weaponArmoireSandySpadeNotes": "Una herramienta para escavar... y para arrojar arena a lo ojos de monstruos enemigos. Aumenta la Fuerza por <%= str %>. Armario Encantado: Conjunto Costero (Artículo 1 de 3).",
"weaponArmoireCannonText": "Cañón",
@@ -492,13 +492,13 @@
"armorSpecialSummerHealerText": "Cola de Marandero",
"armorSpecialSummerHealerNotes": "¡Esta prenda con escamas relucientes transforma a quien la usa en un Marandero real! Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Verano 2014.",
"armorSpecialFallRogueText": "Túnica Rojo Sangre",
"armorSpecialFallRogueNotes": "Vívido. Aterciopelado. Vampírico. Aumenta <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallRogueNotes": "Vívida. Aterciopelada. Vampírica. Incrementa la Percepción por <%= per %>. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallWarriorText": "Guardapolvo de Ciencia",
"armorSpecialFallWarriorNotes": "Te protege de derrames de pociones misteriosas. Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallWarriorNotes": "Te protege de derrames de pociones misteriosas. Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallMageText": "Túnica Embrujada",
"armorSpecialFallMageNotes": "Esta túnica tiene muchos bolsillos para guardar raciones adicionales de ojo de tritón y lengua de rana. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallMageNotes": "Esta túnica tiene muchos bolsillos para guardar raciones adicionales de ojo de tritón y lengua de rana. Incrementa la Inteligencia por <%= int %>. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallHealerText": "Armadura de Gasa",
"armorSpecialFallHealerNotes": "¡Irrumpe en la batalla pre-vendado! Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialFallHealerNotes": "¡Irrumpe en la batalla pre-vendado! Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Otoño 2014.",
"armorSpecialWinter2015RogueText": "Armadura de Bestia de Hielo",
"armorSpecialWinter2015RogueNotes": "Esta armadura es extremadamente fría, pero definitivamente valdrá la pena cuando descubras las riquezas inconmensurables en el centro de las colmenas de las Bestias de Hielo. Por supuesto, no quiere decir que estés buscando activamente esas riquezas inconmensurables, porque tú real, definitiva y absolutamente eres una Bestia de Hielo, ¡¿sí?! ¡Deja de hacer preguntas! Incrementa la Percepción por <%= per %>. Equipamiento de Edición Limitada de Invierno de 2014-2015.",
"armorSpecialWinter2015WarriorText": "Armadura de Pan de Jengibre",
@@ -524,13 +524,13 @@
"armorSpecialSummer2015HealerText": "Armadura de Marinero",
"armorSpecialSummer2015HealerNotes": "Esta armadura les demuestra a todos que eres un marinero comerciante honesto que jamás pensaría en comportarse como un sinvergüenza. Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Verano 2015.",
"armorSpecialFall2015RogueText": "Armadura de Bati-Batalla",
"armorSpecialFall2015RogueNotes": "¡Vuela hacia la bati-batalla! Aumenta <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015RogueNotes": "¡Vuela hacia la bati-batalla! Incrementa la Percepción por <%= per %>. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015WarriorText": "Armadura de Espantapájaros",
"armorSpecialFall2015WarriorNotes": "A pesar de estar rellena de paja, ¡esta armadura es extremadamente pesada! Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015WarriorNotes": "A pesar de estar rellena de paja, ¡esta armadura es extremadamente pesada! Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015MageText": "Túnica Cosida",
"armorSpecialFall2015MageNotes": "Cada puntada en esta armadura centellea al estar encantada. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015MageNotes": "Cada puntada en esta armadura centellea al estar encantada. Incrementa la Inteligencia por <%= int %>. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015HealerText": "Túnica de Fabricante de Pociones",
"armorSpecialFall2015HealerNotes": "¿Qué? Por supuesto que esa era una poción de complexión. No, ¡definitivamente no te estás transformando en una rana! No seas ilógi*croac*. Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialFall2015HealerNotes": "¿Qué? Por supuesto que esa era una poción de constitución. No, ¡definitivamente no te estás transformando en una rana! No seas ilógicroac. Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Otoño 2015.",
"armorSpecialWinter2016RogueText": "Armadura de Cacao",
"armorSpecialWinter2016RogueNotes": "Esta armadura de cuero te mantiene cómodo y calentito. ¿Realmente está hecha de cacao? Nunca te darás cuenta. Incrementa la Percepción por <%= per %>. Equipamiento de Edición Limitada de Invierno 2015-2016.",
"armorSpecialWinter2016WarriorText": "Traje de Muñeco de Nieve",
@@ -556,13 +556,13 @@
"armorSpecialSummer2016HealerText": "Cola de caballito de mar",
"armorSpecialSummer2016HealerNotes": "Esta prenda puntiaguda convierte a su usuario en un verdadero Hipocampo Sanador. Incrementa la Constitución por <%= con %>. Equipamiento de edición limitada Verano de 2016.",
"armorSpecialFall2016RogueText": "Armadura de viuda negra",
"armorSpecialFall2016RogueNotes": "Los ojos de esta armadura parpadean constantemente. Aumenta <%= per %> de Percepción. Equipamento de Edición Limitada de Otoño 2016.",
"armorSpecialFall2016RogueNotes": "Los ojos en esta armadura parpadean constantemente. Incrementa la Percepción en <%= per %>. Equipo de Otoño 2016, Edición Limitada.",
"armorSpecialFall2016WarriorText": "Armadura de Baba a Rayas",
"armorSpecialFall2016WarriorNotes": "¡Misteriosamente húmedo y musgoso! Aumenta <%= con %> de Constitución. Equipamento de Edición Limitada de Otoño 2016.",
"armorSpecialFall2016WarriorNotes": "¡Misteriosamente húmedo y musgoso! Incrementa la Constitución en <%= con %>. Equipo de Otoño 2016, Edición Limitada.",
"armorSpecialFall2016MageText": "Capa de la maldad",
"armorSpecialFall2016MageNotes": "Cuando tu capa se agita, puedes oír el sonido de una carcajada. Aumenta <%= int %> de Inteligencia. Equipamento de Edición Limitada de Otoño 2016.",
"armorSpecialFall2016MageNotes": "Cuando tu capa se agita, puedes oír el sonido de una carcajada. Incrementa la Inteligencia en <%= int %>. Equipo de Otoño 2016, Edición Limitada.",
"armorSpecialFall2016HealerText": "Túnica de Gorgona",
"armorSpecialFall2016HealerNotes": "En realidad esta túnica esta hecha de piedra. ¿Cómo es tan cómoda? Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2016.",
"armorSpecialFall2016HealerNotes": "En realidad esta túnica esta hecha de piedra. ¿Cómo es tan cómoda? Incrementa la Constitución por <%= con %>. Equipamiento de Edición Limitada de Otoño 2016.",
"armorSpecialWinter2017RogueText": "Armadura Helada",
"armorSpecialWinter2017RogueNotes": "¡Este sigiloso traje refleja la luz para deslumbrar a tus desprevenidas tareas mientras tomas tus recompensas de ellas! Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada Invierno 2016-2017.",
"armorSpecialWinter2017WarriorText": "Armadura de Hockey en Hielo",
@@ -588,13 +588,13 @@
"armorSpecialSummer2017HealerText": "Cola Marplata",
"armorSpecialSummer2017HealerNotes": "¡Esta prenda de escamas plateadas transforma a su usuario en un verdadero Sanador Marino! Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada de Verano 2017.",
"armorSpecialFall2017RogueText": "Ropajes de Huerto de Calabaza",
"armorSpecialFall2017RogueNotes": "¿Necesitas esconderte? ¡Agáchate ente las Calabazas de Halloween y esta túnica te ocultará! Aumenta <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2017.",
"armorSpecialFall2017RogueNotes": "¿Necesitas esconderte? ¡Agáchate ente las Calabazas de Halloween y este ropaje te ocultará! Aumenta la Percepción en <%= per %>. Equipamiento de Otoño de Edición Limitada 2017.",
"armorSpecialFall2017WarriorText": "Armadura Fuerte y dulce",
"armorSpecialFall2017WarriorNotes": "Esta armadura te protegerá como una deliciosa capa de caramelo. Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2017.",
"armorSpecialFall2017WarriorNotes": "Esta armadura te protegerá como una deliciosa cascara de caramelo. Aumenta la Constitución en <%= con %>. Equipamiento de Otoño de Edición Limitada 2017.",
"armorSpecialFall2017MageText": "Ropaje de Mascarada",
"armorSpecialFall2017MageNotes": "¿Qué conjunto de mascarada estaría completo sin ropajes dramáticos y deslumbrantes? Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2017.",
"armorSpecialFall2017MageNotes": "¿Qué grupo de mascarada estaría completo sin ropajes dramáticos y deslumbrantes? Aumenta la Inteligencia en <%= int %>. Equipamiento de Otoño de Edición Limitada 2017.",
"armorSpecialFall2017HealerText": "Armadura de Casa Embrujada",
"armorSpecialFall2017HealerNotes": "Tu corazón es una puerta abierta. ¡Y tus hombros son tejas! Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2017.",
"armorSpecialFall2017HealerNotes": "Tu corazón es una puerta abierta. ¡Y tus hombros son tejas! Aumenta la Constitución en <%= con %>. Equipamiento de Otoño de Edición Limitada 2017.",
"armorSpecialWinter2018RogueText": "Disfraz de Reno",
"armorSpecialWinter2018RogueNotes": "Te ves tan adorable y suave, ¿quién podría sospechar que vas tras el botín festivo? Aumenta la Percepción en <%= per %>. Equipamiento de Invierno de Edición Limitada, 2017-2018.",
"armorSpecialWinter2018WarriorText": "Armadura de Papel para Envolver",
@@ -620,13 +620,13 @@
"armorSpecialSummer2018HealerText": "Ropaje de Monarca Sirena",
"armorSpecialSummer2018HealerNotes": "Estas vestiduras cerúleas revelan que tienes pies que caminan por la tierra... bueno. Ni siquiera un monarca puede ser perfecto. Aumenta la Constitución en <%= con %>. Equipo de Edición Limitada de Verano, 2018.",
"armorSpecialFall2018RogueText": "Levita de Alter Ego",
"armorSpecialFall2018RogueNotes": "Estilo para el día. Comodidad y protección para la noche. Aumenta <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2018.",
"armorSpecialFall2018RogueNotes": "Estilo para el día. Comodidad y protección para la noche. Aumenta la Percepción en <%= per %>. Equipamiento de Otoño de Edición Limitada, 2018.",
"armorSpecialFall2018WarriorText": "Armadura de Minotauro",
"armorSpecialFall2018WarriorNotes": "Completa con cascos para tamborilear una cadencia suave mientras paseas por tu laberinto meditativo. Aumenta <%= con %> de Constitución. Equipamiento de Edición Limitada de Otoño 2018.",
"armorSpecialFall2018WarriorNotes": "Completa con cascos para tamborilear una cadencia suave mientras paseas por tu laberinto meditativo. Aumenta la Constitución en <%= con %>. Equipamiento de Otoño de Edición Limitada, 2018.",
"armorSpecialFall2018MageText": "Túnica de Dulcimante",
"armorSpecialFall2018MageNotes": "¡La tela de esta túnica tiene dulces mágicos tejidos dentro! Aunque te recomendamos que no intentes comerlos. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2018.",
"armorSpecialFall2018MageNotes": "¡La tela de esta túnica tiene dulces mágicos tejidos dentro! Aunque te recomendamos que no intentes comerlas. Aumenta la Inteligencia en <%= int %>. Equipamiento de Otoño de Edición Limitada, 2018.",
"armorSpecialFall2018HealerText": "Túnicas de Carnívoro",
"armorSpecialFall2018HealerNotes": "Está hecho de plantas, pero eso no significa que sea vegetariano. Los malos hábitos temen acercarse a varios kilómetros de estas túnicas. Aumenta <%= con %> de Complexión. Equipamiento de Edición Limitada de Otoño 2018.",
"armorSpecialFall2018HealerNotes": "Está hecho de plantas, pero eso no significa que son vegetarianas. Los Malos Hábitos tienen miedo de acercarse a varios kilómetros de estas túnicas. Aumenta la Constitución en <%= con %>. Equipamiento de Otoño de Edición Limitada, 2018.",
"armorSpecialWinter2019RogueText": "Armadura de Flor de Pascua",
"armorSpecialWinter2019RogueNotes": "¡Con la vegetación navideña por todas partes, nadie va a notar otro arbusto más! Puedes moverte por las reuniones festivas con facilidad y sigilo. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada Invierno 2018-2019.",
"armorSpecialWinter2019WarriorText": "Armadura Glacial",
@@ -1723,7 +1723,7 @@
"weaponSpecialSpring2019HealerText": "Canto Primaveral",
"weaponArmoireBaseballBatNotes": "¡Anota un cuadrangular a esos buenos hábitos! Aumenta la Constitución en <%= con %>. Armario Encantado: Conjunto de Béisbol (Artículo 3 de 4).",
"weaponSpecialFall2019WarriorText": "Tridente de Garra",
"weaponSpecialFall2019RogueNotes": "Ya sea que estés dirigiendo la orquesta o cantando un aria, ¡este útil dispositivo mantiene tus manos libres para gestos dramáticos! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño de 2019.",
"weaponSpecialFall2019RogueNotes": "Sea que estés dirigiendo la orquesta o cantando un aria, ¡este útil dispositivo mantiene tus manos libres para gestos dramáticos! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño de 2019.",
"weaponSpecialFall2019RogueText": "Atril de Música",
"weaponSpecialSummer2019MageNotes": "Fruto de tu esfuerzo y obtenido del estanque, este pequeño tesoro da poder e inspira. Aumenta la Inteligencia en <%= int %>. Equipo de Verano de 2019, Edición Limitada.",
"weaponSpecialSummer2019HealerNotes": "Las burbujas de esta varita capturan energía sanadora y magia oceánica antigua. Aumenta la Inteligencia en <%= int %>. Edición Limitada de verano de 2019.",
@@ -1762,19 +1762,19 @@
"weaponSpecialWinter2020WarriorText": "Cono de Conífera Puntudo",
"weaponSpecialWinter2020RogueNotes": "La oscuridad es un elemento de los Pícaros. Entonces, ¿quién mejor que ellos para iluminar el camino en la época más oscura del año? Aumenta la Fuerza en <%= str %>. Equipo de Invierno 2019-2020, Edición Limitada.",
"weaponSpecialWinter2020RogueText": "Vara de Linterna",
"weaponSpecialFall2019HealerNotes": "Esta filacteria puede llamar los espíritus de tareas abatidas hace tiempo y usar su poder sanador. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2019HealerNotes": "Esta filacteria puede llamar los espíritus de tareas abatidas hace tiempo y usar su poder sanador. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2019HealerText": "Filacteria Temible",
"weaponSpecialFall2019MageNotes": "Sea forjando rayos, erigiendo fortificaciones o, simplemente, infundiendo terror en los corazones de los mortales, este bastón presta el poder de los gigantes para hacer maravillas. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2019MageNotes": "Sea forjando rayos, erigiendo fortificaciones o, simplemente, infundiendo terror en los corazones de los mortales, este bastón presta el poder de los gigantes para hacer maravillas. Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2019MageText": "Bastón Tuerto",
"weaponSpecialFall2019WarriorNotes": "¡Prepárate para desgarrar a tus enemigos con las garras de un cuervo! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2019WarriorNotes": "¡Prepárate para desgarrar a tus enemigos con las garras de un cuervo! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada Otoño 2019.",
"weaponSpecialFall2020RogueText": "Katar Afilado",
"weaponSpecialFall2020WarriorNotes": "Esta espada fue al más allá con un poderoso Guerrero, ¡y regresa para que la manejes! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020WarriorNotes": "Esta espada fue al más allá con un poderoso guerrero, ¡y regresa para que la manejes! Aumenta la fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020WarriorText": "Espada del Espectro",
"weaponSpecialFall2020RogueNotes": "¡Atraviesa a tu enemigo con un golpe seco! Incluso la armadura más gruesa dará paso a tu espada. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020RogueNotes": "¡Atraviesa a tu enemigo con un golpe seco! Incluso la armadura más gruesa dará paso a tu espada. Aumenta la fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020MageText": "Tres Visiones",
"weaponSpecialFall2020MageNotes": "Si algo escapa a tu vista de mago, los cristales brillantes sobre este bastón iluminarán lo que pasaste por alto. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020MageNotes": "Si algo escapa a tu vista de mago, los cristales brillantes sobre este bastón iluminarán lo que pasaste por alto. Aumenta la inteligencia en un <%= int %> y la percepción en un <%= per %>. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020HealerText": "Caña Capullo",
"weaponSpecialFall2020HealerNotes": "Ahora que tu transformación está completa, este remanente de tu vida como pupa ahora sirve como la vara adivina con la que mides los destinos. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2020.",
"weaponSpecialFall2020HealerNotes": "Ahora que tu transformación está completa, este remanente de tu vida como pupa ahora sirve como la vara adivina con la que mides los destinos. Aumenta la inteligencia en <%= int %>. Equipamiento de Edición Limitada de Otoño 2020.",
"armorArmoireBoatingJacketText": "Chaqueta de Barquero",
"armorArmoireNephriteArmorNotes": "Hecha de fuertes anillos de acero y decorada con jade, ¡Esta armadura te protegerá de la procrastinación! Aumenta la Fuerza en <%= str %> y la Percepción en <%= per %>. Armario Encantado: Conjunto de Arquero de Neferita (Artículo 3 de 3).",
"armorArmoireNephriteArmorText": "Armadura de Neferita",
@@ -1948,7 +1948,7 @@
"armorSpecialFall2019MageText": "Delantal de Polifemo",
"armorSpecialFall2019WarriorNotes": "Estas túnicas de plumas otorgan el poder de vuelo, permitiéndote volar sobre cualquier batalla. Aumenta la Constitución en <%= con %>. Equipamiento de Edición Limitada Otoño 2019.",
"armorSpecialFall2019WarriorText": "Alas de la Noche",
"armorSpecialFall2019RogueNotes": "Este traje viene completo con guantes blancos, y es ideal para meditar en tu palco privado sobre el escenario o para hacer entradas sorprendentes por las grandes escaleras. Aumenta <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2019.",
"armorSpecialFall2019RogueNotes": "Este traje viene completo con guantes blancos, y es ideal para meditar en tu palco privado sobre el escenario o para hacer entradas sorprendentes por las grandes escaleras. Aumenta la Percepción en <%= per %>. Equipamiento de Edición Limitada Otoño 2019.",
"armorSpecialFall2019RogueText": "Abrigo de Ópera con Capa",
"headSpecialFall2020HealerNotes": "La terrible palidez de este rostro parecido a una calavera brilla como una advertencia para todos los mortales: ¡El tiempo es fugaz! ¡Atiende a tus plazos, antes de que sea demasiado tarde! Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada Otoño 2020.",
"headSpecialFall2020HealerText": "Máscara de la Cabeza de la Muerte",
@@ -2390,12 +2390,12 @@
"weaponArmoireSkullLanternNotes": "Deja esto brillar en las noches más oscuras de tus aventuras. Aumenta la Inteliencia por <%= int =%>. Armario encantado: Objeto independiente.",
"armorSpecialFall2021RogueText": "Armadura afortunadamente no a prueba de slime",
"weaponSpecialFall2021WarriorText": "Hacha de Jinete",
"weaponSpecialFall2021WarriorNotes": "Esta estilizada hacha de hoja simple es ideal para cortar... ¡calabazas! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño, 2021.",
"weaponSpecialFall2021WarriorNotes": "Esta estilizada hacha de hoja simple es ideal para cortar... ¡calabazas! Aumenta la Fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño, 2021.",
"weaponSpecialFall2021HealerText": "Varita de Invocación",
"weaponSpecialFall2021RogueText": "Baba Escurridiza",
"weaponSpecialFall2021RogueNotes": "¿En qué desastre te has metido? Cuando la gente dice que los pícaros tienen dedos pegajosos, ¡no se refieren a esto! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialFall2021MageNotes": "El conocimiento busca conocimiento. Formada a partir de memorias y deseos, esta temible mano desea aún más. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialFall2021HealerNotes": "Usa esta varita para invocar llamas sanadoras y una criatura fantasmal para ayudarte. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialFall2021RogueNotes": "¿En qué desastre te has metido? Cuando la gente dice que los pícaros tienen dedos pegajosos, ¡no se refieren a esto! Incrementa la fuerza en <%= str %>. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialFall2021MageNotes": "El conocimiento busca conocimiento. Formada a partir de memorias y deseos, esta temible mano desea aún más. Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialFall2021HealerNotes": "Usa esta varita para invocar llamas sanadoras y una criatura fantasmal para ayudarte. Aumenta la Inteligencia en <%= int %>. Equipamiento de Edición Limitada de Otoño 2021.",
"weaponSpecialSpring2022RogueText": "Pendiente de Botón Gigante",
"weaponSpecialSpring2022RogueNotes": "¡Qué brillante! Es tan brillante y resplandeciente y bonito y lindo y todo tuyo. Aumenta la Fuerza en <%= str %>. Equipamiento de edición limitada de primavera 2022.",
"weaponSpecialSpring2022WarriorText": "Paraguas del Revés",
@@ -2508,7 +2508,7 @@
"weaponSpecialSpring2023WarriorText": "Lámina de colibrí",
"weaponSpecialSpring2023WarriorNotes": "¡En garde! ¡Ahuyenta a los enemigos de tus flores con este florete! Aumenta la fuerza en <%= str %>. Edición limitada de primavera de 2023.",
"weaponSpecialSpring2023MageText": "Piedra Lunar Mágica",
"weaponSpecialSpring2023MageNotes": "Cuanto mayor sea su brillo, más potente será su poder. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición limitada de Primavera 2023.",
"weaponSpecialSpring2023MageNotes": "Cuanto mayor sea su brillo, más potente será su poder. Aumenta la inteligencia en <%= int %>. Edición limitada 2023.",
"weaponSpecialSpring2023HealerText": "Polen de lirio",
"weaponSpecialSpring2023HealerNotes": "Con un soplo y una chispa despiertas nuevos brotes, alegría y color. Aumenta la Inteligencia en <%= int %>. Equipamiento de edición limitada de primavera 2023.",
"weaponSpecialWinter2023RogueText": "Banda verde de satén",
@@ -2537,7 +2537,7 @@
"weaponArmoireScholarlyTextbooksNotes": "Aquí tienes la oportunidad de profundizar y aprender sobre cualquier tema que te interese. ¿Cuál es tu hiperfijación actual? Aumenta la Inteligencia en <%= int %>. Armario Encantado: Conjunto de Uniformes Escolares (Artículo 3 de 4).",
"weaponArmoireDragonKnightsLanceText": "Lanza del Caballero Dragón",
"armorSpecialBirthday2023Text": "Fabulosas batas de fiesta",
"weaponArmoireCorsairsBladeNotes": "Ya sea que lo uses para saquear o para proteger, puedes estar feliz de haber traído esta feroz espada al mar contigo. Solo asegúrate de guardarla de manera segura cuando no la estés usando. Aumenta <%= str %> de Fuerza. Armario Encantado: Conjunto Corsario (Artículo 3 de 3).",
"weaponArmoireCorsairsBladeNotes": "Ya sea que lo uses para saquear o para proteger, puedes estar feliz de haber traído esta feroz espada al mar contigo. Solo asegúrate de guardarla de manera segura cuando no la estés usando. Aumenta la Fuerza en <%= str %>. Armario Encantado: Conjunto Corsario (Ítem 3 de 3)",
"armorSpecialWinter2024RogueText": "Túnicas de búho nival",
"armorSpecialWinter2024RogueNotes": "¿Quién serás con esta túnica? Cubierta de plumas y pelusa, ¡estarás abrigada y discreta! Aumenta la percepción en <%= per %>. Edición limitada 2023-2024.",
"armorSpecialWinter2024MageText": "Túnicas de mago narval",
@@ -2550,7 +2550,7 @@
"armorSpecialSummer2024HealerText": "Armadura de caracol marino",
"armorSpecialSummer2024HealerNotes": "No hagas caso a los detractores. ¡Despacio y con constancia es una estrategia válida para afrontar las tareas! Aumenta la Constitución en <%= con %>. Edición limitada de verano de 2024.",
"armorMystery202406Notes": "¡Atormenta a tus enemigos con estilo y elegancia! No ofrece ningún beneficio. Artículo de suscriptor de junio de 2024.",
"weaponArmoireSpookyCandyBucketNotes": "Con un disfraz tan épico como ese, ¡vas a recibir un montón de dulces! Menos mal que tienes este cubo sin fondo para guardarlos todos. Intenta no picar nada hasta llegar a casa. Aumenta <%= int %> de Inteligencia. Armario Encantado: Conjunto Noche de Terror (Artículo 2 de 2).",
"weaponArmoireSpookyCandyBucketNotes": "Con un disfraz tan épico como ese, ¡vas a recibir un montón de dulces! Menos mal que tienes este cubo sin fondo para guardarlos todos. Intenta no picar nada hasta llegar a casa. Aumenta la inteligencia en <%= int %>. Armario Encantado: Set Noche de Terror (Artículo 2 de 2)",
"weaponArmoirePottersWheelText": "Torno de alfarero",
"armorSpecialSpring2024RogueText": "Túnica de nieve derretida",
"weaponSpecialSpring2024WarriorText": "Lanza de fluorita",
@@ -2572,7 +2572,7 @@
"weaponSpecialWinter2025WarriorText": "Hacha de guerra Moose",
"weaponSpecialWinter2025WarriorNotes": "¡Un hacha poderosa para un alce poderoso! ¡Serás imparable! Aumenta la Fuerza en <%= str %>. Edición Limitada Invierno 2024-2025.",
"weaponSpecialWinter2025HealerNotes": "Lo que necesitas ahora son más luces y una estrella que brille en lo alto. ¡Serás imparable! Aumenta la Inteligencia en <%= int %>. Engranaje de invierno 2024-2025 de edición limitada.",
"weaponArmoireDragonKnightsLanceNotes": "Esta lanza roja y plateada ha derribado a muchos oponentes de sus monturas. Aumenta <%= con %> de Complexión. Armario Encantado: Conjunto Caballero Dragón (Artículo 3 de 3).",
"weaponArmoireDragonKnightsLanceNotes": "Esta lanza roja y plateada ha derribado a muchos oponentes de sus monturas. Aumenta la Constitución en <%= con %>. Armario Encantado: Conjunto de Caballero Dragón (Artículo 3 de 3)",
"weaponArmoireFunnyFoolBatonText": "Bastón del tonto gracioso",
"armorSpecialWinter2024WarriorNotes": "Resulta que el chocolate, la menta y el glaseado son materiales más resistentes de lo que crees. Aumenta la Constitución en <%= con %>. Edición limitada 2023-2024.",
"armorSpecialSpring2022HealerText": "Armadura de peridoto",
@@ -2623,7 +2623,7 @@
"weaponSpecialSpring2025HealerText": "Plumeria Crook1",
"weaponSpecialSpring2025MageNotes": "Con un solo tajo, puedes usar la magia elemental para controlar el entorno que te rodea. ¡Aprovecha y salta hacia delante! Aumenta la Inteligencia en <%= int %> y la Percepción en <%= per %>. Edición limitada Spring 2025 Gear.",
"weaponArmoireOptimistsCloverNotes": "Bueno, ¿mirarías lo que encontraste? Nunca está de más tener un poco más de buena suerte de tu lado. Aumenta la Fuerza y la Constitución en un <%= attrs %> cada una. Armario encantado: Conjunto optimista (Objeto 4 de 4).",
"weaponArmoireFunnyFoolBatonNotes": "Con un solo movimiento de bastón, puedes lanzar un remate, redirigir la atención o provocar aplausos. Aumenta <%= attrs %> de Constitución y Fuerza. Armario Encantado: Conjunto Bufón Divertido (Artículo 3 de 3).",
"weaponArmoireFunnyFoolBatonNotes": "Con un solo movimiento de bastón, puedes lanzar un remate, redirigir la atención o provocar aplausos. Aumenta la Constitución y la Fuerza en <%= attrs %> cada una. Armario Encantado: Set del Tonto Divertido (Artículo 3 de 3)",
"armorSpecialSpring2024WarriorText": "Armadura de fluorita",
"armorSpecialSpring2024WarriorNotes": "Esta armadura de piedra estabilizadora te ayudará a mantenerte firme mientras deslumbra a todo lo que enfrentes. Aumenta la Constitución en <%= con %>. Edición limitada de primavera de 2024.",
"armorSpecialSpring2024MageText": "Túnicas de hibisco",
@@ -2648,7 +2648,7 @@
"weaponArmoireHattersShearsNotes": "Corta a través del agobio y las complicaciones. Estas tijeras también cortan muy bien la tela, por supuesto. Aumenta la Fuerza en <%= str %>. Armario Encantado: Set de Sombrerero (Artículo 3 de 4).",
"weaponArmoirePottersWheelNotes": "Echa un poco de arcilla en esta rueda y haz un cuenco o una taza o un jarrón o un cuenco ligeramente diferente. Si tienes suerte, ¡un fantasma podría visitarte mientras creas! Aumenta la percepción en un <%= por %>. Armario encantado: Conjunto de alfarero (Objeto 4 de 4).",
"weaponArmoireCorsairsBladeText": "Espada de corsario",
"weaponArmoireStormKnightAxeNotes": "¡Reúne tu furia y asesta un golpe como un trueno! Aumenta <%= str %> de Fuerza. Armario Encantado: Conjunto Caballero de la Tormenta (Artículo 3 de 3).",
"weaponArmoireStormKnightAxeNotes": "¡Reúne tu furia y asesta un golpe como un trueno! Aumenta la fuerza en <%= str %>. Armario encantado: Conjunto de caballero de la tormenta (objeto 3 de 3)",
"armorSpecialSpring2024HealerNotes": "Estas fabulosas plumas harán realidad tus sueños más felices. Aumenta tu Constitución en <%= con %>. Edición limitada primavera 2024.",
"armorSpecialSummer2024WarriorNotes": "Tras transformarte en un auténtico guerrero tiburón ballena, ¡nada con valentía hacia tus objetivos! Aumenta tu Constitución en <%= con %>. Edición limitada de verano de 2024.",
"armorSpecialSummer2024MageText": "Cola de anémona de mar",
@@ -2689,7 +2689,7 @@
"weaponArmoireFinelyCutGemNotes": "¡Menudo hallazgo! Esta impresionante gema tallada con precisión será el premio de su colección. Y puede que contenga una magia especial, esperando a que la aproveches. Aumenta la Constitución en <%= con %>. Armario Encantado: Set de Joyero (Artículo 4 de 4).",
"weaponArmoireMopNotes": "Paso 1: Sumerja la fregona en un cubo con agua y espuma. Paso 2: Arrastra la fregona por el suelo. Paso 3: Imagina que el extremo del mango de la fregona es un micrófono y canta con todas tus fuerzas. Paso 4: Repite los pasos 1-3 hasta que el suelo esté limpio. Aumenta la Constitución y la Percepción en <%= attrs %> cada una. Armoire Enchanted: Cleaning Supplies Set Two (Item 2 of 3)",
"weaponArmoireRidingBroomText": "Escoba de equitación",
"weaponArmoireRidingBroomNotes": "Realiza todas tus más mágicas encomiendas sobre esta fina escoba o, simplemente, llévala a pasear por el vecindario. ¡Wiii! Aumenta <%= str %> de Fuerza y <%= int %> de Inteligencia. Armario Encantado: Conjunto Hechicería Espeluznante (Artículo 1 de 3)",
"weaponArmoireRidingBroomNotes": "Haz todos tus recados más mágicos con esta escoba o, simplemente, llévala a pasear por el vecindario. ¡Vaya! Aumenta la Fuerza en <%= str %> y la Inteligencia en <%= int %>. Armario Encantado: Set de Hechicería Espeluznante (Artículo 1 de 3)",
"weaponArmoireRollingPinText": "Rodillo",
"weaponArmoireCleaningClothNotes": "Llévate esta herramienta de orden en tus aventuras y siempre podrás pulir una bonita placa o limpiar un alféizar de madera. Aumenta la Fuerza y la Constitución en <%= attrs %> cada una. Armario encantado: set dos de artículos de limpieza (unidad 3 de 3)",
"weaponArmoireRollingPinNotes": "Enrolla tu pasta tan fina como quieras entre golpear malos hábitos cuando aparezcan a tu alrededor como cierto juego de golpear roedores. Aumenta la Fuerza en <%= str %>. Armario encantado: Set de utensilios de cocina 2 (objeto 2 de 2).",
@@ -2794,56 +2794,5 @@
"armorMystery202306Notes": "¡Nadie te va a amargar la fiesta! Y si lo intentan, ¡mantendrás la frescura y la sequedad! No ofrece ningún beneficio. Artículo para suscriptores de junio de 2023.",
"armorMystery202310Text": "Túnica de espectro",
"armorMystery202310Notes": "Una prenda fantasmal que se riza y se mueve con gracia mientras flotas entre los pantanos y páramos embrujados. No ofrece ningún beneficio. Artículo de suscriptor de octubre de 2023.",
"armorArmoireHeraldsTunicText": "Túnica del heraldo",
"weaponSpecialSpring2026WarriorText": "Florete de la Poderosa Ranita",
"weaponSpecialSpring2026RogueText": "Rama Primaveral",
"weaponSpecialSpring2026WarriorNotes": "¡En cualquier momento podría presentarse la oportunidad de batirse en duelo, y con este formidable florete estarás preparado! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026RogueNotes": "¡Una oportunidad de crecimiento se acerca, y con estas ramas en ciernes, estarás preparado! Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponSpecialSpring2026HealerText": "Bastón de Campanilla de Invierno",
"weaponSpecialSpring2026MageText": "Parasol de Palo de Mayo",
"weaponSpecialSpring2026MageNotes": "¡Una oportunidad para celebrar se acerca, y con este bonito parasol, estarás preparado! Aumenta <%= per %> de Inteligencia. Equipamiento de Edición Limitada de Primavera 2026.",
"weaponMystery202512Text": "Espada del Campeón de las Galletas",
"weaponMystery202512Notes": "Una brillante espada hecha de azúcar, menta, y encantamientos arcanos. No otorga ningún beneficio. Artículo para Suscriptores de Diciembre 2025.",
"weaponMystery202601Text": "Égida de Invierno",
"weaponMystery202603Notes": "¡Lanza hechizos para calentar el aire primaveral y favorecer la floración! No otorga ningún beneficio. Artículo para suscriptores de Marzo 2026.",
"weaponMystery202603Text": "Bastón de Mago de las Glicinias",
"weaponArmoireGildedKnightsSpearText": "Lanza del Caballero Dorado",
"weaponArmoireGildedKnightsSpearNotes": "Con esta arma, puedes asegurarte de que todo el mundo pague siempre sus deudas. Aumenta <%= str %> de Fuerza. Armario Encantado: Conjunto Caballero Dorado (Artículo 3 de 3).",
"weaponArmoireBambooFluteText": "Flauta de Bambú",
"weaponArmoirePrettyPinkParasolText": "Parasol Rosa Bonito",
"weaponArmoirePrettyPinkParasolNotes": "La combinación perfecta de belleza y practicidad. Y para una presentación especialmente impactante, ¡dale una vuelta a este parasol! Aumenta <%= attrs %> a todos los atributos. Armario Encantado: Conjunto Bonito en Rosa (Artículo 1 de 2)",
"weaponSpecialFall2025WarriorText": "Hacha de Pie Grande",
"weaponSpecialFall2025WarriorNotes": "Un arma poderosa para abrirse paso de manera segura a través de un bosque otoñal repleto de complicaciones. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2025.",
"weaponSpecialFall2025RogueText": "Cuchilla Esqueleto",
"weaponSpecialFall2025RogueNotes": "Un arma poderosa para abrirse paso de manera segura a través de un bosque otoñal repleto de obstáculos. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Otoño 2025.",
"weaponSpecialFall2025HealerText": "Hacha Kobold",
"weaponSpecialFall2025HealerNotes": "Un arma poderosa para abrirse paso de manera segura a través de un bosque otoñal repleto de impedimentos. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Otoño 2025.",
"weaponSpecialFall2025MageText": "Hacha de Fantasma Enmascarado",
"weaponSpecialFall2025MageNotes": "Un arma poderosa para abrirse paso de manera segura a través de un bosque otoñal repleto de espantos. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Otoño 2025.",
"weaponMystery202511Text": "Espada Helada",
"weaponMystery202511Notes": "El brillo helado de esta espada acabará rápidamente incluso con las Tareas de color rojo oscuro. No otorga ningún beneficio. Artículo para Suscriptores de Noviembre 2025.",
"weaponArmoireBeekeepersSmokerText": "Ahumador",
"weaponArmoireBlacksmithsHammerText": "Martillo del Herrero",
"weaponArmoireBlacksmithsHammerNotes": "Este martillo es para trabajar el metal, pero también es perfectamente apto para trabajar entre brasas al rojo vivo y Tareas Diarias de color rojo vivo, también. Aumenta <%= str %> de Fuerza. Armario Encantado: Conjunto Herrero (Artículo 3 de 3).",
"weaponSpecialSummer2025WarriorText": "Lanza Vieira",
"weaponSpecialSummer2025RogueText": "Tentáculo de Calamar",
"weaponSpecialSummer2025RogueNotes": "Este tentáculo se aferrará firmemente a tus metas para que no pierdas impulso al completar tareas. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Verano 2025.",
"weaponSpecialSummer2025WarriorNotes": "Es imposible saber su antigüedad, pero te acompañará en muchas tareas difíciles. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Verano 2025.",
"weaponSpecialSummer2025HealerText": "Remo de Ala de Ángel de Mar",
"weaponSpecialSummer2025MageText": "Coral Ramificado",
"weaponSpecialSummer2025MageNotes": "Expande las ramas de tus talentos y habilidades para abordar diversas tareas. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Verano 2025.",
"weaponMystery202508Notes": "¡Esta espada giratoria aterrorizará a cualquier monstruo o Tarea Diaria en rojo que se cruce en tu camino! No otorga ningún beneficio. Artículo para Suscriptores de Agosto 2025.",
"weaponMystery202508Text": "Espada Carmesí Brillante",
"weaponSpecialSpring2026HealerNotes": "¡Se acerca una oportunidad para empezar de nuevo con un fresco comienzo, y con este espléndido bastón, estarás listo! Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Primavera de 2026.",
"weaponMystery202601Notes": "Un escudo de burbujas heladas que otorga protección mágica contra elementos opuestos. No otorga ningún beneficio. Artículo para suscriptores de Enero 2026.",
"weaponSpecialWinter2026MageNotes": "Los candelabros son útiles porque pueden sostener más de una vela al mismo tiempo; sigue su ejemplo la próxima vez que necesites realizar varias tareas simultáneamente. Aumenta <%= int %> de Inteligencia y <%= per %> de Percepción. Equipamiento de Edición Limitada de Invierno 2025-2026.",
"weaponArmoireBeekeepersSmokerNotes": "Úsalo para calmar a tus abejas y tomar un poco de miel. A las abejas no les importa. Honestamente, a todos nos vendrían bien algunos minutos extra de calma de vez en cuando. Aumenta <%= int %> de Inteligencia. Armario Encantado: Conjunto Apicultor (Artículo 3 de 4).",
"weaponArmoireBambooFluteNotes": "¡Fiuuuuu! ¡Fiu-fiuuuu! Reúne a tu Grupo para una sesión de meditación o una siesta de auto-cuidado mientras se relajan al sonido de esta flauta de bambú. Aumenta <%= attrs %> de Complexión e Inteligencia. Armario Encantado: Conjunto 2 Instrumento Musical (Artículo 2 de 3)",
"weaponSpecialWinter2026WarriorText": "Guadaña Escarcha",
"weaponSpecialWinter2026RogueText": "Bastón de Esquí",
"weaponSpecialWinter2026HealerText": "Bastón Polar",
"weaponSpecialWinter2026RogueNotes": "Los bastones de esquí te ayudan a mantener el equilibrio, la estabilidad y la sincronización; todo lo que necesitas para ser verdaderamente productivo. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Invierno 2025-2026.",
"weaponSpecialWinter2026WarriorNotes": "Las guadañas ayudan a cortar, segar y cubrir grandes áreas; todo lo que necesitas a la hora de elaborar una lista de tareas. Aumenta <%= str %> de Fuerza. Equipamiento de Edición Limitada de Invierno 2025-2026.",
"weaponSpecialWinter2026HealerNotes": "Los bastones dan soporte, estabilidad y dirección; todo lo que ayuda a conquistar verdaderamente una lista de tareas. Aumenta <%= int %> de Inteligencia. Equipamiento de Edición Limitada de Invierno 2025-2026.",
"weaponSpecialWinter2026MageText": "Bastón Candelabro"
"armorArmoireHeraldsTunicText": "Túnica del heraldo"
}
+42 -89
View File
@@ -1,8 +1,8 @@
{
"tavern": "Chat de la Taberna",
"tavernChat": "Chat de la Taberna",
"innCheckOutBanner": "Actualmente la opción de recibir daño se encuentra pausada. Tus Tareas Diarias no te dañarán y no progresarás en las Misiones.",
"innCheckOutBannerShort": "Has pausado la opción de recibir daño.",
"innCheckOutBanner": "Actualmente estás registrado en la Taberna. Tus Tareas Diarias no te dañarán y no progresarás en las Misiones.",
"innCheckOutBannerShort": "Has entrado en la Taberna.",
"resumeDamage": "Reanudar Daño",
"helpfulLinks": "Enlaces Útiles",
"lookingForGroup": "Publicaciones para buscar un Equipo (Party Wanted)",
@@ -16,7 +16,7 @@
"wiki": "Wiki",
"resources": "Recursos",
"communityGuidelines": "Normas de la Comunidad",
"bannedWordUsed": "¡Ups! Parece que esta publicación contiene alguna palabra altisonante, referencia a sustancias adictivas o temas adultos (<%= swearWordsUsed %>). En Habitica mantenemos nuestro chat limpio. ¡Siéntete libre de editar tu mensaje para que puedas publicarlo! Deberás remover la palabra en cuestión, no sólo censurarla.",
"bannedWordUsed": "¡Ups! Parece que esta publicación contiene alguna palabrota, juramento religioso o referencia a una sustancia adictiva o tema adulto (<%= swearWordsUsed %>). Habitica tiene usuarios de todo tipo de origen, así que mantenemos nuestro chat muy limpio. ¡Puedes editar tu mensaje para poder publicarlo!",
"bannedSlurUsed": "Tu publicación contenía lenguaje inapropiado, y tus privilegios de chat han sido revocados.",
"party": "Equipo",
"usernameCopied": "Nombre de usuario copiado al portapapeles.",
@@ -76,9 +76,9 @@
"PMDisabledOptPopoverText": "Los Mensajes Privados están desactivados. Habilita esta opción para permitir que los usuarios se comuniquen contigo a través de tu perfil.",
"PMDisabledCaptionTitle": "Los Mensajes Privados están deshabilitados",
"PMDisabledCaptionText": "Aún puedes enviar mensajes, pero nadie puede enviártelos.",
"block": "Bloquear Jugador",
"unblock": "Desbloquear Jugador",
"blockWarning": "Esta acción no tendrá efecto si el jugador es administrador.",
"block": "Bloquear",
"unblock": "Desbloquear",
"blockWarning": "Bloquear - Esto no tendrá efecto si el jugador es un moderador ahora o se convierte en un moderador en el futuro.",
"inbox": "Bandeja de Entrada",
"messageRequired": "Un mensaje es requerido.",
"toUserIDRequired": "Un ID de Usuario es requerido",
@@ -89,10 +89,10 @@
"badAmountOfGemsToSend": "La cantidad de gemas debe de estar entre 1 y el número de gemas que tienes.",
"report": "Reportar",
"abuseFlagModalHeading": "Reportar una Infracción",
"abuseFlagModalBody": "Sólo debes denunciar las publicaciones que infrinjan las <%= firstLinkStart %>Normas de la Comunidad<%= linkEnd %> y/o <%= secondLinkStart %>los Terms of Service<%= linkEnd %>. Presentar una denuncia falsa es en sí misma una infracción a las Normas de la Comunidad de Habitica.",
"abuseFlagModalBody": "¿Estás seguro de querer reportar este mensaje? <strong>Sólo</strong> debes reportar un mensaje que infringe las <%= firstLinkStart %>Normas de la Comunidad<%= linkEnd %> y/o los <%= secondLinkStart %>Términos de Servicio<%= linkEnd %>. Reportar inapropiadamente un mensaje es una violación de las Normas de la Comunidad y podría generarte una infracción.",
"abuseReported": "Gracias por reportar esta violación. Los moderadores han sido notificados.",
"whyReportingPost": "¿Por qué denuncias esta publicación?",
"whyReportingPostPlaceholder": "Motivo del reporte",
"whyReportingPostPlaceholder": "Ayuda a nuestros moderadores contándonos por qué estás denunciando esta publicación como infracción; por ejemplo, spam, groserías, juramentos religiosos, intolerancia, insultos, temas para adultos, violencia.",
"optional": "Opcional",
"needsTextPlaceholder": "Escribe tu mensaje aqui.",
"leaderOnlyChallenges": "Sólo el líder del grupo puede crear desafíos",
@@ -111,10 +111,10 @@
"sendGiftCost": "Total: $<%= cost %> (USD)",
"sendGiftFromBalance": "De tu saldo",
"sendGiftPurchase": "Comprar",
"sendGiftMessagePlaceholder": "Añadir un mensaje al regalo",
"sendGiftMessagePlaceholder": "Mensaje personal (opcional)",
"sendGiftSubscription": "<%= months %> Mes(es): $<%= price %> USD",
"gemGiftsAreOptional": "Por favor ten en cuenta que Habitica nunca te pedirá que regales gemas a otros usuarios. La gente que pide gemas está <strong>violación a las Normas de la Comunidad</strong>, por lo que todos esos casos deben ser reportados a <%= hrefTechAssistanceEmail %>.",
"battleWithFriends": "Juega Habitica con Otros",
"battleWithFriends": "Combate a monstruos con tus amigos",
"startAParty": "Crea un Equipo",
"partyUpName": "Equipados",
"partyOnName": "Equipazo",
@@ -150,10 +150,10 @@
"onlyCreatorOrAdminCanDeleteChat": "¡No autorizado a eliminar este mensaje!",
"onlyGroupLeaderCanEditTasks": "¡No autorizado para administrar tareas!",
"onlyGroupTasksCanBeAssigned": "Solo las tareas grupales pueden ser asignadas",
"assignedTo": "Asignado a",
"assignedTo": "Asignar a",
"assignedToUser": "Asignado a <strong><%= userName %></strong>",
"assignedToMembers": "<%= userCount %> miembros",
"assignedToYouAndMembers": "<strong>Tú</strong>, <%= userCount %> miembros",
"assignedToMembers": "Asignado a <strong><%= userCount %> miembros</strong>",
"assignedToYouAndMembers": "Asignado a ti y a <strong><%= userCount %> miembros</strong>",
"youAreAssigned": "Asignado: <strong>tú</strong>",
"taskIsUnassigned": "Esta tarea está sin asignar",
"confirmUnClaim": "¿Estás seguro que no quieres reclamar esta tarea?",
@@ -206,43 +206,43 @@
"badAmountOfGemsToPurchase": "La cantidad debe ser de al menos 1.",
"groupPolicyCannotGetGems": "La política de un grupo al que perteneces impide que sus miembros obtengan gemas.",
"viewParty": "Ver Grupo",
"newGuildPlaceholder": "Ingresa el nombre de tu Grupo.",
"guildBank": "Banco",
"chatPlaceholder": "Escribe tu mensaje para los miembros del Grupo aquí",
"newGuildPlaceholder": "Ingresa el nombre de tu gremio.",
"guildBank": "Banco del Gremio",
"chatPlaceholder": "Escribe tu mensaje para los miembros del Gremio aquí",
"partyChatPlaceholder": "Escribe tu mensaje para los miembros del Grupo aquí",
"fetchRecentMessages": "Recuperar Mensajes Recientes",
"like": "Me gusta",
"liked": "Te gusta",
"inviteToGuild": "Invitar al Grupo",
"inviteToGuild": "Invitar al Gremio",
"inviteToParty": "Invitar al Equipo",
"inviteEmailUsername": "Invitar via Email o Nombre de Usuario",
"inviteEmailUsernameInfo": "Invita a usuarios usando email o nombre de usuario válidos. Si un email no está registrado aún, lo invitaremos a unirse.",
"emailOrUsernameInvite": "Dirección de Email o Nombre de Usuario",
"messageGuildLeader": "Mensaje para el Líder del Grupo",
"messageGuildLeader": "Mensaje para el Líder del Gremio",
"donateGems": "Donar Gemas",
"updateGuild": "Actualizar Grupo",
"updateGuild": "Actualizar Gremio",
"viewMembers": "Ver Miembros",
"memberCount": "Número de Miembros",
"recentActivity": "Actividad Reciente",
"myGuilds": "Mis Gremios",
"guildsDiscovery": "Descubrir Gremios",
"role": "Role",
"guildLeader": "Líder de Grupo",
"guildLeader": "Líder del Gremio",
"member": "Miembro",
"guildSize": "Tamaño del Grupo",
"guildSize": "Tamaño del Gremio",
"goldTier": "Nivel Oro",
"silverTier": "Nivel Plata",
"bronzeTier": "Nivel Bronce",
"privacySettings": "Ajustes de Privacidad",
"onlyLeaderCreatesChallenges": "Solo el Líder puede crear Desafíos",
"onlyLeaderCreatesChallengesDetail": "Con esta opción seleccionada, los miembros ordinarios del grupo no pueden crear Desafíos para el Grupo.",
"privateGuild": "Grupo Privado",
"onlyLeaderCreatesChallengesDetail": "Con esta opción seleccionada, los miembros ordinarios del grupo no pueden crear desafíos para el grupo.",
"privateGuild": "Gremio Privado",
"charactersRemaining": "<%= characters %> caracteres restantes",
"guildSummary": "Resumen",
"guildSummaryPlaceholder": "Escribe una breve descripción de tu Grupo. ¿Cuál es el propósito principal del Grupo y a qué se dedicarán sus miembros?",
"guildSummaryPlaceholder": "Escribe una corta descripción anunciando tu Gremio para los otros Habiticans. ¿Cuál es el propósito principal de tu Gremio y por qué debería unirse la gente? Intenta incluir palabras claves en el resumen ¡así los Habiticans podrán encontrarte fácilmente cuando estén buscando!",
"groupDescription": "Descripción",
"guildDescriptionPlaceholder": "Utiliza esta sección para entrar en detalles sobre todo lo que los miembros deberían saber sobre el Grupo. Consejos útiles, enlaces de ayuda, declaraciones alentadoras... ¡escríbelo aquí!",
"markdownFormattingHelp": "[Guía de formato Markdown](https://habitica.fandom.com/es/wiki/Gu%C3%ADa_de_Markdown)",
"guildDescriptionPlaceholder": "Utiliza esta sección para entrar en más detalles sobre todo lo que los miembros del Gremio deben saber sobre su Gremio. ¡Aquí encontrarás consejos útiles, enlaces útiles y declaraciones alentadoras!",
"markdownFormattingHelp": "[Ayuda del formato Markdown](https://habitica.fandom.com/es/wiki/Gu%C3%ADa_de_Markdown)",
"partyDescriptionPlaceholder": "Esta es la descripción de nuestro Equipo. Describe lo que hacemos en este Equoi. Si quieres saber más sobre lo que hacemos en este Equipo, lee la descripción. Equipo Activo.",
"guildGemCostInfo": "Un costo de gema promueve Gremios de alta calidad y se transfiere al banco de tu gremio.",
"noGuildsTitle": "No eres miembro de algún gremio.",
@@ -250,15 +250,15 @@
"noGuildsParagraph2": "Haz clic en la pestaña Descubrir para ver los Gremios recomendados según tus intereses, explora los Gremios públicos de Habitica o crea tu propio Gremio.",
"noGuildsMatchFilters": "No pudimos encontrar ningún gremio que coincida.",
"privateDescription": "Un gremio privado no se mostrará en el directorio de gremios de Habitica. Solo se pueden agregar nuevos miembros mediante invitación.",
"removeInvite": "Cancelar Invitación",
"removeInvite": "Remover invitación",
"removeMember": "Remover miembro",
"sendMessage": "Mandar mensaje",
"promoteToLeader": "Transferir Posesión",
"inviteFriendsParty": "Invita a otro jugador a tu Equipo<br/> y recibe el exclusivo Pergamino de Misión para luchar contra la Basi-Lista",
"inviteFriendsParty": "¡Invitar amigos a tu Equipo te dará un exclusivo <br/> Pergamino de Misión para luchar juntos contra la Basi-Lista!",
"createParty": "Crear un Equipo",
"inviteMembersNow": "¿Te gustaría invitar a miembros ahora?",
"playInPartyTitle": "¡Juega Habitica en Grupo!",
"playInPartyDescription": "Emprende increíbles Misiones con amigos o por tu cuenta. Combate monstruos, crea Desafíos y ayúdate a ser responsable con ayuda de los Equipos.",
"playInPartyDescription": "Emprende increíbles misiones con amigos o por tu cuenta. Combate monstruos, crea Desafíos y ayúdate a ser responsable con Equipos.",
"wantToJoinPartyTitle": "¿Quieres unirte a un Equipo?",
"wantToJoinPartyDescription": "¡Da tu nombre de usuario a un amigo que ya tiene un Equipo, o ve al gremio <a href='/groups/guild/f2db2a7f-13c5-454d-b3ee-ea1f5089e601'>Party Wanted Guild</a> para encontrar posibles compañeros!",
"copy": "Copiar",
@@ -271,7 +271,7 @@
"details": "Detalles",
"participantDesc": "Cuando todos los miembros hayan aceptado o rechazado, comenzará la Misión. Solo los que hicieron clic en \"aceptar\" podrán participar en la Misión y recibir los premios.",
"groupGems": "Gemas del Grupo",
"groupGemsDesc": "¡Las Gemas del Grupo se pueden gastar para crear Desafíos! En el futuro, podrás agregar más Gemas del Grupo.",
"groupGemsDesc": "¡Las Gemas del gremio se pueden gastar para crear Desafíos! En el futuro, podrás agregar más Gemas del Gremio.",
"groupTaskBoard": "Tablero de Tareas",
"groupInformation": "Información del Grupo",
"groupBilling": "Facturación del Grupo",
@@ -287,15 +287,15 @@
"worldBossBullet3": "Puedes continuar con Jefes de Misión normales, el daño aplicará a ambos",
"worldBossBullet4": "Revisa la Taberna regularmente para ver el progreso del Jefe Mundial y los Ataques de Ira",
"worldBoss": "Jefe Mundial",
"groupPlanTitle": "¿Necesitas más para tu Gremio?",
"groupPlanDesc": "¿Organizando las tareas del hogar o un pequeño proyecto de clase? Con los Planes Grupales de Habitica puedes acceder a tareas compartidas, además de una sala de chat especial para ayudarte a ti y a tu Grupo a mantenerse motivados.",
"groupPlanTitle": "¿Necesitas más para tu gente?",
"groupPlanDesc": "¿Manejando un grupo pequeño u organizando tareas del hogar? ¡Nuestros planes grupales te otorgan acceso exclusivo a un tablero de tareas privado y una sala de chat dedicada para ti y los miembros de tu grupo!",
"billedMonthly": "*pagado como suscripción mensual",
"teamBasedTasksList": "Lista de Tareas Compartidas",
"teamBasedTasksListDesc": "Todos los miembros del Grupo pueden trabajar desde el mismo tablero de tareas para garantizar que nadie se pierda de nada. Completa las tareas desde el tablero de Tareas Compartidas o cópialas a tus Tareas Personales para completarlas sobre la marcha.",
"groupManagementControls": "Responsabilidad Flexible",
"groupManagementControlsDesc": "Distribuye responsabilidades asignando tareas a uno o varios miembros, o marca las tareas como Desafíos para ver quién las completa primero. Los miembros del Grupo pueden estar al pendiente del progreso de otros al ver el estado de la tarea.",
"inGameBenefits": "¡Todos los beneficios!",
"inGameBenefitsDesc": "Los miembros del Grupo obtienen una Montura exclusiva de Jackalope, así como beneficios de suscripción completos, incluyendo Conjuntos de Equipamiento Especiales cada mes y la habilidad de comprar Gemas con Oro.",
"teamBasedTasksList": "Lista de Tareas del Grupo",
"teamBasedTasksListDesc": "Configura una lista compartida de tareas fácil de ver para el grupo. ¡Asigna tareas a tus compañeros de grupo, o deja que reclamen sus propias tareas para que sea claro en qué están trabajando todos!",
"groupManagementControls": "Controles de Gestión de Grupo",
"groupManagementControlsDesc": "Usa la aprobación de tareas para verificar que una tarea fue completada, agrega Administradores de Grupo para compartir responsabilidades y disfruta de un chat privado para todos los miembros del grupo.",
"inGameBenefits": "Beneficios dentro del Juego",
"inGameBenefitsDesc": "Los miembros del grupo obtienen una exclusiva Montura de Jackalope, así como beneficios de suscripción completos, incluyendo conjuntos especiales de equipamiento cada mes y la habilidad de comprar gemas con oro.",
"letsMakeAccount": "Primero, creemos una cuenta",
"nameYourGroup": "Siguiente, nombra tu grupo",
"exampleGroupName": "Por Ejemplo: Academia de los Vengadores",
@@ -331,7 +331,7 @@
"PMDisabled": "Desactivar Mensajes Privados",
"taskClaimed": "<%= userName %> ha reclamado la tarea <span class=\"notification-bold\"><%= taskText %></span>.",
"thisTaskApproved": "Esta tarea fue aprovada",
"chooseTeamMember": "Buscar miembro de equipo",
"chooseTeamMember": "Escoge un miembro del equipo",
"unassigned": "Sin asignar",
"claimRewards": "Reclamar Premios",
"managerNotes": "Notas de Administrador",
@@ -341,10 +341,10 @@
"onlyPrivateGuildsCanUpgrade": "Solo los gremios privados pueden actualizarse a un plan grupal.",
"assignedDateAndUser": "Asignado por <strong>@<%= username %></strong> en <strong><%= date %></strong>",
"assignedDateOnly": "Asignado en <strong><%= date %></strong>",
"bannedWordsAllowedDetail": "Con esta opción seleccionada, se permitirá el uso de palabras prohibidas en este Grupo.",
"bannedWordsAllowedDetail": "Con esta opción seleccionada, se permitirá el uso de palabras prohibidas en este Gremio.",
"bannedWordsAllowed": "Permitir palabras prohibidas",
"languageSettings": "Configuración de Lenguaje",
"newPartyPlaceholder": "Ingresa el nombre de tu Equipo.",
"newPartyPlaceholder": "Ingresa el nombre de tu Grupo.",
"cannotRemoveQuestOwner": "No puedes eliminar al dueño de la misión activa. Aborta la misión primero.",
"features": "Características",
"giftMessageTooLong": "La longitud máxima para mensajes de regalo es <%= maxGiftMessageLength %>.",
@@ -358,52 +358,5 @@
"joinParty": "Unirse al Equipo",
"editGuild": "Editar Gremio",
"editParty": "Editar Equipo",
"leaveGuild": "Abandonar Gremio",
"createGroup": "Crea un Grupo",
"newGroupsWelcome": "¡Bienvenido al Nuevo Tablón de Tareas Compartidas!",
"newGroupsWhatsNew": "Noticias:",
"challengeBannedWords": "Tu Desafío contiene una o más palabras altisonantes o referencias a temas adultos. Por favor edítalo para que puedas guardarlo. Deberás remover la palabra, no sólo censurarla.",
"challengeBannedSlurs": "Tu Desafío contiene una palabra ofensiva, lo que infringe las Normas de la Comunidad de Habitica, por tanto, tu acceso al chat y la creación de Desafíos ha sido revocado. Para más información, contacta al correo admin@habitica.com.",
"challengeBannedSlursPrivate": "Tu Desafío contiene una palabra ofensiva, lo cual infringe las Normas de la Comunidad de Habitica. Por favor remuévela para poder guardar tu Desafío.",
"lookForParty": "Buscar un Equipo",
"currentlyLookingForParty": "¡Estás en búsqueda de un Equipo!",
"groupUseDefault": "Elige una respuesta",
"groupParentChildren": "Para uso familiar",
"groupManager": "Para trabajar",
"readyToUpgrade": "¿Listo para subir de nivel?",
"messageCopiedToClipboard": "Mensaje copiado al portapapeles.",
"partyFinderDescription": "¿Quieres unirte a un Equipo, pero no conoces a otros jugadores? ¡Deja que los Líderes de Equipo sepan que estás buscando una invitación!",
"upgradeYourCrew": "¿Listo para mejorar tu Gremio?",
"createGroupToday": "¡Crea tu grupo hoy mismo!",
"createGroupTitle": "Crear Grupo",
"groupUse": "¿Qué describe mejor el propósito de tu Grupo?*",
"groupTeacher": "Para educar",
"descriptionOptionalText": "Añade una descripción",
"nextPaymentMethod": "Siguiente: Método de Pago",
"dayStart": "<strong>Comienza el día</strong>: <%= startTime %>",
"lastCompleted": "Último completado",
"youEmphasized": "<strong>Tú</strong>",
"chatTemporarilyUnavailable": "El chat no está disponible por ahora. Inténtalo de nuevo más tarde.",
"newGroupsBullet01": "Interactúa con tareas directamente desde el tablón de tareas compartidas",
"newGroupsBullet02": "Cualquiera puede completar una tarea sin asignar",
"newGroupsBullet03": "Las Tareas Compartidas se reinician a la misma hora para todos para fácilitar la colaboración",
"newGroupsBullet04": "Las actividades diarias compartidas no causarán daño si se omiten o aparecen en la ventana de \"Registrar Actividad del Día Anterior\"",
"newGroupsBullet05": "Las tareas compartidas se degradarán de color si se dejan incompletas para ayudar a manejar el progreso",
"newGroupsBullet06": "El estado de la tarea permite ver fácilmente qué persona asignada ha completado una tarea",
"newGroupsBullet07": "Activa la habilidad de mostrar las tareas compartidas en tu tablón personal",
"partyExceedsInvitesLimit": "Un Equipo sólo puede tener un máximo de <%= maxInvites %> invitaciones pendientes.",
"assignTo": "Asignar a",
"newGroupsBullet08": "El líder de grupo y los administradores pueden añadir tareas rápidamente desde la parte superior de las columnas de tareas",
"newGroupsBullet09": "Una tarea compartida puede ser desmarcada para indicar que aún necesita trabajarse en ella",
"sendGiftLabel": "¿Te gustaría añadir un mensaje al regalo?",
"groupFriends": "Para uso con amigos",
"nameStar": "Nombre*",
"nameStarText": "Añade un título",
"descriptionOptional": "Descripción",
"viewStatus": "Estado",
"questWithOthers": "Acepta Misiones con Otros",
"startPartyDetail": "¡Comienza tu propio Equipo o únete a uno existente <br/>para aceptar Misiones y potenciar tu motivación!",
"blockedUser": "<strong>Has bloqueado a este jugador.</strong>&nbsp;No podrán enviarte Mensajes Privados, pero aún podrás ver sus publicaciones.",
"bannedUser": "<strong>Este jugador ha sido suspendido.</strong>",
"sendTotal": "Total:"
"leaveGuild": "Abandonar Gremio"
}
@@ -1,6 +1,6 @@
{
"questEvilSantaText": "Santa Trampero",
"questEvilSantaNotes": "Escuchas rugidos agonizantes en las profundidades de los campos de hielo. Sigues los gruñidos, interrumpidos por carcajadas, hasta un claro en el bosque, donde ves una osa polar adulta. Está enjaulada y encadenada, luchando por su vida. Bailando encima de la jaula se encuentra un diablillo malicioso vestido como náufrago. ¡Vence al Cazador Santa y salva a la bestia!<br><br><strong>Nota</strong>: Vencer a “Cazador Santa” otorga un logro de misión acumulable, pero también una montura rara que sólo se puede añadir a tu establo una vez.",
"questEvilSantaNotes": "Escuchas rugidos lastimosos en las profundidades de los campos de hielo. Sigues los gruñidos - interrumpidos por carcajadas - hasta un claro en el bosque, donde ves una osa polar adulta. Está enjaulada y encadenada, luchando por su vida. Bailando encima de la jaula se encuentra un diablillo malicioso vestido como un náufrago. Vence a Santa Trampero ¡y salva a la bestia! <br><br><strong>Nota</strong>: “Santa Trampero ” premia una misión apilable además da una montura rara que solo puede ser agregada a tu establo una unica vez.",
"questEvilSantaCompletion": "Trapper Santa chilla de rabia y se aleja hacia la noche. La osita agradecida, entre rugidos y gruñidos, intenta decirte algo. La llevas de vuelta a los establos, donde Matt Boch, el Amo de las Bestias, escucha su historia con un grito de horror. Tiene un cachorro. Huyó a los campos de hielo cuando mamá osa fue capturada.",
"questEvilSantaBoss": "Santa Trampero",
"questEvilSantaDropBearCubPolarMount": "Oso Polar (Montura)",
+1 -3
View File
@@ -939,7 +939,5 @@
"backgrounds042026": "Ensemble 143 : Sortie Avril 2026",
"backgroundRidingACometText": "Chevaucher une Comète",
"backgrounds052026": "Ensemble 144 : Sortie Mai 2026",
"backgroundElvenCitadelText": "Citadelle Elfique",
"backgroundOnAStrangePlanetText": "Sur une Étrange Planète",
"backgroundOnAStrangePlanetNotes": "Aventurez vous là ou aucun·e habitant·e d'Habitica n'est allé·e auparavant : Sur une Étrange Planète."
"backgroundElvenCitadelText": "Citadelle Elfique"
}
+12 -19
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "Tyrannosaure squelettique",
"questTRexUndeadRageTitle": "Soin squelettique",
"questTRexUndeadRageDescription": "Cette barre se remplit lorsque vous ne complétez pas vos tâches quotidiennes. Lorsqu'elle est pleine, le tyrannosaure squelettique se soigne à hauteur de 30% de sa santé restante !",
"questTRexUndeadRageEffect": "Le Tyrannosaure Squelette utilise SOIN SQUELETTIQUE !\n\nLe monstre laisse échapper un rugissement surnaturel, et certains de ses os brisés se ressoudent !",
"questTRexUndeadRageEffect": "Le tyrannosaure squelette utilise SOIN SQUELETTIQUE !\n\nLe monstre laisse échapper un rugissement surnaturel, et certains de ses os brisés se ressoudent !",
"questTRexDropTRexEgg": "Tyrannosaure (œuf)",
"questTRexUnlockText": "Déverrouille l'achat d’œufs de tyrannosaure au marché",
"questRockText": "Échappez à la créature des cavernes",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "Nuée de crânes aquatiques",
"questDilatoryDistress2RageTitle": "Régénération de la nuée",
"questDilatoryDistress2RageDescription": "Régénération de la nuée : Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle est pleine, la nuée récupère 30% de sa santé restante !",
"questDilatoryDistress2RageEffect": "La Nuée utilise RÉGÉNÉRATION DE LA NUÉE !\n\nEncouragés par leurs victoires, plus de crânes affluent de la crevasse pour renforcer la nuée !",
"questDilatoryDistress2RageEffect": "'La nuée utilise RÉGÉNÉRATION DE LA NUÉE !'\n\nEncouragés par leurs victoires, plus de crânes affluent de la crevasse pour renforcer la nuée !",
"questDilatoryDistress2DropSkeletonPotion": "Potion d'éclosion squelette",
"questDilatoryDistress2DropCottonCandyBluePotion": "Potion d'éclosion bleu barbe-à-papa",
"questDilatoryDistress2DropHeadgear": "Diadème de corail de feu (couvre-chef)",
@@ -343,7 +343,7 @@
"questAxolotlUnlockText": "Déverrouille l'achat d’œufs d'axolotl au marché",
"questAxolotlRageTitle": "Regénération de l'axolotl",
"questAxolotlRageDescription": "Cette barre se remplit lorsque vous ne terminez pas vos tâches quotidiennes. Lorsqu'elle est pleine, l'axolotl magique se régénère à hauteur de 30% de sa santé restante !",
"questAxolotlRageEffect": "L'Axolotl Magique utilise la RÉGÉNÉRATION DE L'AXOLOTL !\n\nUn rideau de bulles colorées dissimule un instant le monstre ; lorsqu'il réapparaît, une partie de ses blessures est guérie !",
"questAxolotlRageEffect": "`L'axolotl magique utilise la RÉGÉNÉRATION DE L'AXOLOTL`\n\n `Un rideau de bulles colorées dissimule un instant le monstre. Lorsqu'il réapparaît, une partie de ses blessures a guéri !`",
"questTurtleText": "Guidez la tortue",
"questTurtleNotes": "À laide ! Cette tortue marine géante ne retrouve pas sa plage de nidification. Elle y retourne tous les ans pour pondre ses œufs, mais cette année la baie dInkomplet est remplie d’épaves de tâches toxiques à cause des quotidiennes rouges et des tâches encore à faire. \"Elle se débat de panique !\" dit @JessicaChase.<br><br>@UncommonCriminal confirme. \"Cest parce quelle est confuse, ses sens sont brouillés.\"<br><br> @Scarabsi vous attrape le bras. \"Pouvez-vous aider à dégager les épaves de tâches qui lui bloquent le chemin ? Ce sera sans doute dangereux, mais il faut quon la sauve !\"",
"questTurtleCompletion": "Grâce à vos vaillants efforts, les eaux ont été dégagées, permettant à notre chère tortue marine de retrouver la plage. Vous, @Bambin, et @JaizakAripaik la regardez pendant quelle enterre sa nichée dans le sable pour permettre à ses œufs dincuber et d’éclore en centaines de petites tortues. La sage tortue vous donne à chacun trois œufs, et demande à ce que vous les nourrissiez et les éleviez afin quils deviennent un jour, eux aussi, de grandes tortues marines.",
@@ -375,7 +375,7 @@
"questTaskwoodsTerror1Boss": "Nuée de crânes enflammés",
"questTaskwoodsTerror1RageTitle": "Régénération de la nuée",
"questTaskwoodsTerror1RageDescription": "Régénération de la nuée : cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle est pleine, la nuée récupère 30% de sa santé restante !",
"questTaskwoodsTerror1RageEffect": "La Nuée utilise RÉGÉNÉRATION DE LA NUÉE !\n\nEncouragés par cette victoire, d'autres crânes se joignent a la nuée dans une gerbe de flammes !",
"questTaskwoodsTerror1RageEffect": "`La nuée utilise RÉGÉNÉRATION DE LA NUÉE !`\n\nEncouragés par cette victoire, d'autres crânes se joignent a la nuée dans une gerbe de flammes !",
"questTaskwoodsTerror1DropSkeletonPotion": "Potion d'éclosion squelette",
"questTaskwoodsTerror1DropRedPotion": "Potion d'éclosion rouge",
"questTaskwoodsTerror1DropHeadgear": "Turban de pyromancienne (couvre-chef)",
@@ -437,7 +437,7 @@
"questStoikalmCalamity1Boss": "Nuée de crânes telluriques",
"questStoikalmCalamity1RageTitle": "Régénération de la nuée",
"questStoikalmCalamity1RageDescription": "Régénération de la nuée : Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle est pleine, la nuée récupère 30% de sa santé restante !",
"questStoikalmCalamity1RageEffect": "La Nuée utilise RÉSURRECTION DE LA NUÉE !\n\nDe nouveaux crânes sortent de terre, leurs dents claquant dans l'air froid !",
"questStoikalmCalamity1RageEffect": "`La nuée utilise RÉSURRECTION DE LA NUÉE !`\n\nDe nouveaux crânes sortent de terre, leurs dents claquant dans l'air froid !",
"questStoikalmCalamity1DropSkeletonPotion": "Potion d'éclosion squelette",
"questStoikalmCalamity1DropDesertPotion": "Potion d'éclosion du désert",
"questStoikalmCalamity1DropArmor": "Armure de chevaucheur de mammouths",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "Nuée de crânes aériens",
"questMayhemMistiflying1RageTitle": "Régénération de la nuée",
"questMayhemMistiflying1RageDescription": "Régénération de la nuée : cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle est pleine, la nuée récupère 30% de sa santé restante !",
"questMayhemMistiflying1RageEffect": "La Nuée utilise RÉGÉNÉRATION DE LA NUÉE !\n\nEncouragés par cette victoire, d'autres crânes apparaissent des nuages, et tournoient de plus belle !",
"questMayhemMistiflying1RageEffect": "`La nuée utilise RÉGÉNÉRATION DE LA NUÉE !`\n\nEncouragés par cette victoire, d'autres crânes apparaissent des nuages, et tournoient de plus belle !",
"questMayhemMistiflying1DropSkeletonPotion": "Potion d'éclosion squelette",
"questMayhemMistiflying1DropWhitePotion": "Potion d'éclosion des neiges",
"questMayhemMistiflying1DropArmor": "Tunique du malicieux messager arc-en-ciel (armure)",
@@ -535,7 +535,7 @@
"questLostMasterclasser3Boss": "Nuée de crânes vides",
"questLostMasterclasser3RageTitle": "Régénération de la nuée",
"questLostMasterclasser3RageDescription": "Régénération de la nuée : Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle est pleine, la nuée récupère 30% de sa santé restante !",
"questLostMasterclasser3RageEffect": "L'Essaim de Crânes Vides utilise RÉGÉNÉRATION DE LA NUÉE !\n\nEncouragés par leurs victoires, de nouveaux crânes tombent des cieux en criant, renforçant l'essaim !",
"questLostMasterclasser3RageEffect": "`L'essaim de crânes vides utilise RÉGÉNÉRATION DE LA NUÉE !`\n\nEncouragés par leurs victoires, de nouveaux crânes tombent des cieux en criant, renforçant l'essaim !",
"questLostMasterclasser3DropBodyAccessory": "Amulette d'éther (accessoire de torse)",
"questLostMasterclasser3DropBasePotion": "Potion d'éclosion de base",
"questLostMasterclasser3DropGoldenPotion": "Potion d'éclosion dorée",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "Anti'zinnya",
"questLostMasterclasser4RageTitle": "Absorption du vide",
"questLostMasterclasser4RageDescription": "Absorption du vide : Cette jauge se remplit lorsque vous ne complétez pas vos quotidiennes. Lorsqu'elle est pleine, Anti'zinnya fera disparaître le mana de l'équipe !",
"questLostMasterclasser4RageEffect": "Anti'zinnya utilise ABSORPTION DU VIDE ! À travers une inversion du sort de Poussée Éthérée, vous sentez votre magie drainée dans les ténèbres !",
"questLostMasterclasser4RageEffect": "`Anti'zinnya utilise ABSORPTION DU VIDE !` À travers une inversion du sort de poussée éthérée, vous sentez votre magie drainée dans les ténèbres !",
"questLostMasterclasser4DropBackAccessory": "Cape d'éther (accessoire dorsal)",
"questLostMasterclasser4DropWeapon": "Cristaux d'éther (arme à deux mains)",
"questLostMasterclasser4DropMount": "Monture d'éther invisible",
@@ -686,7 +686,7 @@
"questRubyText": "Rapport rubis",
"questWaffleUnlockText": "Déverrouille l'achat de potion d'éclosion de confiserie au marché",
"questWaffleDropDessertPotion": "Potion d'éclosion de confiserie",
"questWaffleRageEffect": "L'Affreuse Gaufre utilise BOURBIER D'ERABLE ! Un sirop de sève collante ralentit vos coups et vos sorts ! Les dégâts en cours sont diminués.",
"questWaffleRageEffect": "`L'affreuse gaufre utilise BOURBIER D'ERABLE !` Un sirop de sève collante ralentit vos coups et vos sorts ! Les dégâts en cours sont diminués.",
"questWaffleRageDescription": "Bourbier d'érable : Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Lorsqu'elle est pleine, l'affreuse gaufre diminuera d'autant les dégâts en cours accumulés par les membres de l'équipe !",
"questWaffleRageTitle": "Bourbier d'érable",
"questWaffleBoss": "Affreuse gaufre",
@@ -749,14 +749,14 @@
"questVirtualPetNotes": "C'est un matin de printemps calme et agréable à Habitica, une semaine après un mémorable premier avril. Vous et @Beffymaroo êtes à l'écurie en train de vous occuper de vos animaux de compagnie (qui sont encore un peu désorientés par le temps qu'ils ont passé virtuellement !)<br><br>Au loin, vous entendez un grondement et un bip, d'abord faible mais dont le volume augmente comme s'il se rapprochait. Une forme d'œuf apparaît à l'horizon et à mesure qu'elle s'approche, en émettant des bips de plus en plus forts, vous voyez qu'il s'agit d'un gigantesque animal de compagnie virtuel !<br><br>\"Oh non\", s'exclame @Beffymaroo, \"Je pense que le poisson d'avril n'a pas terminé son travail avec ce grand gaillard, il semble vouloir attirer l'attention !\".<br><br>L'animal virtuel émet un bip furieux, fait une crise de colère virtuelle et se rapproche de plus en plus.",
"questVirtualPetBoss": "Wotchimon",
"questVirtualPetRageTitle": "L'événe-biiip",
"questVirtualPetRageEffect": "Wotchimon utilise Bip Énervant ! Wotchimon fait un bip énervant, et sa barre de joie disparaît soudain ! Les dégâts en cours sont réduits.",
"questVirtualPetRageEffect": "`Wotchimon utilise Bip énervant !` Wotchimon fait un bip énervant, et sa barre de joie disparaît soudain ! Les dégâts en cours sont réduits.",
"questVirtualPetDropVirtualPetPotion": "Potion d'éclosion de familier virtuel",
"questVirtualPetUnlockText": "Déverrouille l'achat de potions d'éclosion de familier virtuel au marché",
"questVirtualPetRageDescription": "Cette barre se remplit quand vous n'effectuez pas vos quotidiennes. Quand elle sera pleine, le Wotchimon fera disparaître une partie des dégâts en cours de votre équipe !",
"questVirtualPetCompletion": "Quelques pressions sur des boutons semblent avoir satisfait les besoins mystérieux de l'animal virtuel, qui s'est finalement calmé et semble content.<br><br>Soudain, dans une explosion de confettis, le poisson d'avril apparaît avec un panier rempli d'étranges potions émettant de doux bips.<br><br>\"Quel timing, poisson d'avril\", dit @Beffymaroo avec un sourire en coin. \"Je soupçonne que ce grand gaillard qui émet des bips est une de vos connaissances.\"<br><br>\"Euh, oui,\" dit-il, penaud. \"Je suis vraiment désolé, et je vous remercie tous les deux d'avoir pris soin de Wotchimon ! Prenez ces potions en guise de remerciement, elles peuvent faire retransformer vos animaux de façon virtuelle quand vous le souhaitez !\".<br><br>Vous doutez d'être 100% d'accord avec tous ces bips, mais comme ils sont mignons, ça vaut le coup d'essayer !",
"questPinkMarbleBoss": "Corrompidon",
"questPinkMarbleRageDescription": "Cette barre se remplit quand vous n'effectuez pas vos tâches quotidiennes. Lorsqu'elle est pleine, Corrompidon réduira une partie des dommages reçues de la part de votre équipe !",
"questPinkMarbleRageEffect": "Corrompidon donne un Coup Rose-Mantique ! Ce n'était pas du tout affectueux ! Votre équipe et vous êtes pris·e·s par surprise. Les dégâts en attente sont réduits.",
"questPinkMarbleRageEffect": "`Corrompidon donne un coup rose-mantique !` Ce n'était pas du tout affectueux ! Vos compagnons et vous êtes pris par surprise. Les prochains dégâts seront réduits.",
"questPinkMarbleDropPinkMarblePotion": "Potion d'éclosion marbre rose",
"questPinkMarbleRageTitle": "Coup Rose-mantique",
"questPinkMarbleCompletion": "Vous réussissez finalement à épingler le petit gars il était bien plus costaud et rapide qu'on aurait pu croire. Avant qu'il ne recommence à remuer, vous lui confisquez son carquois de flèches brillantes. Il cligne des yeux et regarde soudainement autour de lui avec un air surpris. \"Je me suis piqué avec une de mes propres flèches pour échapper à ma tristesse et à mon chagrin d'amour… Je ne me souviens de rien après ça !\"<br><br>Alors qu'il s'apprête à fuir la cave, il remarque que @Loremi a pris un échantillon de poussière de marbre rose et se met à sourire. \"Essayez d'utiliser un peu de cette poussière rose dans une potion ! Nourrissez les familiers qu'elle fera éclore et vous découvrirez que les vraies relations naissent de la communication, de la confiance mutuelle et du soin. Je vous souhaite bonne chance, et je vous souhaite de l'amour !\"",
@@ -860,12 +860,5 @@
"questOpalNotes": "Les érudit·e·s d'Habitica recherchent depuis longtemps la légendaire Potion d'Éclosion Magique d'Opale. Une potion si puissante qu'elle confère aux familiers et aux montures une couleur ardente et une brillance incomparable à celles de toute autre pierre précieuse ou métal précieux. On raconte même que la magie des opales améliore la planification, la perspicacité et la créativité. Quel atout pour vos tâches !<br><br>Après de longues recherches, vous avez peut-être enfin trouvé la réponse. Les Potions d'Opale nécessitent la forge de pierres d'opale brutes avec les runes magiques de Balance et Mercure. Ces objets anciens ne se trouvent qu'à un seul endroit... les ruines périlleuses de la cité perdue, aux confins du Désert du Temps Perdu.<br><br>Vous arrivez aux ruines après avoir chevauché votre monture la plus puissante pendant des jours à travers un terrain reculé et accidenté. Parmi les pierres brisées et blanchies par le soleil, vous apercevez une lueur éclatante. La recherche commence !",
"questOpalCompletion": "Enfin, fatigué·e et poussiéreu·x·se, vous trouvez les dernières runes et l'opale nécessaires à la création de la Potion d'Éclosion Magique.<br><br>Vous commencez le processus de forge dès votre retour dans la ville principale d'Habitica. Le pouvoir des runes et des opales emplit votre laboratoire d'une lumière arc-en-ciel ! En un rien de temps, vous obtenez trois potions et vous avez hâte de faire éclore de nouveaux amis colorés.",
"questOpalCollectLibraRunes": "Rune de la Balance",
"questOpalCollectMercuryRunes": "Rune de Mercure",
"questAlienText": "Invasion des Voleurs de Motivation",
"questAlienRageTitle": "Incident Intergalactique",
"questAlienRageDescription": "Cette barre se remplit quand vous n'effectuez pas vos Quotidiennes. Si elle se remplit, l'Extraterrestre vous découragera en regagnant une partie de sa Vie !",
"questAlienRageEffect": "Le Voleur d'Encouragement utilise Incident Intergalactique ! Vous glissez dans l'hyperespace. Votre opposant récupère des PV !",
"questAlienBoss": "Voleur d'Encouragement, l'Extraterrestre",
"questAlienUnlockText": "Débloque la possibilité d'acquérir des Potions d'Éclosion Alien au Marché",
"questAlienDropAlienPotion": "Potion d'Éclosion Alien"
"questOpalCollectMercuryRunes": "Rune de Mercure"
}
+3 -10
View File
@@ -1,8 +1,8 @@
{
"rebirthNew": "Renaissance : Une nouvelle aventure disponible !",
"rebirthUnlock": "Vous avez débloqué la renaissance ! Cet article spécial du marché vous permet de commencer une nouvelle partie au niveau 1 tout en gardant vos tâches, vos familiers et bien plus. Utilisez-le pour vous procurer un vent de renouveau dans Habitica si vous sentez que vous en avez fait le tour, ou pour expérimenter de nouvelles fonctionnalités avec le regard nouveau d'un personnage débutant !",
"rebirthAchievement": "Vous avez utilisé l'Orbe de Renaissance <strong><%= number %></strong> fois, et votre plus haut niveau atteint est <strong><%= level %></strong>.",
"rebirthAchievement100": "Vous avez utilisé l'Orbe de Renaissance <strong><%= number %></strong> fois, et votre plus haut niveau atteint est <strong><%= level %>100</strong> ou plus.",
"rebirthAchievement": "Vous avez commencé une nouvelle aventure ! C'est votre renaissance <%= number %> et le niveau le plus élevé que vous avez atteint est <%= level %>. Pour cumuler ce succès, commencez votre nouvelle aventure une fois que vous aurez atteint un niveau encore plus élevé !",
"rebirthAchievement100": "Vous avez commencé une nouvelle aventure ! C'est votre renaissance <%= number %>, et le plus haut niveau que vous ayez atteint est 100 ou plus. Pour obtenir ce succès une fois de plus, commencez votre prochaine nouvelle aventure lorsque vous aurez atteint la prochaine centaine !",
"rebirthBegan": "A commencé une nouvelle aventure",
"rebirthText": "A débuté <%= rebirths %> nouvelles aventures",
"rebirthOrb": "A utilisé un orbe de renaissance pour recommencer à zéro après avoir atteint le niveau <%= level %>.",
@@ -11,12 +11,5 @@
"rebirthPop": "Recommencez instantanément avec un avatar guerrier de niveau 1, tout en conservant vos succès, votre butin et votre équipement. Les tâches et leur historique seront également conservés mais ramenés à la couleur jaune. Vos combos seront remis à zéro, sauf pour les tâches de provenant de défis ou de groupes. Votre or, votre expérience, votre mana et les effets de vos compétences seront supprimés. Tout ceci prendra effet immédiatement.",
"rebirthName": "Orbe de renaissance",
"rebirthComplete": "Vous avez été ressuscité !",
"nextFreeRebirth": "<strong><%= days %> jours</strong> avant un orbe de résurrection <strong>GRATUIT</strong>",
"rebirthUnlockedOrb": "Une nouvelle aventure s'offre à vous !",
"rebirthNewAchievement": "Nouveau Succès",
"rebirthNewAdventure": "Une nouvelle aventure commence !",
"rebirthAchievementPlural": "Vous avez utilisé l'Orbe de Renaissance <strong><%= number %></strong> fois, et votre plus haut niveau atteint est <strong><%= level %></strong>.",
"rebirthStackInfo": "Ce succès va s'accumuler à chaque fois que vous utilisez l'Orbe de Renaissance.",
"rebirthUnlockedDesc": "Utilisez l'Orbe de Renaissance pour instaurer un nouveau souffle dans votre aventure Habitica quand vous aurez la sensation d'avoir tout accompli ! Recommencez au niveau 1 sans perdre vos tâches, Succès, et Familiers avec cet objet spécial que vous trouverez au Marché.",
"rebirthUnlockedNewItem": "Orbe de Renaissance Débloquée"
"nextFreeRebirth": "<strong><%= days %> jours</strong> avant un orbe de résurrection <strong>GRATUIT</strong>"
}
+15 -18
View File
@@ -189,7 +189,7 @@
"questTRexUndeadBoss": "骸骨ティラノサウルス",
"questTRexUndeadRageTitle": "骨休め",
"questTRexUndeadRageDescription": "日課を終えないと、このバーがたまっていきます。いっぱいになると、骸骨ティラノサウルスは残っている体力の 30% を回復します!",
"questTRexUndeadRageEffect": "骸骨ティラノサウルスは、秘技『骨休め』を使いました!\n\nモンスターはとんでもない声でほえ、傷ついた骨の一部をくっつけました!",
"questTRexUndeadRageEffect": "`骸骨ティラノサウルスは、秘技『骨休め』を使た!`\n\nモンスターはとんでもない声でほえ、傷ついた骨の一部をくっつけました!",
"questTRexDropTRexEgg": "ティラノサウルス ( たまご )",
"questTRexUnlockText": "市場でティラノサウルスのたまごを買えるようになります",
"questRockText": "洞窟の化け物から逃げろ",
@@ -241,7 +241,7 @@
"questDilatoryDistress2Boss": "ウミドクロの群れ",
"questDilatoryDistress2RageTitle": "群れの復活",
"questDilatoryDistress2RageDescription": "群れの復活:日課を完了しないとこのバーが増えていきます。いっぱいになると、ウミドクロの群れは残りの体力を30%回復します!",
"questDilatoryDistress2RageEffect": "ウミドクロの群れは、『群れの復活』を使いました!\n\nウミドクロは勝利に活気づき、クレバスから仲間がやってきて、群れを強化しました!",
"questDilatoryDistress2RageEffect": "`ウミドクロの群れは、『群れの復活』を使いました!`\n\nウミドクロは勝利に活気づき、クレバスから仲間がやってきて、群れを強化しました!",
"questDilatoryDistress2DropSkeletonPotion": "骨のたまごがえしの薬",
"questDilatoryDistress2DropCottonCandyBluePotion": "わたあめブルーのたまごがえしの薬",
"questDilatoryDistress2DropHeadgear": "炎のサンゴのサークレット(頭装備)",
@@ -277,7 +277,7 @@
"questBurnoutBossRageSeasonalShop": "`モエツ鬼は、消耗の一撃を放った!`\n\nあーっ! 私たちの未了の日課が、モエツ鬼の炎のえさになってしまい、再度一撃を放つだけのエネルギーとなってしまいました! したたり落ちるスペクトルの炎が、季節の店をおそいました。恐る恐るのぞくと、陽気な季節の魔女が消耗した魂となって、ぶら下がっています。\n\nNPC たちを救わないと! Habitica の民よ、急いで自分のタスクをこなし、モエツ鬼が三度の攻撃を加えてくる前に退治しましょう!",
"questBurnoutBossRageTavern": "`モエツ鬼が、消耗の一撃を下した!`\n\nたくさんの Habitica の人びとは、キャンプ場にひそみ、モエツ鬼から隠れていますが、もう無理です! 天をも揺るがす吠え声とともに、モエツ鬼は熱さで白く光る手でキャンプ場を一かき。キャンプ場の支配人・Danielが、モエツ鬼の手に捕まり、目の前で消耗した魂へと姿を変えられてしまいました!\n\nこのせっかちがもたらす恐怖は、そう長くはつづきません。あきらめてはいけません…モエツ鬼にとどめを刺すまで、あとほんの少しです!",
"questFrogText": "散らかしカエルの沼",
"questFrogNotes": "友達と一緒にヨドミ沼をのろのろと歩いていると、@starsystemicが大きな看板を指さします。そこには、「進み続けなさい――できるなら」と書いてあります。<br><br>「そんな難しいことじゃないでしょ!」と、@RosemonkeyCTが言います。「道は広いし、見晴らしもいいもん!」<br><br>しかし、進み続けるうち、通り道が徐々に沼の汚れにふさがれ、奇妙な青いゴミやがらくたが散らかされていくことに気づきます。とうとう、先に進むことはできなくなってしまいました。<br><br>どうしてこんなに汚くなったのだろう、と周囲を見回していると、「気を付けて!」と、@Jon Arjinbornの叫び声が聞こえてきます。汚い洗濯物を身につけて、青い炎で照らされたカエルが、怒って泥沼から飛び出てきます。先に進むためには、この散らかし毒ガエルを打ち破らなければなりません!",
"questFrogNotes": "友達と一緒にヨドミ沼をのろのろと歩いていると、@starsystemicが大きな看板を指さします。そこには、「進み続けなさいできるなら」と書いてあります。<br><br>「そんな難しいことじゃないでしょ!」と、@RosemonkeyCTが言います。「道は広いし、見晴らしもいいもん!」<br><br>しかし、進み続けるうち、通り道が徐々に沼の汚れにふさがれ、奇妙な青いゴミやがらくたが散らかされていくことに気づきます。とうとう、先に進むことはできなくなってしまいました。<br><br>どうしてこんなに汚くなったのだろう、と周囲を見回していると、「気を付けて!」と、@Jon Arjinbornの叫び声が聞こえてきます。汚い洗濯物を身につけて、青い炎で照らされたカエルが、怒って泥沼から飛び出てきます。先に進むためには、この散らかし毒ガエルを打ち破らなければなりません!",
"questFrogCompletion": "戦いを負けたカエルは縮こまって、泥の中に戻っていきます。カエルがにゅるりと去っていくと、青いスライムが消え、視界が明けます。<br><br>なんと、道の真ん中に 3 つのきれいなたまごが見えてきました!「透明な殻の中に小さなオタマジャクシが見れるよ!」と、@Breadstrings 。「これ、あなたが持って帰りな。」",
"questFrogBoss": "散らかしカエル",
"questFrogDropFrogEgg": "カエル(たまご)",
@@ -343,7 +343,7 @@
"questAxolotlUnlockText": "市場でウーパールーパーのたまごを買えるようになります",
"questAxolotlRageTitle": "アホロートル再生",
"questAxolotlRageDescription": "日課を完了しないとこのバーが増えていきます。いっぱいになると魔のウーパールーパーは体力を30%回復してしまいます!",
"questAxolotlRageEffect": "魔のウーパールーパーが、『アホロートル再生』の呪文を使いました!\n\n`色とりどりの泡がモンスターの姿をさえぎったかと思うと、それが晴れるとモンスターの傷の一部が消えています!`",
"questAxolotlRageEffect": "`魔のウーパールーパーが、『アホロートル再生』の呪文を使た!`\n\n`色とりどりの泡がモンスターの姿をさえぎったかと思うと、それが晴れるとモンスターの傷の一部が消えています!`",
"questTurtleText": "カメの案内",
"questTurtleNotes": "助けてください! この巨大ウミガメは、海岸の巣への戻り方がわからなくなってしまいました。この母ウミガメは、毎年そこに戻ってきてたまごを生んでいましたが、今年、ミスミ湾は赤くなった日課や未達成のTo Doでできた毒性のタスクがガラクタとなって埋まっています。@JessicaChaseがいうには「ウミガメがパニックを起こして暴れている!」 とのこと。<br><br>@UncommonCriminalはうなずいて、「あのウミガメの帰巣本能がぼやけてしまって混乱してるんだ」。<br><br>@Scarabsiは、あなたの腕をつかんで「彼女の行く手を邪魔しているタスクのガラクタをかたづけるのを手伝ってくれませんか? このままでは危険です。あのウミガメを救わないと!」",
"questTurtleCompletion": "あなたの勇敢な働きにより、水はきれいになり、ウミガメは海岸への戻り方を思い出しました。あなたと @Bambin、そして @JaizakAripaik はウミガメが、浜の砂深くにたまごを生むのを見つめました。あのたまごはやがてかえり、数百のウミガメの子となることでしょう。母ウミガメは、あなた方各々に 3 個ずつのたまごを託し、いつか大きなウミガメとなる日まで育ててくれるよう、頭を下げました。",
@@ -375,7 +375,7 @@
"questTaskwoodsTerror1Boss": "炎ドクロの群れ",
"questTaskwoodsTerror1RageTitle": "群れの復活",
"questTaskwoodsTerror1RageDescription": "群れの復活:日課を完了しないとこのバーが増えていきます。いっぱいになると、炎ドクロの群れは体力を30%回復してしまいます!",
"questTaskwoodsTerror1RageEffect": "炎ドクロの群れは、『群れの復活』を使いました!\n\nヤツらは勝利に勢いづき、熱風とともにもっとたくさんのドクロがあなたのまわりに渦巻いています!",
"questTaskwoodsTerror1RageEffect": "`炎ドクロの群れは、『群れの復活』を使いました!`\n\nヤツらは勝利に勢いづき、熱風とともにもっとたくさんのドクロがあなたのまわりに渦巻いています!",
"questTaskwoodsTerror1DropSkeletonPotion": "骨のたまごがえしの薬",
"questTaskwoodsTerror1DropRedPotion": "赤のたまごがえしの薬",
"questTaskwoodsTerror1DropHeadgear": "火占い師のターバン ( 帽子・ヘルメット )",
@@ -432,12 +432,12 @@
"questTriceratopsUnlockText": "市場でトリケラトプスのたまごを買えるようになります",
"questGroupStoikalmCalamity": "オダヤカニの厄災",
"questStoikalmCalamity1Text": "オダヤカニの厄災・第1部:地底からの敵",
"questStoikalmCalamity1Notes": "@Kiwibotから短い手紙が届き、霜に覆われたその巻物は触れた指先と同じくらい心臓を凍えさせました。「オダヤカニ草原に来てみたら――地面から爆発する怪物が――救援を!!」あなたはパーティーを結成して北へ向かいます。しかし危険な山岳地帯を抜けてから間もなく、足元の雪が爆発し、ぞっとするような笑いを浮かべたドクロに取り囲まれてしまいました!<br><br>その時、突如として槍が脇をすり抜け、あなたを雪の中から不意打ちせんとしているドクロを貫きました。砕けたドクロから無造作に槍を引き戻すのに合わせて長い三つ編みがなびき、精巧な鎧をまとった長身の女性がマンモスの背に乗って躍り込んできます。マンモス乗りたちのリーダー、レディ・グラシエイトの助けを借りて、今こそ戦うときです!",
"questStoikalmCalamity1Notes": "@Kiwibotから短い手紙が届き、霜に覆われたその巻物は触れた指先と同じくらい心臓を凍えさせました。「オダヤカニ草原に来てみたら -- 地面から爆発する怪物が -- 救援を!!」あなたはパーティーを結成して北へ向かいます。しかし危険な山岳地帯を抜けてから間もなく、足元の雪が爆発し、ぞっとするような笑いを浮かべたドクロに取り囲まれてしまいました!<br><br>その時、突如として槍が脇をすり抜け、あなたを雪の中から不意打ちせんとしているドクロを貫きました。砕けたドクロから無造作に槍を引き戻すのに合わせて長い三つ編みがなびき、精巧な鎧をまとった長身の女性がマンモスの背に乗って躍り込んできます。マンモス乗りたちのリーダー、レディ・グラシエイトの助けを借りて、今こそ戦うときです!",
"questStoikalmCalamity1Completion": "あなたがドクロの群れに最後の一撃を食らわせると、それらは魔法の煙の中に掻き消えてしまいました。「忌々しい連中はいなくなったようだ」レディ・グラシエイトは告げます。「しかしもっと厄介な問題が残っている。私についてこい」彼女はあなたに冷たい外気から守ってくれる外套を投げてよこします。そうしてあなたたちは彼女に従い、その場を後にしたのでした。",
"questStoikalmCalamity1Boss": "ツチドクロの群れ",
"questStoikalmCalamity1RageTitle": "群れの復活",
"questStoikalmCalamity1RageDescription": "群れの復活:日課を完了しないとこのバーが増えていきます。いっぱいになると、ツチドクロの群れは体力を30%回復してしまいます!",
"questStoikalmCalamity1RageEffect": "ツチドクロの群れは『群れの復活』を使いました!\n\n更なるドクロが地底から現れ、寒さに歯をガチガチ鳴らしています!",
"questStoikalmCalamity1RageEffect": "`ツチドクロの群れは『群れの復活』を使いました!`\n\n更なるドクロが地底から現れ、寒さに歯をガチガチ鳴らしています!",
"questStoikalmCalamity1DropSkeletonPotion": "骨のたまごがえしの薬",
"questStoikalmCalamity1DropDesertPotion": "砂漠のたまごがえしの薬",
"questStoikalmCalamity1DropArmor": "マンモス乗りの鎧",
@@ -478,7 +478,7 @@
"questMayhemMistiflying1Boss": "風ドクロの群れ",
"questMayhemMistiflying1RageTitle": "群れの復活",
"questMayhemMistiflying1RageDescription": "群れの復活:日課を完了しないとこのバーが増えていきます。いっぱいになると、風ドクロの群れは体力を30%回復してしまいます!",
"questMayhemMistiflying1RageEffect": "風ドクロの群れは『群れの復活』を使いました!\n\n勝利に勢いづき、さらにたくさんのドクロが雲の中から飛び出してきます!",
"questMayhemMistiflying1RageEffect": "`風ドクロの群れは『群れの復活』を使いました!`\n\n勝利に勢いづき、さらにたくさんのドクロが雲の中から飛び出してきます!",
"questMayhemMistiflying1DropSkeletonPotion": "骨のたまごがえしの薬",
"questMayhemMistiflying1DropWhitePotion": "白いたまごがえしの薬",
"questMayhemMistiflying1DropArmor": "ゆかいな虹色の配達人ローブ(鎧)",
@@ -535,7 +535,7 @@
"questLostMasterclasser3Boss": "虚無ドクロの群れ",
"questLostMasterclasser3RageTitle": "群れの復活",
"questLostMasterclasser3RageDescription": "群れの復活:日課を完了しないとこのバーが増えていきます。いっぱいになると、虚無ドクロの群れは体力を30%回復してしまいます!",
"questLostMasterclasser3RageEffect": "虚無ドクロの群れは『群れの復活』を使いました!\n\n勝利に勢いづき、空から更なるドクロが叫びながら急降下してきて、群れを強化しました!",
"questLostMasterclasser3RageEffect": "`虚無ドクロの群れは『群れの復活』を使いました!`\n\n勝利に勢いづき、空から更なるドクロが叫びながら急降下してきて、群れを強化しました!",
"questLostMasterclasser3DropBodyAccessory": "エーテルのアミュレット(胴のアクセサリー)",
"questLostMasterclasser3DropBasePotion": "普通のたまごがえしの薬",
"questLostMasterclasser3DropGoldenPotion": "金のたまごがえしの薬",
@@ -548,7 +548,7 @@
"questLostMasterclasser4Boss": "昏冥のジニア",
"questLostMasterclasser4RageTitle": "吸い上げる虚無",
"questLostMasterclasser4RageDescription": "吸い上げる虚無:日課を完了しないとこのバーが増えていきます。いっぱいになると、昏冥のジニアはパーティーのマナを奪ってしまいます!",
"questLostMasterclasser4RageEffect": "昏冥のジニアは吸い上げる虚無を使いました! エーテル波動呪文の作用がねじれて反転し、マナが闇に吸い取られていくのを感じます!",
"questLostMasterclasser4RageEffect": "`昏冥のジニアは吸い上げる虚無を使いました!`エーテル波動呪文の作用がねじれて反転し、マナが闇に吸い取られていくのを感じます!",
"questLostMasterclasser4DropBackAccessory": "エーテルの外套(背のアクセサリー)",
"questLostMasterclasser4DropWeapon": "エーテルクリスタル(両手武器)",
"questLostMasterclasser4DropMount": "不可視のエーテル獣の乗騎",
@@ -693,7 +693,7 @@
"questAmberNotes": "キャンプ場のロッジでくつろいでいるあなたと@beffymarooと@-Tyr-の3人のところに、@Vikteがドアを勢いよく開けて、タスクの森に隠されているという新しいタイプの魔法のたまごがえしの薬の噂を、興奮ぎみにしゃべり出しました。今日の日課は終わっているので、3人は@Vikteの調査を手伝うことに大賛成です。人生には小さな冒険が必要ですものね。<br><br>タスクの森を数時間歩くと、あなたはこの森の調査に参加したことを後悔し始めました。あなたがまさに家に帰ろうとしたその時、突然、驚くような鳴き声が聞こえ、琥珀色に輝くうろこのトカゲが巨大な木に巻きついた様が見えたのです。トカゲは、@Vikteをその爪でつかんでいます。@beffymarooは剣を構えます。<br><br>「待って!」@-Tyr-は叫びました。「琥珀トカゲだよ! 危険な生き物じゃない! あのままつかみ続けられたら死んじゃうけど!」",
"jungleBuddiesNotes": "サル、木人、ナマケモノのペットのたまごが手に入るクエストセット:「巨大マンドリルといたずらザル」「混乱の木」「ネムネムのねむケモノ」。",
"jungleBuddiesText": "「ジャングルの相棒」クエストセット",
"questWaffleRageEffect": "荒ぶるワッフルは「べとべとメープルシロップ」を使いました!  ネバネバでべとべとのシロップが攻撃や呪文を鈍らせます! 保留中のダメージが減ってしまいました。",
"questWaffleRageEffect": "`荒ぶるワッフルは「べとべとメープルシロップ」を使た!` ネバネバでべとべとのシロップが攻撃や呪文を鈍らせます! 保留中のダメージが減ってしまいました。",
"questWaffleRageDescription": "べとべとメープルシロップ:このバーはあなたが日課を完了しないと増えていきます。いっぱいになると、荒ぶるワッフルはパーティーメンバーがためてきた保留中のダメージを減らしてしまいます!",
"questWaffleRageTitle": "べとべとメープルシロップ",
"questWaffleBoss": "荒ぶるワッフル",
@@ -751,7 +751,7 @@
"questVirtualPetBoss": "ウォッチモン",
"questVirtualPetRageTitle": "ビービー音",
"questVirtualPetRageDescription": "このバーはあなたが日課を完了しないと増えていきます。いっぱいになると、ウォッチモンはパーティーの保留ダメージを減らします!",
"questVirtualPetRageEffect": "ウォッチモンは秘技『うるさいビービー音』を使いました! ウォッチモンはうるさいビービー音を鳴らし、ハピネスバーは突然消え去ってしまいました!保留中のダメージは減少しました。",
"questVirtualPetRageEffect": "`ウォッチモンは秘技『うるさいビービー音』を使た!`ウォッチモンはうるさいビービー音を鳴らし、ハピネスバーは突然消え去った!保留中のダメージは減少した。",
"questVirtualPetNotes": "Habiticaの静かで気持ちのよい春の朝です。忘れられないエイプリルフールの日から一週間が過ぎました。あなたと@Beffymarooは動物小屋でペットに餌やりをしていました。ペットたちはまだエイプリル・フールのいたずらでヴァーチャルペットになったことに少し混乱しているようです。<br><br>遠くからがやがやという音とビーッビーッというノイズが聞こえました。はじめは小さかったのですが、近づくにつれてボリュームが上がります。卵形の影が地平線上に姿をあらわしました。やがてそれは側に来ると爆音でビービー音を鳴らします。――巨大なバーチャルペットです!<br><br>「うわーお」@Beffymarooは叫びます。「エイプリル・フールはなにかこのでっかいやつの作業を終わらせなかったんじゃない?だから警告を発してるんだと思う!」<br><br>ヴァーチャルペットはビービー怒り、ヴァーチャルかんしゃくをおこし、ますます近づいてきます。",
"questVirtualPetCompletion": "慎重にボタンを押すとヴァーチャルペットの謎の欲求を満たしたらしく、最終的にヴァーチャルペットは静かになり満足そうな表情を浮かべています。<br><br>突如として紙吹雪が舞い上がり、エイプリル・フールが姿を現しました。その手にはかごがあり、小さくピーピーと鳴る魔法のたまごがえしの薬でいっぱいです。<br><br>「良いタイミングね」@Beffymarooは皮肉げな笑みを浮かべます。「このビービーうるさいやつは、あなたのお知り合いだと思うのだけど」<br><br>「えぇ、はい……」フールは弱腰です。「それについては謝罪をいたしますよ。お二人にはウォッチモンの面倒を見ていただいたことをまことに感謝いたします!感謝の気持ちにこのたまごがえしの薬をお持ち下さい。いつでも好きなときにヴァーチャルペットに会えるようになりますよ!」<br><br>ピーピー音と共にする覚悟が100%あるわけではありませんが、ヴァーチャルペットがカワイイのは周知のことですから、このたまごがえしの薬を試してみる価値はありそうです!",
"questPinkMarbleBoss": "キューピット",
@@ -759,7 +759,7 @@
"questPinkMarbleCompletion": "ようやく彼を押さえ込むことに成功する。彼は予想以上にタフですばしこかった。彼が再び動き出す前に光り輝く矢筒を取り上げた。彼はまばたきをし、突然驚いてあたりを見回した。「しばらくの間、自分の悲しみと傷心から逃れるために、私は矢の一本で自分を刺した......その後のことは何も覚えていない!」。<br><br>彼は洞窟から逃げ出そうとしたとき、@Loremiが大理石の粉を採取したことに気づき、ニヤリと笑った。「このピンクの大理石の粉を薬に使ってみてください!そこから生まれたペットを育てれば、コミュニケーションから本当の人間関係、相互の信頼と配慮が生まれることに気づくでしょう。幸運と愛を祈ります!」",
"questPinkMarbleText": "堕落したキューピッドを鎮圧せよ",
"questPinkMarbleRageDescription": "このバーはあなたが日課を完了しないと増えていきます。いっぱいになると、キューピッドはパーティーの保留ダメージを減らします!",
"questPinkMarbleRageEffect": "キューピッドはピンクパンチを使いました! まったく愛情がこもっていません!パーティーの仲間は驚いています。保留中のダメージが減少しました。",
"questPinkMarbleRageEffect": "キューピッドはピンクパンチを使た!まったく愛情がこもっていない!パーティーの仲間は驚いてい。保留中のダメージが減少する。",
"questPinkMarbleDropPinkMarblePotion": "ピンク大理石のたまごがえしの薬",
"questPinkMarbleRageTitle": "ピンクパンチ",
"questPinkMarbleUnlockText": "市場で購入できる「ピンクマーブルのたまごがえしの薬」をアンロックします。",
@@ -776,7 +776,7 @@
"questCrabBoss": "いじわるカニ",
"questCrabRageTitle": "気まぐれないじわる",
"questGiraffeText": "キ・リン",
"questOpalUnlockText": "市場でオパールのたまごがえしの薬を買えるようになります",
"questOpalUnlockText": "オパールのたまごがえしの薬を市場で買えるようにな",
"questGiraffeNotes": "イッポイッポ草原の長い草の中を歩いて、タスクから休憩するために自然を感じ取っている途中です。小山を通り抜けると、遠くに物がたくさん置いてあることに気づきます。楽器、画材、電子機器などがたくさん集まっているようです。<br><br>ちょっと近づいてみると、「おい!何してるの?!」と、アカシアの木の後ろから叫び声が聞こえます。カッコいいサングラスをつけた、背の高くて堂々としたキリンが現れます。手にはギターを持っていて、長い首からカメラがぶら下がっています。「これは全部僕の物だよ!気を付けて、何も触らないで!」<br><br>多くの物にほこりがたまっていることに気づきます。「わぁ、趣味がたくさんあるんだね!好きな作品とか、見せてくれない?」と聞いてみます。<br><br>キリンは集めたものを全部見渡すと、顔をそむけてしまいます。「こんなにたくさんあるのに、どこから始めたらいいのか、わからないんだよ!やる気が出るように手伝って!」",
"questRaccoonBoss": "欲張りなアライグマ",
"questRaccoonDropRaccoonEgg": "アライグマ(たまご)",
@@ -860,8 +860,5 @@
"questJadeBoss": "ヒスイ・ジンクス",
"questDogNotes": "あなたは Habitica の地下の洞窟を調査し、地図を作成するための探検に選ばれました!ハビットシティの研究者たちは、ここにはタスクを管理するための新しいツールや、まだ発見されていない生物が存在するかもしれないと考えています。<br><br>ヨモヤマ山脈の麓の周りのトンネルを地図化していると、目の前の洞窟の入り口から光が漏れているのに気づきます。近づいてみると…おもちゃ?床には、ぬいぐるみやボールがたくさん散らばっています。今の、吠え声…?<br><br>突然、巨大な三つ首の犬が飛び出てきて、拾おうとしていたおもちゃに走ってきます!あなたは思わず凍りついてしまいます。しかし…犬の口はおもちゃで夢中で、攻撃する暇はなさそうです。<br><br>「ワン!」犬の一つの頭が吠え、ボロボロなアライグマのぬいぐるみを落とします。「掃除を手伝いに来てくれたの??片づけなきゃいけないんだけど、おもちゃを拾うたびに遊んじゃって、全然進まないのさ… ほらっ、急いで!!」<br><br>犬はボールをあなたに向かって、次々と投げてきます。頭の多い分、ドッジするのも大変です!",
"questJadeCompletion": "数え切れないほどの挫折を経て、あなたはなんとか翡翠の岩を山頂まで転がすことに成功しました!石の姿をした人物が追いつき、微笑みます。彼はそっと岩を押し、あなたは恐怖とともにそれが再び山のふもとまで転がり落ちるのを見ます。<br><br>「なぜそんなことを…?また最初からやらなきゃいけないじゃないか!」とあなたは思わず叫びます。<br><br>「同じことを何度もやらなければならないからといって、あなたの達成が無意味になるわけではない」と石の人物は言います。「今は、達成したことに目を向けて、報酬を楽しむといい!」<br><br>あなたはソファで目を覚まし、携帯電話は床に落ちています。その代わりに、流れる翡翠で満たされた三本の瓶が置かれていました!今日は皿を片付けたあと、このポーションがペットの卵にどんな効果をもたらすか試してみる時間かもしれません…",
"questOtterCompletion": "リストの項目を拾い上げると、それぞれのタスクの重要度ごとに整理し、かなり取り組みやすい状態にまとめることができました!<br><br>「なるほど!」とあなたはカワウソに言います。「あのふざけたやり方が、本当にどのタスクを優先すべきか考える助けになったんだね。」<br><br>カワウソは喜びながら水しぶきを上げ、ほっぺたをこすります。「僕のちょっとした作戦が、タスクを違った見方で考えるきっかけになってよかったよ。」水中に潜り、近くで再び顔を出すと、「リストは達成可能な範囲にしておくことを忘れずに。報酬も役立つから、これを受け取って!」",
"questAlienDropAlienPotion": "エイリアンのたまごがえしの薬",
"questAlienUnlockText": "市場でエイリアンのたまごがえしの薬を買えるようになります",
"questAlienBoss": "宇宙人「勇気の泥棒」"
"questOtterCompletion": "リストの項目を拾い上げると、それぞれのタスクの重要度ごとに整理し、かなり取り組みやすい状態にまとめることができました!<br><br>「なるほど!」とあなたはカワウソに言います。「あのふざけたやり方が、本当にどのタスクを優先すべきか考える助けになったんだね。」<br><br>カワウソは喜びながら水しぶきを上げ、ほっぺたをこすります。「僕のちょっとした作戦が、タスクを違った見方で考えるきっかけになってよかったよ。」水中に潜り、近くで再び顔を出すと、「リストは達成可能な範囲にしておくことを忘れずに。報酬も役立つから、これを受け取って!」"
}
+1 -4
View File
@@ -11,8 +11,5 @@
"rebirthPop": "Recomece seu personagem no Nível 1 enquanto mantém conquistas, colecionáveis e equipamentos. Suas tarefas, incluindo o histórico, continuarão, mas se tornarão amarelas. Seus combos das tarefas serão removidos, com exceção das tarefas pertencentes a Desafios ativos ou a Planos de Grupo. Seu Ouro, Experiência, Mana e efeitos de todas as Habilidades serão removidos. Tudo isso terá efeito imediatamente.",
"rebirthName": "Orbe do Renascimento",
"rebirthComplete": "Você renasceu!",
"nextFreeRebirth": "<strong><%= days %> dias</strong> até Orbe de Renascimento <strong>GRÁTIS</strong>",
"rebirthUnlockedOrb": "Nova aventura disponível!",
"rebirthNewAchievement": "Nova conquista",
"rebirthNewAdventure": "Uma nova aventura começa agora!"
"nextFreeRebirth": "<strong><%= days %> dias</strong> até Orbe de Renascimento <strong>GRÁTIS</strong>"
}
+1 -4
View File
@@ -13,8 +13,5 @@
"faqQuestion27": "Varför ändrar uppgifter färg?",
"faqQuestion28": "Kan jag pausa mina Dagliga uppgifter om jag behöver en paus?",
"faqQuestion29": "Hur återfår jag HP?",
"webFaqAnswer29": "Du kan få tillbaka HP genom att köpa en Hälsobrygd från dina Belöningar för 25 Guld. Utöver det kommer du alltid att återfå all din HP när du når nästa Nivå!",
"faqQuestion30": "Vad händer när mitt HP tar slut?",
"faqQuestion31": "Varför förlorade jag HP när jag interagerade med en uppgift utan ett negativt alternativ?",
"webFaqAnswer30": "Om du förlorat allt HP kommer du gå ner en nivå samt den nivåns egenskaptpoäng, allt ditt guld och en utrustning som kan återköpas. Du kan återförbättra genom att utföra uppgifter och gå upp i nivå."
"webFaqAnswer29": "Du kan få tillbaka HP genom att köpa en Hälsobrygd från dina Belöningar för 25 Guld. Utöver det kommer du alltid att återfå all din HP när du når nästa Nivå!"
}
+1 -3
View File
@@ -1851,7 +1851,5 @@
"armorArmoireSoftPinkSuitText": "Mjuk rosa kostym",
"headArmoirePinkFloppyHatNotes": "Många trollformler har sys in i denna enkla hatt, vilket ger den en perfekt rosa färg. Ökar Visdom med <%= int %>. Förtrollat skåp: Rosa vardagskläder uppsättningen (Föremål 1 av 3).",
"headArmoirePinkFloppyHatText": "Rosa diskett hatt",
"armorArmoireMedievalLaundryOutfitNotes": "Ta på dig arbetskläderna och rulla upp ärmarna: det är dags att göra klart tvätten! Ökar Tålighet med <%= con %>. Förtrollat skåp: Medeltida tvättare set (Föremål 1 av 6).",
"moreArmoireGearComing": "Det förtrollade klädskåpet återrfylls också varje månad!",
"moreArmoireGearAvailable": "Tills dess finns det <%= armorieCount %> utrustning att finna i det förtrollade klädskåpet!"
"armorArmoireMedievalLaundryOutfitNotes": "Ta på dig arbetskläderna och rulla upp ärmarna: det är dags att göra klart tvätten! Ökar Tålighet med <%= con %>. Förtrollat skåp: Medeltida tvättare set (Föremål 1 av 6)."
}
+1 -2
View File
@@ -200,6 +200,5 @@
"options": "Alternativ",
"finish": "Klar",
"congratulations": "Grattis!",
"onboardingAchievs": "Introducerade Prestationer",
"titleCustomizations": "Anpassningar"
"onboardingAchievs": "Introducerade Prestationer"
}
+2 -3
View File
@@ -4,7 +4,7 @@
"questEvilSantaCompletion": "Pälsjägartomten tjuter av ilska och studsar iväg i natten. En tacksam hon-björn försöker genom att ryta och morra tala om något för dig. Du tar henne till stallet, där Djurviskaren Matt Boch lyssnar till hennes berättelse och flämtar till av fasa. Hon har en unge! Han flydde till isfälten när mamma björn fångades.",
"questEvilSantaBoss": "Pälsjägartomten",
"questEvilSantaDropBearCubPolarMount": "Isbjörn (Riddjur)",
"questEvilSanta2Text": "Hitta Ungen",
"questEvilSanta2Text": "Hitta Björnungen",
"questEvilSanta2Notes": "När pälsjägartomten fångade isbjörnsriddjuret sprang hennes unge iväg över isfälten. Du hör grenar brytas och snö krasa genom det kristallklara ljudet av skogen. Tassavtryck! Ni börjar båda springa efter spåren. Hitta alla spår och brutna kvistar, och hämta tillbaka ungen!<br><br><strong>Notera</strong>: “Björnungen” belönar dig med en upprepbar prestation men delar ut ett ovanligt riddjur som endast kan läggas till i ditt stall en gång.",
"questEvilSanta2Completion": "Du har hittat björnungen! Den kommer hålla dig sällskap för alltid.",
"questEvilSanta2CollectTracks": "Spår",
@@ -645,6 +645,5 @@
"sandySidekicksText": "Uppdragsbunten Sandiga hjälpredor",
"jungleBuddiesText": "Uppdragsbunten Djungelvänner",
"delightfulDinosText": "Uppdragsbunten Förtjusande dinosaurier",
"rockingReptilesText": "Uppdragsbunten Festande reptiler",
"evilSantaAddlNotes": "Observera att Tomtefångaren och Finn Ungen har uppdragsprestationer som kan stapla men ger ett sällsynt husdjur och riddjur som endast kan läggas till i ditt stall en gång."
"rockingReptilesText": "Uppdragsbunten Festande reptiler"
}
+1 -8
View File
@@ -11,12 +11,5 @@
"rebirthPop": "Starta om din karaktär omedelbart vid Nivå 1 och behåll dina prestationer, samlarföremål och utrustning. Dina uppgifter och deras historik kommer behållas men de kommer att bli återställda till gult. Dina följer kommer att bli borttagna förutom på utmanings-uppgifter. Allt ditt Guld, Erfarenhet, Mana och alla effekter från Färdigheter kommer att bli borttaget. Detta kommer att träda i kraft omedelbart.",
"rebirthName": "Återfödelsens Sfär",
"rebirthComplete": "Du har återfötts!",
"nextFreeRebirth": "<strong><%= days %> dagar</strong> till <strong>GRATIS</strong> Återfödelsens Sfär",
"rebirthUnlockedOrb": "Ett nytt äventyr är tillgängligt!",
"rebirthNewAchievement": "Ny prestation",
"rebirthNewAdventure": "Ett nytt äventyr börjar!",
"rebirthAchievementPlural": "Du har använt Återfödelsens Sfär <strong><%number%<>/strong> gånger och den högsta nivån du uppnåt är <strong<>%=level%>",
"rebirthStackInfo": "Denna prestation kommer öka varje gång du använder Återfödelsens Sfär.",
"rebirthUnlockedNewItem": "Återfödelsens Sfär är Upplåst",
"rebirthUnlockedDesc": "Använd Återfödelsens Sfär för att ge nytt liv åt dina äventyr i Habitica när du tycker att du uppnåt allt! Starta om på första nivånmedans du behåller dina uppgifter, insatser och djur med detta särskilda föremål som hittas på Marknaden."
"nextFreeRebirth": "<strong><%= days %> dagar</strong> till <strong>GRATIS</strong> Återfödelsens Sfär"
}
+2 -2
View File
@@ -7,8 +7,8 @@
"reachedGoldToGemCapQuantity": "Det begärda beloppet <%= quantity %> överskrider Guld=>Juvel konverterings-begränsningen <%= convCap %> för denna månaden. Begränsningen återställs inom de första tre dagarna för varje månad. Tack för att du abonnerar!",
"mysteryItem": "Exklusiva föremål varje månad",
"mysteryItemText": "Varje månad får du en unik kosmetiskt föremål för din karaktär! Dessutom, för varje tredje månad av konsekutiv prenumeration så kommer de Mysteriska Tidsresenärerna att ge dig tillgång till historiska (och framtida!) kosmetiska föremålen.",
"exclusiveJackalopePet": "Särskilt Husdjur",
"giftSubscription": "Vill du ge någon en prenumeration som gåva?",
"exclusiveJackalopePet": "Exklusivt husdjur",
"giftSubscription": "Vill du ge någon en prenumeration?",
"giftSubscriptionText4": "Tack för att du stödjer Habitica!",
"groupPlans": "Grupp Planer",
"nowSubscribed": "Du här nu en abonnent för Habitica!",
+1 -13
View File
@@ -3,10 +3,8 @@ import isFunction from 'lodash/isFunction';
import min from 'lodash/min';
import reduce from 'lodash/reduce';
import filter from 'lodash/filter';
import pick from 'lodash/pick';
import pickBy from 'lodash/pickBy';
import size from 'lodash/size';
import moment from 'moment';
import content from '../content/index';
import i18n from '../i18n';
import { daysSince } from '../cron';
@@ -28,7 +26,7 @@ function trueRandom () {
return Math.random();
}
export default function randomDrop (user, options, req = {}, analytics) {
export default function randomDrop (user, options, req = {}) {
let acceptableDrops;
let drop;
let dropMultiplier;
@@ -157,15 +155,5 @@ export default function randomDrop (user, options, req = {}, analytics) {
user._tmp.drop = drop;
user.items.lastDrop.date = Number(new Date());
user.items.lastDrop.count += 1;
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('dropped item', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: drop.key,
category: 'behavior',
headers: req.headers,
});
}
}
}
+1 -12
View File
@@ -1,5 +1,3 @@
import pick from 'lodash/pick';
export function hasCompletedOnboarding (user) {
return (
user.achievements.createdTask === true
@@ -16,18 +14,9 @@ export function onOnboardingComplete (user) {
}
// Add notification and awards (server)
export function checkOnboardingStatus (user, req, analytics) {
export function checkOnboardingStatus (user) {
if (hasCompletedOnboarding(user) && user.addNotification) {
user.addNotification('ONBOARDING_COMPLETE');
if (analytics) {
analytics.track('onboarding complete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
}
onOnboardingComplete(user);
}
}
@@ -1,7 +1,5 @@
/* eslint-disable max-classes-per-file */
import get from 'lodash/get';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import {
NotAuthorized,
@@ -15,12 +13,10 @@ export class AbstractBuyOperation {
/**
* @param {User} user - the User-Object
* @param {Request} req - the Request-Object
* @param {analytics} analytics
*/
constructor (user, req, analytics) {
constructor (user, req) {
this.user = user;
this.req = req || {};
this.analytics = analytics;
const quantity = get(req, 'quantity');
@@ -87,10 +83,6 @@ export class AbstractBuyOperation {
throw new NotImplementedError('executeChanges');
}
analyticsData () { // eslint-disable-line class-methods-use-this
throw new NotImplementedError('sendToAnalytics');
}
async purchase () {
if (!this.multiplePurchaseAllowed() && this.quantity > 1) {
throw new NotAuthorized(this.i18n('messageNotAbleToBuyInBulk'));
@@ -98,34 +90,10 @@ export class AbstractBuyOperation {
this.extractAndValidateParams(this.user, this.req);
const resultObj = await this.executeChanges(this.user, this.item, this.req, this.analytics);
if (this.analytics) {
this.sendToAnalytics(this.analyticsData());
}
const resultObj = await this.executeChanges(this.user, this.item, this.req);
return resultObj;
}
analyticsLabel () { // eslint-disable-line class-methods-use-this
return 'buy';
}
sendToAnalytics (additionalData = {}) {
// spread-operator produces an "unexpected token" error
const analyticsData = merge(additionalData, {
user: pick(this.user, ['preferences', 'registeredThrough']),
uuid: this.user._id,
category: 'behavior',
headers: this.req.headers,
});
if (this.multiplePurchaseAllowed()) {
analyticsData.quantityPurchased = this.quantity;
}
this.analytics.track(this.analyticsLabel(), analyticsData);
}
}
export class AbstractGoldItemOperation extends AbstractBuyOperation {
@@ -149,15 +117,6 @@ export class AbstractGoldItemOperation extends AbstractBuyOperation {
user.stats.gp -= itemValue * this.quantity;
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
currency: 'Gold',
goldCost: this.getItemValue(this.item),
};
}
}
export class AbstractGemItemOperation extends AbstractBuyOperation {
@@ -179,15 +138,6 @@ export class AbstractGemItemOperation extends AbstractBuyOperation {
await updateUserBalance(user, -(itemValue * this.quantity), 'spend', item.key, item.text());
}
analyticsData () {
return {
itemKey: this.getItemKey(this.item),
itemType: this.getItemType(this.item),
currency: 'Gems',
gemCost: this.getItemValue(this.item) * 4,
};
}
}
export class AbstractHourglassItemOperation extends AbstractBuyOperation {
@@ -202,11 +152,4 @@ export class AbstractHourglassItemOperation extends AbstractBuyOperation {
async subtractCurrency (user, item) { // eslint-disable-line class-methods-use-this
await updateUserHourglasses(user, -1, 'spend', item.key);
}
analyticsData () {
return {
itemKey: this.item.key,
currency: 'Hourglass',
};
}
}
+14 -15
View File
@@ -24,7 +24,6 @@ import { BuyHourglassMountOperation } from './buyMount';
export default async function buy (
user,
req = {},
analytics,
options = { quantity: 1, hourglass: false },
) {
const key = get(req, 'params.key');
@@ -42,35 +41,35 @@ export default async function buy (
switch (type) {
case 'armoire': {
const buyOp = new BuyArmoireOperation(user, req, analytics);
const buyOp = new BuyArmoireOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'backgrounds':
if (!hourglass) throw new BadRequest(errorMessage('useUnlockForCosmetics'));
buyRes = await hourglassPurchase(user, req, analytics);
buyRes = await hourglassPurchase(user, req);
break;
case 'mystery':
buyRes = await buyMysterySet(user, req, analytics);
buyRes = await buyMysterySet(user, req);
break;
case 'potion': {
const buyOp = new BuyHealthPotionOperation(user, req, analytics);
const buyOp = new BuyHealthPotionOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'gems': {
const buyOp = new BuyGemOperation(user, req, analytics);
const buyOp = new BuyGemOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'quests': {
if (hourglass) {
buyRes = await hourglassPurchase(user, req, analytics, quantity);
buyRes = await hourglassPurchase(user, req, quantity);
} else {
const buyOp = new BuyQuestWithGemOperation(user, req, analytics);
const buyOp = new BuyQuestWithGemOperation(user, req);
buyRes = await buyOp.purchase();
}
@@ -81,36 +80,36 @@ export default async function buy (
case 'food':
case 'gear':
case 'bundles':
buyRes = await purchaseOp(user, req, analytics);
buyRes = await purchaseOp(user, req);
break;
case 'mounts': {
const buyOp = new BuyHourglassMountOperation(user, req, analytics);
const buyOp = new BuyHourglassMountOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'pets':
if (key === 'Gryphatrice-Jubilant') {
const buyOp = new BuyPetWithGemOperation(user, req, analytics);
const buyOp = new BuyPetWithGemOperation(user, req);
buyRes = await buyOp.purchase();
} else {
buyRes = hourglassPurchase(user, req, analytics);
buyRes = hourglassPurchase(user, req);
}
break;
case 'quest': {
const buyOp = new BuyQuestWithGoldOperation(user, req, analytics);
const buyOp = new BuyQuestWithGoldOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
case 'special': {
const buyOp = new BuySpellOperation(user, req, analytics);
const buyOp = new BuySpellOperation(user, req);
buyRes = await buyOp.purchase();
break;
}
default: {
const buyOp = new BuyMarketGearOperation(user, req, analytics);
const buyOp = new BuyMarketGearOperation(user, req);
buyRes = await buyOp.purchase();
break;
@@ -69,19 +69,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
];
}
_trackDropAnalytics (user, key) {
this.analytics.track(
'Enchanted Armoire',
{
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
category: 'behavior',
headers: this.req.headers,
},
);
}
_gearResult (user, eligibleEquipment) {
const emptied = eligibleEquipment.length === 1;
eligibleEquipment.sort();
@@ -105,10 +92,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
removeItemByPath(user, `gear.flat.${drop.key}`);
if (this.analytics) {
this._trackDropAnalytics(user, drop.key);
}
const armoireResp = {
type: 'gear',
dropKey: drop.key,
@@ -134,9 +117,6 @@ export class BuyArmoireOperation extends AbstractGoldItemOperation { // eslint-d
user.items.food[drop.key] += 1;
if (user.markModified) user.markModified('items.food');
if (this.analytics) {
this._trackDropAnalytics(user, drop.key);
}
return {
message: this.i18n('armoireFood', {
image: `<span class="Pet_Food_${drop.key} pull-left"></span>`,
-4
View File
@@ -70,8 +70,4 @@ export class BuyGemOperation extends AbstractGoldItemOperation { // eslint-disab
this.i18n('plusGem', { count: this.quantity }),
];
}
analyticsLabel () { // eslint-disable-line class-methods-use-this
return 'purchase gems';
}
}
@@ -60,7 +60,7 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
}
}
executeChanges (user, item, req, analytics) {
executeChanges (user, item, req) {
let message;
if (user.preferences.autoEquip) {
@@ -70,7 +70,7 @@ export class BuyMarketGearOperation extends AbstractGoldItemOperation { // eslin
if (!user.achievements.purchasedEquipment && user.addAchievement) {
user.addAchievement('purchasedEquipment');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
removePinnedGearAddPossibleNewOnes(user, `gear.flat.${item.key}`, item.key);
@@ -49,10 +49,4 @@ export class BuyHourglassMountOperation extends AbstractHourglassItemOperation {
message,
];
}
analyticsData () {
const data = super.analyticsData();
data.itemType = 'mounts';
return data;
}
}
+1 -14
View File
@@ -1,6 +1,5 @@
import get from 'lodash/get';
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import content from '../../content/index';
import {
@@ -13,7 +12,7 @@ import updateUserHourglasses from '../updateUserHourglasses';
import { removeItemByPath } from '../pinnedGearUtils';
import getItemInfo from '../../libs/getItemInfo';
export default async function buyMysterySet (user, req = {}, analytics) {
export default async function buyMysterySet (user, req = {}) {
const key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
@@ -35,18 +34,6 @@ export default async function buyMysterySet (user, req = {}, analytics) {
const itemInfo = getItemInfo(user, 'mystery_set', mysterySet);
removeItemByPath(user, itemInfo.path);
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: mysterySet.key,
itemType: 'Subscriber Gear',
currency: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}
// Here we need to trigger vue reactivity through reassign object
user.items.gear.owned = {
...user.items.gear.owned,
@@ -1,7 +1,6 @@
import get from 'lodash/get';
import includes from 'lodash/includes';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import i18n from '../../i18n';
import content from '../../content/index';
import {
@@ -13,7 +12,7 @@ import getItemInfo from '../../libs/getItemInfo';
import { removeItemByPath } from '../pinnedGearUtils';
import updateUserHourglasses from '../updateUserHourglasses';
export default async function purchaseHourglass (user, req = {}, analytics, quantity = 1) {
export default async function purchaseHourglass (user, req = {}, quantity = 1) {
const key = get(req, 'params.key');
if (!key) throw new BadRequest(errorMessage('missingKeyParam'));
@@ -94,18 +93,6 @@ export default async function purchaseHourglass (user, req = {}, analytics, quan
}
}
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
itemType: type,
currency: 'Hourglass',
category: 'behavior',
headers: req.headers,
});
}
return [
{ items: user.items, purchasedPlanConsecutive: user.purchased.plan.consecutive },
i18n.t('hourglassPurchase', req.language),
+1 -14
View File
@@ -76,7 +76,7 @@ async function purchaseItem (user, item, price, type, key) {
const acceptedTypes = ['eggs', 'hatchingPotions', 'food', 'gear', 'bundles'];
const singlePurchaseTypes = ['gear'];
export default async function purchase (user, req = {}, analytics) {
export default async function purchase (user, req = {}) {
const type = get(req.params, 'type');
const key = get(req.params, 'key');
@@ -130,19 +130,6 @@ export default async function purchase (user, req = {}, analytics) {
await purchaseItem(user, item, price, type, key);
}
/* eslint-enable no-await-in-loop */
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: key,
itemType: type,
currency: 'Gems',
gemCost: price * 4,
quantityPurchased: quantity,
category: 'behavior',
headers: req.headers,
});
}
return [
pick(user, splitWhitespace('items balance')),
+3 -15
View File
@@ -34,19 +34,18 @@ async function resetClass (user, req = {}) {
return balanceRemoved;
}
export default async function changeClass (user, req = {}, analytics) {
export default async function changeClass (user, req = {}) {
const klass = get(req, 'query.class');
let balanceRemoved = 0;
// user.flags.classSelected is set to false after the user paid the 3 gems
if (user.stats.lvl < 10) {
throw new NotAuthorized(i18n.t('lvl10ChangeClass', req.language));
} else if (!klass) {
// if no class is specified, reset points and set user.flags.classSelected to false.
// User will have paid 3 gems and will be prompted to select class.
balanceRemoved = await resetClass(user, req);
await resetClass(user, req);
} else if (klass === 'warrior' || klass === 'rogue' || klass === 'wizard' || klass === 'healer') {
if (user.flags.classSelected) {
balanceRemoved = await resetClass(user, req);
await resetClass(user, req);
}
user.stats.class = klass;
@@ -67,17 +66,6 @@ export default async function changeClass (user, req = {}, analytics) {
if (user.markModified) user.markModified('items.gear.owned');
removePinnedItemsByOwnedGear(user);
if (analytics) {
analytics.track('change class', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
class: klass,
currency: balanceRemoved === 0 ? 'Free' : 'Gems',
category: 'behavior',
headers: req.headers,
});
}
} else {
// if invalid class is specified, throw an error.
throw new BadRequest(i18n.t('invalidClass', req.language));
+2 -15
View File
@@ -2,9 +2,7 @@ import forEach from 'lodash/forEach';
import findIndex from 'lodash/findIndex';
import get from 'lodash/get';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import upperFirst from 'lodash/upperFirst';
import moment from 'moment';
import i18n from '../i18n';
import content from '../content/index';
import {
@@ -36,7 +34,7 @@ function evolve (user, pet, req) {
}, req.language);
}
export default function feed (user, req = {}, analytics) {
export default function feed (user, req = {}) {
let pet = get(req, 'params.pet');
const foodK = get(req, 'params.food');
let amount = Number(get(req.query, 'amount', 1));
@@ -116,7 +114,7 @@ export default function feed (user, req = {}, analytics) {
if (!user.achievements.fedPet && user.addAchievement) {
user.addAchievement('fedPet');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
}
@@ -141,17 +139,6 @@ export default function feed (user, req = {}, analytics) {
}
});
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('pet feed', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
foodKey: food.key,
petKey: pet.key,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.pets[pet.key],
message,
+2 -14
View File
@@ -2,9 +2,7 @@ import findIndex from 'lodash/findIndex';
import forEach from 'lodash/forEach';
import get from 'lodash/get';
import keys from 'lodash/keys';
import pick from 'lodash/pick';
import upperFirst from 'lodash/upperFirst';
import moment from 'moment';
import i18n from '../i18n';
import content from '../content/index';
import {
@@ -15,7 +13,7 @@ import {
import { errorMessage } from '../libs/errorMessage';
import { checkOnboardingStatus } from '../libs/onboarding';
export default function hatch (user, req = {}, analytics) {
export default function hatch (user, req = {}) {
const egg = get(req, 'params.egg');
const hatchingPotion = get(req, 'params.hatchingPotion');
@@ -57,7 +55,7 @@ export default function hatch (user, req = {}, analytics) {
if (!user.achievements.hatchedPet && user.addAchievement) {
user.addAchievement('hatchedPet');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
if (content.dropEggs[egg]) {
@@ -152,16 +150,6 @@ export default function hatch (user, req = {}, analytics) {
});
}
if (analytics && moment().diff(user.auth.timestamps.created, 'days') < 7) {
analytics.track('pet hatch', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
petKey: pet,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items,
i18n.t('messageHatched', req.language),
+1 -18
View File
@@ -1,5 +1,4 @@
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../i18n';
import { capByLevel } from '../statHelpers';
import {
@@ -13,31 +12,15 @@ import updateUserBalance from './updateUserBalance';
const USERSTATSLIST = ['per', 'int', 'con', 'str', 'points', 'gp', 'exp', 'mp'];
export default async function rebirth (user, tasks = [], req = {}, analytics) {
export default async function rebirth (user, tasks = [], req = {}) {
const notFree = !isFreeRebirth(user);
if (user.balance < 1.5 && notFree) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
const analyticsData = {
uuid: user._id,
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
};
if (notFree) {
await updateUserBalance(user, -1.5, 'rebirth');
analyticsData.currency = 'Gems';
analyticsData.gemCost = 6;
} else {
analyticsData.currency = 'Free';
analyticsData.gemCost = 0;
}
if (analytics) {
analyticsData.headers = req.headers;
analytics.track('Rebirth', analyticsData);
}
const lvl = capByLevel(user.stats.lvl);
+1 -13
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { mountMasterProgress } from '../count';
import i18n from '../i18n';
@@ -7,7 +6,7 @@ import {
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default async function releaseMounts (user, req = {}, analytics) {
export default async function releaseMounts (user, req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -42,17 +41,6 @@ export default async function releaseMounts (user, req = {}, analytics) {
user.achievements.mountMasterCount += 1;
}
if (analytics) {
analytics.track('release mounts', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.mounts,
i18n.t('mountsReleased'),
+1 -13
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import content from '../content/index';
import { beastMasterProgress } from '../count';
import i18n from '../i18n';
@@ -7,7 +6,7 @@ import {
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default function releasePets (user, req = {}, analytics) {
export default function releasePets (user, req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -42,17 +41,6 @@ export default function releasePets (user, req = {}, analytics) {
user.achievements.beastMasterCount += 1;
}
if (analytics) {
analytics.track('release pets', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items.pets,
i18n.t('petsReleased'),
+1 -13
View File
@@ -1,12 +1,11 @@
import each from 'lodash/each';
import pick from 'lodash/pick';
import i18n from '../i18n';
import {
NotAuthorized,
} from '../libs/errors';
import updateUserBalance from './updateUserBalance';
export default async function reroll (user, tasks = [], req = {}, analytics) {
export default async function reroll (user, tasks = [], req = {}) {
if (user.balance < 1) {
throw new NotAuthorized(i18n.t('notEnoughGems', req.language));
}
@@ -22,17 +21,6 @@ export default async function reroll (user, tasks = [], req = {}, analytics) {
}
});
if (analytics) {
analytics.track('Fortify Potion', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
currency: 'Gems',
gemCost: 4,
category: 'behavior',
headers: req.headers,
});
}
return [
{ user, tasks },
i18n.t('fortifyComplete'),
+1 -12
View File
@@ -1,5 +1,4 @@
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import reduce from 'lodash/reduce';
import each from 'lodash/each';
import i18n from '../i18n';
@@ -13,7 +12,7 @@ import predictableRandom from '../fns/predictableRandom';
import { removePinnedGearByClass, addPinnedGearByClass, addPinnedGear } from './pinnedGearUtils';
import getItemInfo from '../libs/getItemInfo';
export default function revive (user, req = {}, analytics) {
export default function revive (user, req = {}) {
if (user.stats.hp > 0) {
throw new NotAuthorized(i18n.t('cannotRevive', req.language));
}
@@ -110,16 +109,6 @@ export default function revive (user, req = {}, analytics) {
message = i18n.t('messageLostItem', { itemText: item.text(req.language) }, req.language);
}
if (analytics) {
analytics.track('Death', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
lostItem,
category: 'behavior',
headers: req.headers,
});
}
return [
user.items,
message,
+2 -2
View File
@@ -225,7 +225,7 @@ function _updateLastHistoryEntry (lastHistoryEntry, task, direction, times) {
}
}
export default function scoreTask (options = {}, req = {}, analytics) {
export default function scoreTask (options = {}, req = {}) {
const {
user, task, direction, times = 1, cron = false,
} = options;
@@ -425,7 +425,7 @@ export default function scoreTask (options = {}, req = {}, analytics) {
if (!user.achievements.completedTask && cron === false && direction === 'up' && user.addAchievement) {
user.addAchievement('completedTask');
checkOnboardingStatus(user, req, analytics);
checkOnboardingStatus(user, req);
}
return delta;
+1 -14
View File
@@ -1,17 +1,4 @@
import pick from 'lodash/pick';
export function sleep (user, req = {}, analytics) {
export function sleep (user) {
user.preferences.sleep = !user.preferences.sleep;
if (analytics) {
analytics.track('sleep', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
status: user.preferences.sleep,
category: 'behavior',
headers: req.headers,
});
}
return [user.preferences.sleep];
}
+1 -15
View File
@@ -1,5 +1,4 @@
import get from 'lodash/get';
import pick from 'lodash/pick';
import setWith from 'lodash/setWith';
import i18n from '../i18n';
import { NotAuthorized, BadRequest } from '../libs/errors';
@@ -208,7 +207,7 @@ function buildResponse ({ purchased, preference, items }, ownsAlready, language)
// If item is already purchased -> equip it
// Otherwise unlock it
// @TODO refactor and take as parameter the set name, for single items use the buy ops
export default async function unlock (user, req = {}, analytics) {
export default async function unlock (user, req = {}) {
const path = get(req.query, 'path');
if (!path) {
@@ -319,19 +318,6 @@ export default async function unlock (user, req = {}, analytics) {
if (!unlockedAlready) {
await updateUserBalance(user, -cost, 'spend', path);
if (analytics) {
analytics.track('buy', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
itemKey: path,
itemType: 'customization',
currency: 'Gems',
gemCost: cost / 0.25,
category: 'behavior',
headers: req.headers,
});
}
}
return buildResponse(user, unlockedAlready, req.language);
@@ -1,6 +1,5 @@
import validator from 'validator';
import moment from 'moment';
import pick from 'lodash/pick';
import sortBy from 'lodash/sortBy';
import nconf from 'nconf';
import {
@@ -127,14 +126,6 @@ api.loginLocal = {
user.auth.timestamps.updated = new Date();
await user.save();
res.analytics.track('login', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
type: 'local',
uuid: user._id,
headers: req.headers,
});
return loginRes(user, req, res);
},
};
@@ -1,7 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import escapeRegExp from 'lodash/escapeRegExp';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import reduce from 'lodash/reduce';
import times from 'lodash/times';
import { authWithHeaders, authWithSession } from '../../middlewares/auth';
@@ -290,19 +289,6 @@ api.createChallenge = {
};
response.group = getChallengeGroupResponse(group);
res.analytics.track('challenge create', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: response._id,
groupID: group._id,
groupName: group.privacy === 'private' ? null : group.name,
groupType: group._id === TAVERN_ID ? 'tavern' : group.type,
prize: response.prize,
headers: req.headers,
});
res.respond(201, response);
},
};
@@ -359,18 +345,6 @@ api.joinChallenge = {
const chalLeader = await User.findById(response.leader).select(nameFields).exec();
response.leader = chalLeader ? chalLeader.toJSON({ minimize: true }) : null;
res.analytics.track('challenge join', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
groupID: group._id,
groupName: group.privacy === 'private' ? null : group.name,
groupType: group._id === TAVERN_ID ? 'tavern' : group.type,
headers: req.headers,
});
res.respond(200, response);
},
};
@@ -410,18 +384,6 @@ api.leaveChallenge = {
// Unlink challenge's tasks from user's tasks and save the challenge
await challenge.unlinkTasks(user, keep);
res.analytics.track('challenge leave', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
groupID: challenge.group._id,
groupName: challenge.group.privacy === 'private' ? null : challenge.group.name,
groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type,
headers: req.headers,
});
res.respond(200, {});
},
};
@@ -895,19 +857,6 @@ api.deleteChallenge = {
// Close channel in background, some ops are run in the background without `await`ing
await challenge.closeChal({ broken: 'CHALLENGE_DELETED' });
res.analytics.track('challenge delete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
groupID: challenge.group._id,
groupName: challenge.group.privacy === 'private' ? null : challenge.group.name,
groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type,
prize: challenge.prize,
headers: req.headers,
});
res.respond(200, {});
},
};
@@ -956,20 +905,6 @@ api.selectChallengeWinner = {
// Close channel in background, some ops are run in the background without `await`ing
await challenge.closeChal({ broken: 'CHALLENGE_CLOSED', winner });
res.analytics.track('challenge close', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
challengeID: challenge._id,
challengeWinnerID: winner._id,
groupID: challenge.group._id,
groupName: challenge.group.privacy === 'private' ? null : challenge.group.name,
groupType: challenge.group._id === TAVERN_ID ? 'tavern' : challenge.group.type,
prize: challenge.prize,
headers: req.headers,
});
res.respond(200, {});
},
};
-32
View File
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import moment from 'moment';
import nconf from 'nconf';
import { authWithHeaders, chatPrivilegesRequired } from '../../middlewares/auth';
@@ -23,9 +22,6 @@ import { getMatchesByWordArray } from '../../libs/stringUtils';
import bannedSlurs from '../../libs/bannedSlurs';
import { apiError } from '../../libs/apiError';
import highlightMentions from '../../libs/highlightMentions';
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
const analytics = getAnalyticsServiceByEnvironment();
const ACCOUNT_MIN_CHAT_AGE = Number(nconf.get('ACCOUNT_MIN_CHAT_AGE'));
@@ -187,13 +183,6 @@ api.postChat = {
// Check if account is newer than the minimum age for chat participation
if (moment().diff(user.auth.timestamps.created, 'minutes') < ACCOUNT_MIN_CHAT_AGE) {
analytics.track('chat age error', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
});
throw new BadRequest(res.t('chatTemporarilyUnavailable'));
}
@@ -239,27 +228,6 @@ api.postChat = {
await Promise.all(toSave);
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
groupType: group.type,
privacy: group.privacy,
headers: req.headers,
};
if (mentions) {
analyticsObject.mentionsCount = mentions.length;
} else {
analyticsObject.mentionsCount = 0;
}
if (group.privacy === 'public') {
analyticsObject.groupName = group.name;
}
res.analytics.track('group chat', analyticsObject);
if (chatUpdated) {
res.respond(200, { chat: chatRes.chat });
} else {
@@ -5,7 +5,6 @@ import findIndex from 'lodash/findIndex';
import includes from 'lodash/includes';
import isArray from 'lodash/isArray';
import mergeWith from 'lodash/mergeWith';
import pick from 'lodash/pick';
import uniqBy from 'lodash/uniqBy';
import nconf from 'nconf';
import moment from 'moment';
@@ -166,25 +165,6 @@ api.createGroup = {
profile: { name: user.profile.name },
};
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupId: savedGroup._id,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
invited: false,
};
if (savedGroup.privacy === 'public') {
analyticsObject.groupName = savedGroup.name;
}
res.analytics.track('join group', analyticsObject);
res.respond(201, response); // do not remove chat flags data as we've just created the group
},
};
@@ -217,19 +197,6 @@ api.createGroupPlan = {
const results = await Promise.all([user.save(), group.save()]);
const savedGroup = results[1];
res.analytics.track('join group', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: true,
groupId: savedGroup._id,
groupType: savedGroup.type,
privacy: savedGroup.privacy,
headers: req.headers,
invited: false,
});
// do not remove chat flags data as we've just created the group
const groupResponse = savedGroup.toJSON();
// the leader is the authenticated user
@@ -585,7 +552,6 @@ api.joinGroup = {
if (!group) throw new NotFound(res.t('groupNotFound'));
let isUserInvited = false;
const seekingParty = Boolean(user.party.seeking);
if (group.type === 'party') {
// Check if was invited to party
@@ -710,20 +676,6 @@ api.joinGroup = {
promises.push(group.save());
const analyticsObject = {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
owner: false,
groupId: group._id,
groupType: group.type,
privacy: group.privacy,
headers: req.headers,
invited: isUserInvited,
seekingParty: group.type === 'party' ? seekingParty : null,
};
promises = await Promise.all(promises);
if (group.hasNotCancelled()) {
@@ -737,8 +689,6 @@ api.joinGroup = {
response.leader = leader.toJSON({ minimize: true });
}
res.analytics.track('join group', analyticsObject);
res.respond(200, response);
},
};
@@ -1,5 +1,4 @@
import escapeRegExp from 'lodash/escapeRegExp';
import pick from 'lodash/pick';
import { authWithHeaders } from '../../middlewares/auth';
import {
model as User,
@@ -734,17 +733,6 @@ api.transferGems = {
}
res.respond(200, {});
if (res.analytics) {
res.analytics.track('transfer gems', {
user: pick(sender, ['preferences', 'registeredThrough']),
uuid: sender._id,
hitType: 'event',
category: 'behavior',
headers: req.headers,
quantity: gemAmount,
});
}
},
};
@@ -1,9 +1,7 @@
import each from 'lodash/each';
import every from 'lodash/every';
import isBoolean from 'lodash/isBoolean';
import pick from 'lodash/pick';
import { authWithHeaders } from '../../middlewares/auth';
import { getAnalyticsServiceByEnvironment } from '../../libs/analyticsService';
import {
model as Group,
basicFields as basicGroupFields,
@@ -24,8 +22,6 @@ import { apiError } from '../../libs/apiError';
import { questActivityWebhook } from '../../libs/webhook';
import { model as UserHistory } from '../../models/userHistory';
const analytics = getAnalyticsServiceByEnvironment();
const questScrolls = common.content.quests;
function canStartQuestAutomatically (group) {
@@ -166,17 +162,6 @@ api.inviteToQuest = {
quest,
});
// track that the inviting user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
category: 'behavior',
headers: req.headers,
owner: true,
questName: questKey,
response: 'accept',
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'invite')
.commit();
@@ -231,17 +216,6 @@ api.acceptQuest = {
res.respond(200, savedGroup.quest);
// track that a user has accepted the quest
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: false,
response: 'accept',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'accept')
.commit();
@@ -297,16 +271,6 @@ api.rejectQuest = {
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: false,
response: 'reject',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withQuestInviteResponse(group.quest.key, 'reject')
.commit();
@@ -360,16 +324,6 @@ api.forceStart = {
]);
res.respond(200, savedGroup.quest);
analytics.track('quest', {
user: pick(user, ['preferences', 'registeredThrough']),
category: 'behavior',
owner: user._id === group.quest.leader,
response: 'force-start',
questName: group.quest.key,
uuid: user._id,
headers: req.headers,
});
},
};
@@ -1,7 +1,6 @@
import assign from 'lodash/assign';
import find from 'lodash/find';
import merge from 'lodash/merge';
import pick from 'lodash/pick';
import moment from 'moment';
import { authWithHeaders } from '../../middlewares/auth';
import {
@@ -330,17 +329,6 @@ api.createChallengeTasks = {
// If adding tasks to a challenge -> sync users
if (challenge) challenge.addTasks(tasks);
tasks.forEach(task => {
res.analytics.track('challenge task created', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
challengeID: challenge._id,
});
});
},
};
@@ -700,17 +688,6 @@ api.updateTask = {
task: savedTask,
});
}
if (group) {
res.analytics.track('task edit', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
});
}
},
};
@@ -1,4 +1,3 @@
import pick from 'lodash/pick';
import isUUID from 'validator/lib/isUUID';
import { authWithHeaders } from '../../../middlewares/auth';
import * as Tasks from '../../../models/task';
@@ -61,18 +60,6 @@ api.createGroupTasks = {
const tasks = await createTasks(req, res, { user, group });
res.respond(201, tasks.length === 1 ? tasks[0] : tasks);
tasks.forEach(task => {
res.analytics.track('team task created', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
headers: req.headers,
});
});
},
};
@@ -251,16 +238,6 @@ api.assignTask = {
await Promise.all(promises);
res.respond(200, task);
res.analytics.track('task assign', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
taskType: task.type,
groupID: group._id,
headers: req.headers,
});
},
};
+17 -33
View File
@@ -1,7 +1,6 @@
import cloneDeep from 'lodash/cloneDeep';
import forEach from 'lodash/forEach';
import isFunction from 'lodash/isFunction';
import pick from 'lodash/pick';
import nconf from 'nconf';
import get from 'lodash/get';
import { authWithHeaders } from '../../middlewares/auth';
@@ -27,7 +26,6 @@ import * as inboxLib from '../../libs/inbox';
import * as userLib from '../../libs/user';
import { model as UserHistory } from '../../models/userHistory';
const OFFICIAL_PLATFORMS = ['habitica-web', 'habitica-ios', 'habitica-android'];
const TECH_ASSISTANCE_EMAIL = nconf.get('EMAILS_TECH_ASSISTANCE_EMAIL');
const DELETE_CONFIRMATION = 'DELETE';
@@ -325,13 +323,6 @@ api.deleteUser = {
]);
}
res.analytics.track('account delete', {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
hitType: 'event',
category: 'behavior',
});
res.respond(200, {});
},
};
@@ -441,7 +432,7 @@ api.sleep = {
url: '/user/sleep',
async handler (req, res) {
const { user } = res.locals;
const sleepRes = common.ops.sleep(user, req, res.analytics);
const sleepRes = common.ops.sleep(user, req);
await user.save();
res.respond(200, ...sleepRes);
},
@@ -500,10 +491,7 @@ api.buy = {
let quantity = 1;
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
res.analytics = undefined;
}
const buyRes = await common.ops.buy(user, req, res.analytics);
const buyRes = await common.ops.buy(user, req);
await user.save();
@@ -558,7 +546,7 @@ api.buyGear = {
url: '/user/buy-gear/:key',
async handler (req, res) {
const { user } = res.locals;
const buyGearRes = await common.ops.buy(user, req, res.analytics);
const buyGearRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyGearRes);
},
@@ -600,10 +588,7 @@ api.buyArmoire = {
const { user } = res.locals;
req.type = 'armoire';
req.params.key = 'armoire';
if (OFFICIAL_PLATFORMS.indexOf(req.headers['x-client']) === -1) {
res.analytics = undefined;
}
const buyArmoireResponse = await common.ops.buy(user, req, res.analytics);
const buyArmoireResponse = await common.ops.buy(user, req);
await user.save();
await UserHistory.beginUserHistoryUpdate(user._id, req.headers)
.withArmoire(buyArmoireResponse[0].armoire.dropKey || 'experience')
@@ -646,7 +631,7 @@ api.buyHealthPotion = {
const { user } = res.locals;
req.type = 'potion';
req.params.key = 'potion';
const buyHealthPotionResponse = await common.ops.buy(user, req, res.analytics);
const buyHealthPotionResponse = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyHealthPotionResponse);
},
@@ -688,7 +673,7 @@ api.buyMysterySet = {
async handler (req, res) {
const { user } = res.locals;
req.type = 'mystery';
const buyMysterySetRes = await common.ops.buy(user, req, res.analytics);
const buyMysterySetRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyMysterySetRes);
},
@@ -731,7 +716,7 @@ api.buyQuest = {
async handler (req, res) {
const { user } = res.locals;
req.type = 'quest';
const buyQuestRes = await common.ops.buy(user, req, res.analytics);
const buyQuestRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...buyQuestRes);
},
@@ -818,7 +803,7 @@ api.hatch = {
url: '/user/hatch/:egg/:hatchingPotion',
async handler (req, res) {
const { user } = res.locals;
const hatchRes = common.ops.hatch(user, req, res.analytics);
const hatchRes = common.ops.hatch(user, req);
await user.save();
@@ -916,7 +901,7 @@ api.feed = {
url: '/user/feed/:pet/:food',
async handler (req, res) {
const { user } = res.locals;
const feedRes = common.ops.feed(user, req, res.analytics);
const feedRes = common.ops.feed(user, req);
await user.save();
@@ -964,7 +949,7 @@ api.changeClass = {
url: '/user/change-class',
async handler (req, res) {
const { user } = res.locals;
const changeClassRes = await common.ops.changeClass(user, req, res.analytics);
const changeClassRes = await common.ops.changeClass(user, req);
await user.save();
res.respond(200, ...changeClassRes);
},
@@ -1040,7 +1025,7 @@ api.purchase = {
if (req.body.quantity) quantity = req.body.quantity;
req.quantity = quantity;
const purchaseRes = await common.ops.buy(user, req, res.analytics);
const purchaseRes = await common.ops.buy(user, req);
await user.save();
res.respond(200, ...purchaseRes);
},
@@ -1083,7 +1068,6 @@ api.userPurchaseHourglass = {
const purchaseHourglassRes = await common.ops.buy(
user,
req,
res.analytics,
{ quantity, hourglass: true },
);
await user.save();
@@ -1180,7 +1164,7 @@ api.userOpenMysteryItem = {
url: '/user/open-mystery-item',
async handler (req, res) {
const { user } = res.locals;
const openMysteryItemRes = common.ops.openMysteryItem(user, req, res.analytics);
const openMysteryItemRes = common.ops.openMysteryItem(user, req);
await user.save();
res.respond(200, ...openMysteryItemRes);
},
@@ -1212,7 +1196,7 @@ api.userReleasePets = {
url: '/user/release-pets',
async handler (req, res) {
const { user } = res.locals;
const releasePetsRes = await common.ops.releasePets(user, req, res.analytics);
const releasePetsRes = await common.ops.releasePets(user, req);
await user.save();
res.respond(200, ...releasePetsRes);
},
@@ -1261,7 +1245,7 @@ api.userReleaseBoth = {
url: '/user/release-both',
async handler (req, res) {
const { user } = res.locals;
const releaseBothRes = common.ops.releaseBoth(user, req, res.analytics);
const releaseBothRes = common.ops.releaseBoth(user, req);
await user.save();
res.respond(200, ...releaseBothRes);
},
@@ -1297,7 +1281,7 @@ api.userReleaseMounts = {
url: '/user/release-mounts',
async handler (req, res) {
const { user } = res.locals;
const releaseMountsRes = await common.ops.releaseMounts(user, req, res.analytics);
const releaseMountsRes = await common.ops.releaseMounts(user, req);
await user.save();
res.respond(200, ...releaseMountsRes);
},
@@ -1373,7 +1357,7 @@ api.userUnlock = {
url: '/user/unlock',
async handler (req, res) {
const { user } = res.locals;
const unlockRes = await common.ops.unlock(user, req, res.analytics);
const unlockRes = await common.ops.unlock(user, req);
await user.save();
res.respond(200, ...unlockRes);
},
@@ -1399,7 +1383,7 @@ api.userRevive = {
url: '/user/revive',
async handler (req, res) {
const { user } = res.locals;
const reviveRes = common.ops.revive(user, req, res.analytics);
const reviveRes = common.ops.revive(user, req);
await user.save();
res.respond(200, ...reviveRes);
},
@@ -1,47 +0,0 @@
import pick from 'lodash/pick';
import {
NotAuthorized,
} from '../../libs/errors';
import {
authWithHeaders,
} from '../../middlewares/auth';
const api = {};
/**
* @apiIgnore Analytics are considered part of the private API
* @api {post} /analytics/track/:eventName Track a generic analytics event
* @apiName AnalyticsTrack
* @apiGroup Analytics
*
* @apiSuccess {Object} data An empty object
* */
api.trackEvent = {
method: 'POST',
url: '/analytics/track/:eventName',
// we authenticate these requests to make sure they actually came from a real user
middlewares: [authWithHeaders()],
async handler (req, res) {
// As of now only web can track events using this route
if (req.headers['x-client'] !== 'habitica-web') {
throw new NotAuthorized('Only habitica.com is allowed to track analytics events.');
}
const { user } = res.locals;
const eventProperties = req.body;
res.analytics.track(req.params.eventName, {
user: pick(user, ['preferences', 'registeredThrough']),
uuid: user._id,
headers: req.headers,
category: 'behavior',
...eventProperties,
});
// not using res.respond
// because we don't want to send back notifications and other user-related data
res.status(200).send({});
},
};
export default api;
-270
View File
@@ -1,270 +0,0 @@
/* eslint-disable camelcase */
import nconf from 'nconf';
import Amplitude from 'amplitude';
import useragent from 'useragent';
import {
omit,
toArray,
} from 'lodash';
import common from '../../common';
import logger from './logger';
const LOG_AMPLITUDE_EVENTS = nconf.get('LOG_AMPLITUDE_EVENTS') === 'true';
const AMPLITUDE_TOKEN = nconf.get('AMPLITUDE_KEY');
const AMPLITUDE_PROPERTIES_TO_SCRUB = [
'uuid', 'user', 'purchaseValue',
'headers', 'registeredThrough',
];
const PLATFORM_MAP = Object.freeze({
'habitica-web': 'Web',
'habitica-ios': 'iOS',
'habitica-android': 'Android',
});
let amplitude;
if (AMPLITUDE_TOKEN) amplitude = new Amplitude(AMPLITUDE_TOKEN);
const Content = common.content;
function _lookUpItemName (itemKey) {
if (!itemKey) return null;
const gear = Content.gear.flat[itemKey];
const egg = Content.eggs[itemKey];
const food = Content.food[itemKey];
const hatchingPotion = Content.hatchingPotions[itemKey];
const quest = Content.quests[itemKey];
const spell = Content.special[itemKey];
let itemName;
if (gear) {
itemName = gear.text();
} else if (egg) {
itemName = `${egg.text()} Egg`;
} else if (food) {
itemName = food.text();
} else if (hatchingPotion) {
itemName = `${hatchingPotion.text()} Hatching Potion`;
} else if (quest) {
itemName = quest.text();
} else if (spell) {
itemName = spell.text();
}
return itemName;
}
function _formatUserData (user) {
const properties = {};
if (user.stats) {
properties.Class = user.stats.class;
properties.Experience = Math.floor(user.stats.exp);
properties.Gold = Math.floor(user.stats.gp);
properties.Health = Math.ceil(user.stats.hp);
properties.Level = user.stats.lvl;
properties.Mana = Math.floor(user.stats.mp);
}
properties.balance = user.balance;
properties.balanceGemAmount = properties.balance * 4;
properties.tutorialComplete = user.flags && user.flags.tour && user.flags.tour.intro === -2;
properties.verifiedUsername = user.flags && user.flags.verifiedUsername;
if (properties.verifiedUsername && user.auth && user.auth.local) {
properties.username = user.auth.local.lowerCaseUsername;
}
if (user.habits && user.dailys && user.todos && user.rewards) {
properties['Number Of Tasks'] = {
habits: user.habits.length,
dailys: user.dailys.length,
todos: user.todos.length,
rewards: user.rewards.length,
};
}
if (user.contributor && user.contributor.level) {
properties.contributorLevel = user.contributor.level;
}
if (user.purchased && user.purchased.plan.planId) {
properties.subscription = user.purchased.plan.planId;
} else {
properties.subscription = null;
}
if (user._ABtests) {
properties.ABtests = toArray(user._ABtests);
}
if (user.loginIncentives) {
properties.loginIncentives = user.loginIncentives;
}
return properties;
}
function _formatPlatformForAmplitude (platform) {
if (!platform) {
return 'Unknown';
}
if (platform in PLATFORM_MAP) {
return PLATFORM_MAP[platform];
}
return '3rd Party';
}
function _formatUserAgentForAmplitude (platform, agentString) {
if (!agentString) {
return 'Unknown';
}
const agent = useragent.lookup(agentString).toJSON();
const formattedAgent = {};
if (platform === 'iOS' || platform === 'Android') {
formattedAgent.name = agent.os.family;
formattedAgent.version = `${agent.os.major}.${agent.os.minor}.${agent.os.patch}`;
if (platform === 'Android' && formattedAgent.name === 'Other') {
formattedAgent.name = 'Android';
}
} else {
formattedAgent.name = agent.family;
formattedAgent.version = agent.major;
}
return formattedAgent;
}
function _formatUUIDForAmplitude (uuid) {
return uuid || 'no-user-id-was-provided';
}
function _formatDataForAmplitude (data) {
const event_properties = omit(data, AMPLITUDE_PROPERTIES_TO_SCRUB);
const platform = _formatPlatformForAmplitude(data.headers && data.headers['x-client']);
const agent = _formatUserAgentForAmplitude(platform, data.headers && data.headers['user-agent']);
const ampData = {
user_id: _formatUUIDForAmplitude(data.uuid),
platform,
os_name: agent.name,
os_version: agent.version,
event_properties,
};
if (data.user) {
ampData.user_properties = _formatUserData(data.user);
}
const itemName = _lookUpItemName(data.itemKey);
if (itemName) {
ampData.event_properties.itemName = itemName;
}
return ampData;
}
function _sendDataToAmplitude (eventType, data, loggerOnly) {
const amplitudeData = _formatDataForAmplitude(data);
amplitudeData.event_type = eventType;
if (LOG_AMPLITUDE_EVENTS) {
logger.info('Amplitude Event', amplitudeData);
}
if (loggerOnly) return Promise.resolve(null);
return amplitude
.track(amplitudeData)
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
function _sendPurchaseDataToAmplitude (data) {
const amplitudeData = _formatDataForAmplitude(data);
// Stripe transactions come via webhook. We can log these as Web events
if (data.paymentMethod === 'Stripe' && amplitudeData.platform === 'Unknown') {
amplitudeData.platform = 'Web';
}
amplitudeData.event_type = 'purchase';
amplitudeData.revenue = data.purchaseValue;
amplitudeData.productId = data.itemPurchased;
if (LOG_AMPLITUDE_EVENTS) {
logger.info('Amplitude Purchase Event', amplitudeData);
}
return amplitude
.track(amplitudeData)
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
function _setOnce (dataToSetOnce, uuid) {
return amplitude
.identify({
user_id: _formatUUIDForAmplitude(uuid),
user_properties: {
$setOnce: dataToSetOnce,
},
})
.catch(err => logger.error(err, 'Error while sending data to Amplitude.'));
}
// There's no error handling directly here because it's handled inside _sendDataTo{Amplitude|Google}
async function track (eventType, data, loggerOnly = false) {
const { user } = data;
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
return null;
}
const promises = [
_sendDataToAmplitude(eventType, data, loggerOnly),
];
if (user.registeredThrough) {
promises.push(_setOnce({
registeredPlatform: user.registeredThrough,
}, data.uuid || user._id));
}
return Promise.all(promises);
}
// There's no error handling directly here because
// it's handled inside _sendPurchaseDataTo{Amplitude|Google}
async function trackPurchase (data) {
const { user } = data;
if (!user || !user.preferences || !user.preferences.analyticsConsent) {
return null;
}
return Promise.all([
_sendPurchaseDataToAmplitude(data),
]);
}
// Stub for non-prod environments
const mockAnalyticsService = {
track: () => { },
trackPurchase: () => { },
};
// Return the production or mock service based on the current environment
function getServiceByEnvironment () {
if (nconf.get('IS_PROD') || (nconf.get('DEBUG_ENABLED') && !nconf.get('BASE_URL').includes('localhost'))) {
return {
track,
trackPurchase,
};
}
return mockAnalyticsService;
}
export {
track,
trackPurchase,
mockAnalyticsService,
getServiceByEnvironment as getAnalyticsServiceByEnvironment,
};
+2 -11
View File
@@ -1,5 +1,4 @@
import moment from 'moment';
import pick from 'lodash/pick';
import {
BadRequest,
NotAuthorized,
@@ -19,6 +18,7 @@ import {
} from './social';
import { loginRes } from './utils';
import { verifyUsername } from '../user/validation';
import { trackRegistrationEvent } from '../localAnalytics';
const USERNAME_LENGTH_MIN = 1;
const USERNAME_LENGTH_MAX = 20;
@@ -180,6 +180,7 @@ async function registerLocal (req, res, { isV3 = false }) {
} else {
newUser = new User(newUser);
newUser.registeredThrough = req.headers['x-client']; // Not saved, used to create the correct tasks based on the device used
trackRegistrationEvent({ user: newUser, method: 'local', ipAddress: req.ip });
}
// we check for partyInvite for backward compatibility
@@ -217,16 +218,6 @@ async function registerLocal (req, res, { isV3 = false }) {
})
.catch(err => logger.error(err));
if (!existingUser) {
res.analytics.track('register', {
user: pick(savedUser, ['preferences', 'registeredThrough']),
category: 'acquisition',
type: 'local',
uuid: savedUser._id,
headers: req.headers,
});
}
return null;
}

Some files were not shown because too many files have changed in this diff Show More