From b7ddadc16d18484cfbbf9d6f90b5a9501c32ac2a Mon Sep 17 00:00:00 2001 From: PhotoNomad0 Date: Fri, 14 Jun 2024 05:53:59 -0400 Subject: [PATCH] merge search --- README.md | 6 +- package-lock.json | 468 +++- package.json | 3 +- scripts/latest/getLatestTcore.js | 194 -- scripts/latest/getLatestTcore.py | 181 -- src/__tests__/alignmentSearchHelpers.test.js | 407 +++ ...5Fgl_el-x-koine_testament_v39%2E1_501.json | 1 + src/js/actions/PopoverActions.js | 5 +- src/js/common/constants.js | 1 + src/js/components/Popover.js | 46 +- .../components/dialogComponents/BaseDialog.js | 6 + .../AlignmentSearchDialogContainer.js | 2046 +++++++++++++++ src/js/containers/AppMenu.js | 10 + .../UsfmFileConversionHelpers.js | 2 +- .../__tests__/alignmentSearchHelpers.test.js | 178 ++ src/js/helpers/alignmentSearchHelpers.js | 2245 +++++++++++++++++ src/js/reducers/popoverReducer.js | 3 + 17 files changed, 5409 insertions(+), 393 deletions(-) delete mode 100644 scripts/latest/getLatestTcore.js delete mode 100644 scripts/latest/getLatestTcore.py create mode 100644 src/__tests__/alignmentSearchHelpers.test.js create mode 100644 src/__tests__/fixtures/alignmentData/es-419_glt_Es-419%5Fgl_el-x-koine_testament_v39%2E1_501.json create mode 100644 src/js/containers/AlignmentSearchDialogContainer.js create mode 100644 src/js/helpers/__tests__/alignmentSearchHelpers.test.js create mode 100644 src/js/helpers/alignmentSearchHelpers.js diff --git a/README.md b/README.md index 725ea9eed0..5e2d669e64 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ You can view progress or help translate at [Crowdin](https://crowdin.com/project - first time do: `npm run load-apps` - launch app by: `npm i --legacy-peer-deps && npm run start-debug` - open chrome to url `chrome://inspect/#devices` -- if you do not see under remote target `electron/js2c/browser_init` and an `inspect` link, - make sure `Discover network targets` is selected and click `Configure` button. Make sure `localhost:5656` is added under `Target discovery settings` and click `Done`. +- if you do not see under remote target `electron/js2c/browser_init` add an `inspect` link, make sure `Discover network targets` is selected and click `Configure` button. Make sure `localhost:5656` is added under `Target discovery settings` and click `Done`. - Under remote target `electron/js2c/browser_init` click on `inspect` link. +- You will need to add the folder that contains the translationCore source files to the workspace. Then you can use control-P or command-P to search for and open source files and set breakpoints. + - the electronite app initialization code is in `electronite/index.js` + - the app UI startup code is in `pages/app.js` but cannot debug with this method, but will have to use debugger in the app and do a reload. diff --git a/package-lock.json b/package-lock.json index c51051c9d1..71a46f458b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "translationCore", - "version": "3.6.4", + "version": "3.6.4-search", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "translationCore", - "version": "3.6.4", + "version": "3.6.4-search", "license": "GPL-2.0", "dependencies": { "@craco/craco": "5.6.4", @@ -33,6 +33,7 @@ "json-stringify-safe": "5.0.1", "konami-code-js": "0.8.0", "lodash": "4.17.19", + "material-table": "1.54.2", "material-ui": "0.20.2", "memoize-one": "5.0.0", "mkdirp": "0.5.1", @@ -2056,6 +2057,22 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "node_modules/@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "node_modules/@date-io/date-fns": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz", + "integrity": "sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==", + "dependencies": { + "@date-io/core": "^1.3.13" + }, + "peerDependencies": { + "date-fns": "^2.0.0" + } + }, "node_modules/@electron/asar": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.3.tgz", @@ -2903,6 +2920,27 @@ "node": ">=6.9.0" } }, + "node_modules/@material-ui/pickers": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.11.tgz", + "integrity": "sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw==", + "deprecated": "This package no longer supported. It has been relaced by @mui/x-date-pickers", + "dependencies": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + }, + "peerDependencies": { + "@date-io/core": "^1.3.6", + "@material-ui/core": "^4.0.0", + "prop-types": "^15.6.0", + "react": "^16.8.0 || ^17.0.0", + "react-dom": "^16.8.0 || ^17.0.0" + } + }, "node_modules/@material-ui/styles": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", @@ -3441,6 +3479,15 @@ "@types/node": "*" } }, + "node_modules/@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "dependencies": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -3540,6 +3587,41 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "dependencies": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + } + }, + "node_modules/@types/react-redux/node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@types/react-redux/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/@types/react-redux/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -3583,6 +3665,14 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, + "node_modules/@types/styled-jsx": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", + "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -7918,6 +8008,14 @@ "node": ">=6.0.0" } }, + "node_modules/css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "dependencies": { + "tiny-invariant": "^1.0.6" + } + }, "node_modules/css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -8330,7 +8428,6 @@ "version": "2.22.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==", - "dev": true, "engines": { "node": ">=0.11" }, @@ -8339,6 +8436,11 @@ "url": "https://opencollective.com/date-fns" } }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11423,6 +11525,11 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "node_modules/filefy": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", + "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" + }, "node_modules/filelist": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", @@ -16714,6 +16821,34 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/material-table": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.54.2.tgz", + "integrity": "sha512-g4Au8wHONslUAZli2QUY2c26gRMs4C3cRvNDNpTeIuD9jNnvS9VPbQ2uXjiC48HMzromNfazciYnfYgRRb2g0Q==", + "dependencies": { + "@date-io/date-fns": "^1.1.0", + "@material-ui/pickers": "^3.2.2", + "classnames": "^2.2.6", + "date-fns": "^2.0.0-alpha.27", + "debounce": "^1.2.0", + "fast-deep-equal": "2.0.1", + "filefy": "0.1.10", + "prop-types": "^15.6.2", + "react-beautiful-dnd": "11.0.3", + "react-double-scrollbar": "0.0.15" + }, + "peerDependencies": { + "@date-io/core": "^1.3.6", + "@material-ui/core": "^4.0.1", + "react": "^16.8.4", + "react-dom": "^16.8.4" + } + }, + "node_modules/material-table/node_modules/fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + }, "node_modules/material-ui": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.20.2.tgz", @@ -20361,6 +20496,11 @@ "performance-now": "^2.1.0" } }, + "node_modules/raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "node_modules/railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -20506,6 +20646,82 @@ "asap": "~2.0.6" } }, + "node_modules/react-beautiful-dnd": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz", + "integrity": "sha512-2FX2SnOlKMmfn90xUHCav7cxRWXwY7FeRa6TzdxWeX7DdP5JTvVQcsWgiOkdbJSj+J+1q1nA9QO4/HQ52D0DAA==", + "dependencies": { + "@babel/runtime-corejs2": "^7.4.4", + "css-box-model": "^1.1.2", + "memoize-one": "^5.0.4", + "raf-schd": "^4.0.0", + "react-redux": "^7.0.3", + "redux": "^4.0.1", + "tiny-invariant": "^1.0.4", + "use-memo-one": "^1.1.0" + }, + "peerDependencies": { + "react": "^16.8.5" + } + }, + "node_modules/react-beautiful-dnd/node_modules/@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/react-beautiful-dnd/node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/react-beautiful-dnd/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-beautiful-dnd/node_modules/react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "dependencies": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + }, + "peerDependencies": { + "react": "^16.8.3 || ^17 || ^18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/react-beautiful-dnd/node_modules/redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "dependencies": { + "@babel/runtime": "^7.9.2" + } + }, + "node_modules/react-beautiful-dnd/node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, "node_modules/react-bootstrap": { "version": "0.32.4", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.32.4.tgz", @@ -20913,6 +21129,17 @@ "react": "^16.0.0" } }, + "node_modules/react-double-scrollbar": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", + "integrity": "sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg==", + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "react": ">= 0.14.7" + } + }, "node_modules/react-draggable": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-3.3.2.tgz", @@ -22355,6 +22582,17 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "node_modules/rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "dependencies": { + "@babel/runtime": "^7.3.1" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -25090,6 +25328,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -25849,6 +26092,14 @@ "react": ">=16.8.0" } }, + "node_modules/use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/usfm-js": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/usfm-js/-/usfm-js-3.4.2.tgz", @@ -29901,6 +30152,19 @@ "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-10.1.0.tgz", "integrity": "sha512-ij4wRiunFfaJxjB0BdrYHIH8FxBJpOwNPhhAcunlmPdXudL1WQV1qoP9un6JsEBAgQH+7UXyyjh0g7jTxXK6tg==" }, + "@date-io/core": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.3.13.tgz", + "integrity": "sha512-AlEKV7TxjeK+jxWVKcCFrfYAk8spX9aCyiToFIiLPtfQbsjmRGLIhb5VZgptQcJdHtLXo7+m0DuurwFgUToQuA==" + }, + "@date-io/date-fns": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@date-io/date-fns/-/date-fns-1.3.13.tgz", + "integrity": "sha512-yXxGzcRUPcogiMj58wVgFjc9qUYrCnnU9eLcyNbsQCmae4jPuZCDoIBR21j8ZURsM7GRtU62VOw5yNd4dDHunA==", + "requires": { + "@date-io/core": "^1.3.13" + } + }, "@electron/asar": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.3.tgz", @@ -30568,6 +30832,19 @@ } } }, + "@material-ui/pickers": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.3.11.tgz", + "integrity": "sha512-pDYjbjUeabapijS2FpSwK/ruJdk7IGeAshpLbKDa3PRRKRy7Nv6sXxAvUg2F+lID/NwUKgBmCYS5bzrl7Xxqzw==", + "requires": { + "@babel/runtime": "^7.6.0", + "@date-io/core": "1.x", + "@types/styled-jsx": "^2.2.8", + "clsx": "^1.0.2", + "react-transition-group": "^4.0.0", + "rifm": "^0.7.0" + } + }, "@material-ui/styles": { "version": "4.11.4", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.4.tgz", @@ -30967,6 +31244,15 @@ "@types/node": "*" } }, + "@types/hoist-non-react-statics": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", + "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", + "requires": { + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0" + } + }, "@types/http-cache-semantics": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz", @@ -31073,6 +31359,40 @@ } } }, + "@types/react-redux": { + "version": "7.1.33", + "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", + "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", + "requires": { + "@types/hoist-non-react-statics": "^3.3.0", + "@types/react": "*", + "hoist-non-react-statics": "^3.3.0", + "redux": "^4.0.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + } + } + }, "@types/react-transition-group": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.1.tgz", @@ -31111,6 +31431,14 @@ "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==" }, + "@types/styled-jsx": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@types/styled-jsx/-/styled-jsx-2.2.9.tgz", + "integrity": "sha512-W/iTlIkGEyTBGTEvZCey8EgQlQ5l0DwMqi3iOXlLs2kyBwYTXHKEiU6IZ5EwoRwngL8/dGYuzezSup89ttVHLw==", + "requires": { + "@types/react": "*" + } + }, "@types/tough-cookie": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.0.tgz", @@ -34600,6 +34928,14 @@ "postcss": "^7.0.5" } }, + "css-box-model": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", + "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", + "requires": { + "tiny-invariant": "^1.0.6" + } + }, "css-color-names": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", @@ -34934,8 +35270,12 @@ "date-fns": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.22.1.tgz", - "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==", - "dev": true + "integrity": "sha512-yUFPQjrxEmIsMqlHhAhmxkuH769baF21Kk+nZwZGyrMoyLA+LugaQtC0+Tqf9CBUUULWwUJt6Q5ySI3LJDDCGg==" + }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" }, "debug": { "version": "4.3.4", @@ -37376,6 +37716,11 @@ "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", "optional": true }, + "filefy": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/filefy/-/filefy-0.1.10.tgz", + "integrity": "sha512-VgoRVOOY1WkTpWH+KBy8zcU1G7uQTVsXqhWEgzryB9A5hg2aqCyZ6aQ/5PSzlqM5+6cnVrX6oYV0XqD3HZSnmQ==" + }, "filelist": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.2.tgz", @@ -41596,6 +41941,30 @@ } } }, + "material-table": { + "version": "1.54.2", + "resolved": "https://registry.npmjs.org/material-table/-/material-table-1.54.2.tgz", + "integrity": "sha512-g4Au8wHONslUAZli2QUY2c26gRMs4C3cRvNDNpTeIuD9jNnvS9VPbQ2uXjiC48HMzromNfazciYnfYgRRb2g0Q==", + "requires": { + "@date-io/date-fns": "^1.1.0", + "@material-ui/pickers": "^3.2.2", + "classnames": "^2.2.6", + "date-fns": "^2.0.0-alpha.27", + "debounce": "^1.2.0", + "fast-deep-equal": "2.0.1", + "filefy": "0.1.10", + "prop-types": "^15.6.2", + "react-beautiful-dnd": "11.0.3", + "react-double-scrollbar": "0.0.15" + }, + "dependencies": { + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" + } + } + }, "material-ui": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/material-ui/-/material-ui-0.20.2.tgz", @@ -44578,6 +44947,11 @@ "performance-now": "^2.1.0" } }, + "raf-schd": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" + }, "railroad-diagrams": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", @@ -44695,6 +45069,67 @@ } } }, + "react-beautiful-dnd": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-11.0.3.tgz", + "integrity": "sha512-2FX2SnOlKMmfn90xUHCav7cxRWXwY7FeRa6TzdxWeX7DdP5JTvVQcsWgiOkdbJSj+J+1q1nA9QO4/HQ52D0DAA==", + "requires": { + "@babel/runtime-corejs2": "^7.4.4", + "css-box-model": "^1.1.2", + "memoize-one": "^5.0.4", + "raf-schd": "^4.0.0", + "react-redux": "^7.0.3", + "redux": "^4.0.1", + "tiny-invariant": "^1.0.4", + "use-memo-one": "^1.1.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.7.tgz", + "integrity": "sha512-UwgBRMjJP+xv857DCngvqXI3Iq6J4v0wXmwc6sapg+zyhbwmQX67LUEFrkK5tbyJ30jGuG3ZvWpBiB9LCy1kWw==", + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "react-redux": { + "version": "7.2.9", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", + "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", + "requires": { + "@babel/runtime": "^7.15.4", + "@types/react-redux": "^7.1.20", + "hoist-non-react-statics": "^3.3.2", + "loose-envify": "^1.4.0", + "prop-types": "^15.7.2", + "react-is": "^17.0.2" + } + }, + "redux": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", + "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", + "requires": { + "@babel/runtime": "^7.9.2" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + } + } + }, "react-bootstrap": { "version": "0.32.4", "resolved": "https://registry.npmjs.org/react-bootstrap/-/react-bootstrap-0.32.4.tgz", @@ -45029,6 +45464,11 @@ "scheduler": "^0.13.6" } }, + "react-double-scrollbar": { + "version": "0.0.15", + "resolved": "https://registry.npmjs.org/react-double-scrollbar/-/react-double-scrollbar-0.0.15.tgz", + "integrity": "sha512-dLz3/WBIpgFnzFY0Kb4aIYBMT2BWomHuW2DH6/9jXfS6/zxRRBUFQ04My4HIB7Ma7QoRBpcy8NtkPeFgcGBpgg==" + }, "react-draggable": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-3.3.2.tgz", @@ -46180,6 +46620,14 @@ "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=" }, + "rifm": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/rifm/-/rifm-0.7.0.tgz", + "integrity": "sha512-DSOJTWHD67860I5ojetXdEQRIBvF6YcpNe53j0vn1vp9EUb9N80EiZTxgP+FkDKorWC8PZw052kTF4C1GOivCQ==", + "requires": { + "@babel/runtime": "^7.3.1" + } + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -48344,6 +48792,11 @@ "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=" }, + "tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "tiny-warning": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", @@ -48931,6 +49384,11 @@ "dequal": "1.0.0" } }, + "use-memo-one": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", + "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==" + }, "usfm-js": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/usfm-js/-/usfm-js-3.4.2.tgz", diff --git a/package.json b/package.json index 2eb0d2427a..fe0cd601db 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "translationCore", "productName": "translationCore", - "version": "3.6.4", + "version": "3.6.4-search", "minCompatibleVersion": "3.6.0", "manifestVersion": "8", "description": "A bridge between TS and TM", @@ -145,6 +145,7 @@ "json-stringify-safe": "5.0.1", "konami-code-js": "0.8.0", "lodash": "4.17.19", + "material-table": "1.54.2", "material-ui": "0.20.2", "memoize-one": "5.0.0", "mkdirp": "0.5.1", diff --git a/scripts/latest/getLatestTcore.js b/scripts/latest/getLatestTcore.js deleted file mode 100644 index ddf04a0c2e..0000000000 --- a/scripts/latest/getLatestTcore.js +++ /dev/null @@ -1,194 +0,0 @@ -const semver = require('semver'); -const axios = require('axios'); - -//////////////////////////////////////////////// -// taken from SoftwareUpdateDialogContainer.js -// -// to debug: node --inspect-brk scripts/latest/getLatestTcore.js - -/** - * Returns the correct update asset for this operating system. - * If the update is not newer than the installed version null will be returned. - * - * @param {object} response - the network response - * @param {string} installedVersion - the installed version of the application - * @param {string} osArch - the operating system architecture - * @param {string} osPlatform - the operating system. - * @return {*} the update object - */ -function getUpdateAsset(response, installedVersion, osArch, osPlatform) { - let fallbackPlatform = null; - let fallbackUpdate = null; - const platformNames = { - 'aix': 'linux', - 'darwin': 'macos', - 'macos': 'macos', - 'freebsd': 'linux', - 'linux': 'linux', - 'openbsd': 'linux', - 'sunos': 'linux', - 'win32': 'win', - 'win': 'win', - }; - - // TRICKY: some architectures will return ia32 instead of x32 - if (osArch === 'ia32') { - osArch = 'x32'; - } - - const platformName = platformNames[osPlatform]; - - if (osArch === 'arm64') { - fallbackPlatform = `${platformName}-universal`; - } - - const platform = `${platformName}-${osArch}`; - let update = null; - const tagName = response.tag_name; - console.log(`Release tag-name `, tagName); - - for (const asset of response.assets) { - if (asset.name.includes(platform)) { - update = { - ...asset, - latest_version: tagName, - installed_version: installedVersion, - }; - break; - } else if (fallbackPlatform && asset.name.includes(fallbackPlatform)) { - fallbackUpdate = { - ...asset, - latest_version: tagName, - installed_version: installedVersion, - }; - } - } - - const isLiteRelease = tagName.toUpperCase().includes('-LITE'); - - if (!update) { // if we didn't find exact match, use fallback compatible match - console.log(`using fallback architecture:`, fallbackUpdate); - update = fallbackUpdate; - } - - // validate version - let upToDate = false; - - if (update) { - const latest = semver.valid(semver.coerce(update.latest_version)); - const installed = semver.valid(semver.coerce(update.installed_version)); - - if (!semver.gt(latest, installed)) { - console.log(`installed version ${update.installed_version} is up to date with release version ${update.latest_version} `); - upToDate = true; - } else { - console.log(`installed version ${update.installed_version} is older than release version ${update.latest_version} `); - } - } - return { - update, - upToDate, - tagName, - isLiteRelease, - }; -} - -/** - * does fetch - * @private - */ -function fetchUrl(url) { - return new Promise((resolve, reject) => { - const CancelToken = axios.CancelToken; - const source = CancelToken.source(); - const request = { - cancelToken: source.token, - method: 'GET', - url, - }; - - axios(request).then(response => { - resolve(response); - }).catch(error => { - reject(error); - }); - }); -} - -async function getLatestRelease() { - const latestReleaseUrl = `https://api.github.com/repos/unfoldingWord-dev/translationCore/releases/latest`; - const isLiteInstall_ = true; - const installedVersion = 'v0.0.0'; - - const configs = { - 'macos': { - 'x64': null, - 'universal': null, - }, - 'linux': { - 'x64': null, - 'arm64': null, - }, - 'win': { - 'x64': null, - 'x32': null, - }, - }; - - let response = null; - - try { - response = await fetchUrl(latestReleaseUrl); - } catch (e) { - console.error(`Error getting latest tCore from ${latestReleaseUrl}`, e); - } - - if (response?.data) { - for (const osPlatform of Object.keys(configs)) { - const osArchs = configs[osPlatform]; - - for (const osArch of Object.keys(osArchs)) { - let browser_download_url = null; - const { - update, - upToDate, - tagName, - isLiteRelease, - } = getUpdateAsset(response.data, installedVersion, osArch, osPlatform); - - if (update) { - browser_download_url = update.browser_download_url; - - if (!upToDate && (isLiteInstall_ !== isLiteRelease)) { - console.log(`found tagName ${tagName} which is a fallback install since isLiteRelease=${isLiteRelease}`, update); - const baseTagName = tagName.split('-')[0]; - const sizeSuffix = isLiteInstall_ ? '-LITE' : ''; - const rightTagName = baseTagName + sizeSuffix; - const tagUrl = `https://api.github.com/repos/unfoldingWord-dev/translationCore/releases/tags/${rightTagName}`; - console.log(`getting release ${rightTagName}`, update); - - try { - // eslint-disable-next-line no-await-in-loop - response = await fetchUrl(tagUrl); - const { update } = getUpdateAsset(response.data, installedVersion, osArch, osPlatform); - browser_download_url = update.browser_download_url; - } catch (e) { - console.error(`Error getting latest tCore from ${tagUrl}`, e); - return {}; - } - } - } - osArchs[osArch] = browser_download_url; - } - } - - return configs; - } - - return {}; -} - -getLatestRelease().then(installs => { - console.log(installs); -}); - diff --git a/scripts/latest/getLatestTcore.py b/scripts/latest/getLatestTcore.py deleted file mode 100644 index 9c34c3db9b..0000000000 --- a/scripts/latest/getLatestTcore.py +++ /dev/null @@ -1,181 +0,0 @@ -import json -import requests -import re -from packaging import version - -################################################### -# taken from SoftwareUpdateDialogContainer.js into getLatestTcore.js and converted with chatGPT - -def get_update_asset(response, installed_version, os_arch, os_platform): - fallback_platform = None - fallback_update = None - platform_names = { - 'aix': 'linux', - 'darwin': 'macos', - 'macos': 'macos', - 'freebsd': 'linux', - 'linux': 'linux', - 'openbsd': 'linux', - 'sunos': 'linux', - 'win32': 'win', - 'win': 'win', - } - - if os_arch == 'ia32': - os_arch = 'x32' - - platform_name = platform_names[os_platform] - - if os_arch == 'arm64': - fallback_platform = f"{platform_name}-universal" - - platform = f"{platform_name}-{os_arch}" - update = None - tag_name = response['tag_name'] - # print(f"Release tag-name {tag_name}") - - for asset in response['assets']: - if platform in asset['name']: - update = { - **asset, - 'latest_version': tag_name, - 'installed_version': installed_version, - } - break - elif fallback_platform and fallback_platform in asset['name']: - fallback_update = { - **asset, - 'latest_version': tag_name, - 'installed_version': installed_version, - } - - is_lite_release = bool(re.search("-LITE", tag_name.upper())) - - if not update: - # print(f"using fallback architecture: {fallback_update}") - update = fallback_update - - up_to_date = False - - if update: - latest = getVersion(update['latest_version']) - installed = getVersion(update['installed_version']) - - if latest <= installed: - # print(f"installed version {update['installed_version']} is up to date with release version {update['latest_version']}") - up_to_date = True - # else: - # print(f"installed version {update['installed_version']} is older than release version {update['latest_version']}") - - return { - 'update': update, - 'up_to_date': up_to_date, - 'tag_name': tag_name, - 'is_lite_release': is_lite_release, - } - -def remove_leading_v(version_string): - if version_string.startswith('v'): - return version_string[1:] - else: - return version_string - -def remove_after_dash(input_string): - if '-' in input_string: - return input_string.split('-')[0] - else: - return input_string - -def getVersion(version_): - versionStr = remove_leading_v(version_) - versionStr = remove_after_dash(versionStr) - return version.parse(versionStr) - -def fetch_url(url): - response = requests.get(url) - response.raise_for_status() - return response.json() - -def get_latest_release(): - latest_release_url = 'https://api.github.com/repos/unfoldingWord-dev/translationCore/releases/latest' - is_lite_install_ = True - installed_version = 'v0.0.0' - - configs = { - 'macos': { - 'x64': None, - 'universal': None, - }, - 'linux': { - 'x64': None, - 'arm64': None, - }, - 'win': { - 'x64': None, - 'x32': None, - }, - } - - response = None - - try: - response = fetch_url(latest_release_url) - except Exception as e: - print(f"Error getting latest tCore from {latest_release_url}", e) - - if response and 'assets' in response: - for os_platform, os_archs in configs.items(): - for os_arch in os_archs: - browser_download_url = None - result = get_update_asset(response, installed_version, os_arch, os_platform) - update = result['update'] - up_to_date = result['up_to_date'] - tag_name = result['tag_name'] - is_lite_release = result['is_lite_release'] - - if update: - browser_download_url = update['browser_download_url'] - - if is_lite_install_ != is_lite_release: - # print(f"found tag name {tag_name} which is a fallback install since isLiteRelease={is_lite_release}", update) - base_tag_name = tag_name.split('-')[0] - size_suffix = '-LITE' if is_lite_install_ else '' - right_tag_name = base_tag_name + size_suffix - tag_url = f"https://api.github.com/repos/unfoldingWord-dev/translationCore/releases/tags/{right_tag_name}" - # print(f"getting release {right_tag_name}", update) - - try: - response = fetch_url(tag_url) - result = get_update_asset(response, installed_version, os_arch, os_platform) - update = result['update'] - browser_download_url = update['browser_download_url'] - except Exception as e: - # print(f"Error getting latest tCore from {tag_url}", e) - return {} - - os_archs[os_arch] = browser_download_url - - return configs - - return {} - -installers = get_latest_release() -json_data = json.dumps(installers, indent=4) -print(json_data) - -##################################################### -# Output is in this format: -# { -# "macos": { -# "x64": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-macos-x64-3.6.0-LITE-2bc4476.dmg", -# "universal": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-macos-universal-3.6.0-LITE-2bc4476.dmg" -# }, -# "linux": { -# "x64": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-linux-x64-3.6.0-LITE-2bc4476.deb", -# "arm64": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-linux-arm64-3.6.0-LITE-2bc4476.deb" -# }, -# "win": { -# "x64": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-win-x64-3.6.0-LITE-2bc4476.exe", -# "x32": "https://github.com/unfoldingWord/translationCore/releases/download/v3.6.0-LITE/tC-win-x32-3.6.0-LITE-2bc4476.exe" -# } -# } diff --git a/src/__tests__/alignmentSearchHelpers.test.js b/src/__tests__/alignmentSearchHelpers.test.js new file mode 100644 index 0000000000..405a963255 --- /dev/null +++ b/src/__tests__/alignmentSearchHelpers.test.js @@ -0,0 +1,407 @@ +import path from 'path-extra'; +import { + buildSearchRegex, + getCount, + loadAlignments, + multiSearchAlignments, + regexSearch, + searchAlignments, +} from '../js/helpers/alignmentSearchHelpers'; + +jest.unmock('fs-extra'); + +describe('test greek alignments', () => { + const fileName = 'es-419_glt_Es-419%5Fgl_el-x-koine_testament_v39%2E1_501'; + const jsonPath = path.join(__dirname, `fixtures/alignmentData/${fileName}.json`); + const alignmentData = loadAlignments(jsonPath); + const { + alignments, + target, + lemma, + source, + } = alignmentData; + + it('verify Greek alignment search data', () => { + // given + const expectedCount = 501; + + // when + + // then + expect(alignments).toBeTruthy(); + expect(Object.keys(alignments).length).toEqual(expectedCount); + const sourceAlignmentsCount = getCount(source.alignments); + const targetAlignmentsCount = getCount(target.alignments); + const lemmaAlignmentsCount = getCount(lemma.alignments); + expect(targetAlignmentsCount).toEqual(expectedCount); + expect(sourceAlignmentsCount).toEqual(expectedCount); + expect(lemmaAlignmentsCount).toEqual(expectedCount); + }); + + describe('test multiSearchAlignments', () => { + it('search partial Pab', () => { // Pablo + // given + const search = 'Pab'; + const config = { + fullWord: false, + caseInsensitive: false, + searchLemma: true, + searchSource: true, + searchTarget: true, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(1); + }); + + it('search full κατὰ', () => { + // given + const search = 'κατὰ'; + const config = { + fullWord: true, + caseInsensitive: false, + searchLemma: true, + searchSource: true, + searchTarget: true, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(4); + }); + + it('search full κατά', () => { + // given + const search = 'κατά'; + const config = { + fullWord: true, + caseInsensitive: false, + searchLemma: true, + searchSource: true, + searchTarget: true, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(7); + }); + + it('search lemma full κατ*', () => { + // given + const search = 'κατ*'; + const config = { + fullWord: true, + caseInsensitive: false, + searchLemma: true, + searchSource: false, + searchTarget: false, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(9); + }); + + it('search source full κατ*', () => { + // given + const search = 'κατ*'; + const config = { + fullWord: true, + caseInsensitive: false, + searchLemma: false, + searchSource: true, + searchTarget: false, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(10); + }); + + it('search all full κατ*', () => { + // given + const search = 'κατ*'; + const config = { + fullWord: true, + caseInsensitive: false, + searchLemma: true, + searchSource: true, + searchTarget: true, + }; + + // when + const found = multiSearchAlignments(alignmentData, null, search, config); + + // then + expect(found.length).toEqual(10); + }); + }); + + describe('test searchAlignments', () => { + it('search text Pab', () => { + // given + const search = 'Pab'; + const fullWord = false; + const caseInsensitive = false; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex partial Pab false', () => { + // given + const search = 'Pab'; + const fullWord = true; + const caseInsensitive = false; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(0); + }); + + it('search regex partial word Pab true', () => { + // given + const search = 'Pab'; + const fullWord = false; + const caseInsensitive = false; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex full word Pablo true', () => { + // given + const search = 'Pablo'; + const fullWord = true; + const caseInsensitive = false; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex full word pablo case insensitive true', () => { + // given + const search = 'pablo'; + const fullWord = true; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex full word p?blo case insensitive true', () => { + // given + const search = 'p?blo'; + const fullWord = true; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex full word pa*o case insensitive true', () => { + // given + const search = 'pa*o'; + const fullWord = true; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(1); + }); + + it('search regex full word jes* case insensitive true', () => { + // given + const search = 'jes*'; + const fullWord = true; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, target.keys, target.alignments); + + // then + expect(foundAlignments.length).toEqual(3); + }); + + it('search regex partial word κατ case insensitive true', () => { + // given + const search = 'κατ'; + const fullWord = false; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, source.keys, source.alignments); + + // then + expect(foundAlignments.length).toEqual(12); + }); + + it('search regex full word κατὰ case insensitive true', () => { + // given + const search = 'Κατὰ'; + const fullWord = true; + const caseInsensitive = true; + + // when + const foundAlignments = searchAlignments(search, fullWord, caseInsensitive, source.keys, source.alignments); + + // then + expect(foundAlignments.length).toEqual(4); + }); + }); + + describe('test regexSearch', () => { + it('search text Pab', () => { + // given + const search = 'Pab'; + + // when + const found = regexSearch(target.keys, search); + + // then + expect(found.length).toEqual(1); + }); + + it('search greek text κατὰ case', () => { + // given + const search = 'κατ'; + + // when + const found = regexSearch(source.keys, search); + + // then + expect(found.length).toEqual(7); + }); + + it('search regex partial Pab false', () => { + // given + const search_ = 'Pab'; + const fullWord = true; + const caseInsensitive = false; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(0); + }); + + it('search regex partial word Pab true', () => { + // given + const search_ = 'Pab'; + const fullWord = false; + const caseInsensitive = false; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(1); + }); + + it('search regex full word Pablo true', () => { + // given + const search_ = 'Pablo'; + const fullWord = true; + const caseInsensitive = false; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(1); + }); + + it('search regex full word pablo case insensitive true', () => { + // given + const search_ = 'pablo'; + const fullWord = true; + const caseInsensitive = true; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(1); + }); + + it('search regex full word p?blo case insensitive true', () => { + // given + const search_ = 'p?blo'; + const fullWord = true; + const caseInsensitive = true; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(1); + }); + + it('search regex full word pa*o case insensitive true', () => { + // given + const search_ = 'pa*o'; + const fullWord = true; + const caseInsensitive = true; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(1); + }); + + it('search regex full word jes* case insensitive true', () => { + // given + const search_ = 'jes*'; + const fullWord = true; + const caseInsensitive = true; + const { search, flags } = buildSearchRegex(search_, fullWord, caseInsensitive); + + // when + const found = regexSearch(target.keys, search, flags); + + // then + expect(found.length).toEqual(3); + }); + }); +}); + +// helpers + diff --git a/src/__tests__/fixtures/alignmentData/es-419_glt_Es-419%5Fgl_el-x-koine_testament_v39%2E1_501.json b/src/__tests__/fixtures/alignmentData/es-419_glt_Es-419%5Fgl_el-x-koine_testament_v39%2E1_501.json new file mode 100644 index 0000000000..013964d066 --- /dev/null +++ b/src/__tests__/fixtures/alignmentData/es-419_glt_Es-419%5Fgl_el-x-koine_testament_v39%2E1_501.json @@ -0,0 +1 @@ +{"alignments":[{"sourceText":"Παῦλος","sourceLemma":"Παῦλος","strong":"G39720","targetText":"Pablo","refs":["tit 1:1"]},{"sourceText":"δοῦλος","sourceLemma":"δοῦλος","strong":"G14010","targetText":"siervo","refs":["tit 1:1"]},{"sourceText":"Θεοῦ","sourceLemma":"θεός","strong":"G23160","targetText":"de Dios","refs":["tit 1:1","tit 1:1","tit 1:3","tit 1:7","tit 2:10"]},{"sourceText":"δὲ","sourceLemma":"δέ","strong":"G11610","targetText":"y","refs":["tit 1:1"]},{"sourceText":"ἀπόστολος","sourceLemma":"ἀπόστολος","strong":"G06520","targetText":"apóstol","refs":["tit 1:1"]},{"sourceText":"Ἰησοῦ Χριστοῦ","sourceLemma":"Ἰησοῦς χριστός","strong":"G24240 G55470","targetText":"de Jesucristo","refs":["tit 1:1"]},{"sourceText":"κατὰ","sourceLemma":"κατά","strong":"G25960","targetText":"conforme","refs":["tit 1:1","tit 1:9"]},{"sourceText":"πίστιν","sourceLemma":"πίστις","strong":"G41020","targetText":"a la fe","refs":["tit 1:1"]},{"sourceText":"ἐκλεκτῶν","sourceLemma":"ἐκλεκτός","strong":"G15880","targetText":"de los elegidos","refs":["tit 1:1"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"y","refs":["tit 1:1","tit 1:4","tit 1:4","tit 1:5","tit 1:9","tit 1:10","tit 2:12","tit 2:13","tit 2:13","tit 2:14","tit 2:15","tit 2:15","tit 3:3","tit 3:3","tit 3:4","tit 3:5","tit 3:8","tit 3:9","tit 3:9","tit 3:9","tit 3:10","tit 3:11","tit 3:13"]},{"sourceText":"ἐπίγνωσιν","sourceLemma":"ἐπίγνωσις","strong":"G19220","targetText":"al conocimiento","refs":["tit 1:1"]},{"sourceText":"ἀληθείας","sourceLemma":"ἀλήθεια","strong":"G02250","targetText":"de la verdad","refs":["tit 1:1"]},{"sourceText":"τῆς","sourceLemma":"ὁ","strong":"G35880","targetText":"que","refs":["tit 1:1"]},{"sourceText":"κατ’","sourceLemma":"κατά","strong":"G25960","targetText":"es de acuerdo con","refs":["tit 1:1"]},{"sourceText":"εὐσέβειαν","sourceLemma":"εὐσέβεια","strong":"G21500","targetText":"la piedad","refs":["tit 1:1"]},{"sourceText":"ἐπ’","sourceLemma":"ἐπί","strong":"G19090","targetText":"sobre","refs":["tit 1:2"]},{"sourceText":"ἐλπίδι","sourceLemma":"ἐλπίς","strong":"G16800","targetText":"la esperanza","refs":["tit 1:2"]},{"sourceText":"ζωῆς","sourceLemma":"ζωή","strong":"G22220","targetText":"de la vida","refs":["tit 1:2","tit 3:7"]},{"sourceText":"αἰωνίου","sourceLemma":"αἰώνιος","strong":"G01660","targetText":"eterna","refs":["tit 1:2","tit 3:7"]},{"sourceText":"ἣν","sourceLemma":"ὅς","strong":"G37390","targetText":"la cual","refs":["tit 1:2"]},{"sourceText":"Θεὸς","sourceLemma":"θεός","strong":"G23160","targetText":"Dios","refs":["tit 1:2"]},{"sourceText":"ὁ ἀψευδὴς","sourceLemma":"ὁ ἀψευδής","strong":"G35880 G08930","targetText":"que no miente","refs":["tit 1:2"]},{"sourceText":"ἐπηγγείλατο","sourceLemma":"ἐπαγγέλλω","strong":"G18610","targetText":"prometió","refs":["tit 1:2"]},{"sourceText":"πρὸ","sourceLemma":"πρό","strong":"G42530","targetText":"antes","refs":["tit 1:2"]},{"sourceText":"χρόνων","sourceLemma":"χρόνος","strong":"G55500","targetText":"de los tiempos","refs":["tit 1:2"]},{"sourceText":"αἰωνίων","sourceLemma":"αἰώνιος","strong":"G01660","targetText":"de los siglos","refs":["tit 1:2"]},{"sourceText":"δὲ","sourceLemma":"δέ","strong":"G11610","targetText":"pero","refs":["tit 1:3","tit 1:15","tit 1:16"]},{"sourceText":"ἐφανέρωσεν","sourceLemma":"φανερόω","strong":"G53190","targetText":"reveló","refs":["tit 1:3"]},{"sourceText":"καιροῖς","sourceLemma":"καιρός","strong":"G25400","targetText":"en los tiempos","refs":["tit 1:3"]},{"sourceText":"ἰδίοις","sourceLemma":"ἴδιος","strong":"G23980","targetText":"apropiados","refs":["tit 1:3"]},{"sourceText":"αὐτοῦ","sourceLemma":"αὐτός","strong":"G08460","targetText":"su","refs":["tit 1:3","tit 3:5"]},{"sourceText":"τὸν λόγον","sourceLemma":"ὁ λόγος","strong":"G35880 G30560","targetText":"palabra","refs":["tit 1:3"]},{"sourceText":"ἐν","sourceLemma":"ἐν","strong":"G17220","targetText":"por","refs":["tit 1:3"]},{"sourceText":"κηρύγματι","sourceLemma":"κήρυγμα","strong":"G27820","targetText":"la predicación","refs":["tit 1:3"]},{"sourceText":"ὃ","sourceLemma":"ὅς","strong":"G37390","targetText":"que","refs":["tit 1:3"]},{"sourceText":"ἐγὼ","sourceLemma":"ἐγώ","strong":"G14730","targetText":"me","refs":["tit 1:3"]},{"sourceText":"ἐπιστεύθην","sourceLemma":"πιστεύω","strong":"G41000","targetText":"fue confiada","refs":["tit 1:3"]},{"sourceText":"κατ’","sourceLemma":"κατά","strong":"G25960","targetText":"conforme","refs":["tit 1:3"]},{"sourceText":"ἐπιταγὴν","sourceLemma":"ἐπιταγή","strong":"G20030","targetText":"al mandato","refs":["tit 1:3"]},{"sourceText":"ἡμῶν","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nuestro","refs":["tit 1:3","tit 1:4","tit 2:10","tit 2:13","tit 3:4","tit 3:6"]},{"sourceText":"τοῦ Σωτῆρος","sourceLemma":"ὁ σωτήρ","strong":"G35880 G49900","targetText":"salvador","refs":["tit 1:3","tit 1:4","tit 2:10"]},{"sourceText":"Τίτῳ","sourceLemma":"Τίτος","strong":"G51030","targetText":"a Tito","refs":["tit 1:4"]},{"sourceText":"γνησίῳ","sourceLemma":"γνήσιος","strong":"G11030","targetText":"verdadero","refs":["tit 1:4"]},{"sourceText":"τέκνῳ","sourceLemma":"τέκνον","strong":"G50430","targetText":"hijo","refs":["tit 1:4"]},{"sourceText":"κατὰ","sourceLemma":"κατά","strong":"G25960","targetText":"conforme a","refs":["tit 1:4"]},{"sourceText":"κοινὴν","sourceLemma":"κοινός","strong":"G28390","targetText":"nuestra común","refs":["tit 1:4"]},{"sourceText":"πίστιν","sourceLemma":"πίστις","strong":"G41020","targetText":"fe","refs":["tit 1:4","tit 2:10"]},{"sourceText":"χάρις","sourceLemma":"χάρις","strong":"G54850","targetText":"Gracia","refs":["tit 1:4"]},{"sourceText":"εἰρήνη","sourceLemma":"εἰρήνη","strong":"G15150","targetText":"paz","refs":["tit 1:4"]},{"sourceText":"ἀπὸ","sourceLemma":"ἀπό","strong":"G05750","targetText":"de","refs":["tit 1:4","tit 2:14"]},{"sourceText":"Θεοῦ","sourceLemma":"θεός","strong":"G23160","targetText":"Dios","refs":["tit 1:4","tit 2:5","tit 2:11","tit 2:13","tit 3:4"]},{"sourceText":"Πατρὸς","sourceLemma":"πατήρ","strong":"G39620","targetText":"Padre","refs":["tit 1:4"]},{"sourceText":"Χριστοῦ","sourceLemma":"χριστός","strong":"G55470","targetText":"de Cristo","refs":["tit 1:4"]},{"sourceText":"Ἰησοῦ","sourceLemma":"Ἰησοῦς","strong":"G24240","targetText":"Jesús","refs":["tit 1:4"]},{"sourceText":"χάριν","sourceLemma":"χάριν","strong":"G54840","targetText":"Por causa","refs":["tit 1:5"]},{"sourceText":"τούτου","sourceLemma":"οὗτος","strong":"G37780","targetText":"esta","refs":["tit 1:5"]},{"sourceText":"σε","sourceLemma":"σύ","strong":"G47710","targetText":"te","refs":["tit 1:5"]},{"sourceText":"ἀπέλιπόν","sourceLemma":"ἀπολίπω","strong":"G06200","targetText":"dejé","refs":["tit 1:5"]},{"sourceText":"ἐν","sourceLemma":"ἐν","strong":"G17220","targetText":"en","refs":["tit 1:5","tit 1:13","tit 2:7","tit 2:9","tit 2:10","tit 2:12","tit 3:3","tit 3:15"]},{"sourceText":"Κρήτῃ","sourceLemma":"Κρήτη","strong":"G29140","targetText":"Creta","refs":["tit 1:5"]},{"sourceText":"ἵνα","sourceLemma":"ἵνα","strong":"G24430","targetText":"para que","refs":["tit 1:5","tit 1:9","tit 1:13","tit 2:4","tit 2:5","tit 2:8","tit 2:10","tit 2:12","tit 2:14","tit 3:7","tit 3:8","tit 3:13","tit 3:14"]},{"sourceText":"ἐπιδιορθώσῃ","sourceLemma":"ἐπιδιορθόω","strong":"G19300","targetText":"pusieras en orden","refs":["tit 1:5"]},{"sourceText":"τὰ","sourceLemma":"ὁ","strong":"G35880","targetText":"lo","refs":["tit 1:5"]},{"sourceText":"λείποντα","sourceLemma":"λείπω","strong":"G30070","targetText":"que falta","refs":["tit 1:5"]},{"sourceText":"καταστήσῃς","sourceLemma":"καθίστημι","strong":"G25250","targetText":"designaras","refs":["tit 1:5"]},{"sourceText":"πρεσβυτέρους","sourceLemma":"πρεσβύτερος","strong":"G42450","targetText":"ancianos","refs":["tit 1:5"]},{"sourceText":"κατὰ","sourceLemma":"κατά","strong":"G25960","targetText":"según","refs":["tit 1:5"]},{"sourceText":"πόλιν","sourceLemma":"πόλις","strong":"G41720","targetText":"la ciudad","refs":["tit 1:5"]},{"sourceText":"ὡς","sourceLemma":"ὡς","strong":"G56130","targetText":"como","refs":["tit 1:5","tit 1:7"]},{"sourceText":"ἐγώ","sourceLemma":"ἐγώ","strong":"G14730","targetText":"yo","refs":["tit 1:5"]},{"sourceText":"σοι","sourceLemma":"σύ","strong":"G47710","targetText":"te","refs":["tit 1:5"]},{"sourceText":"διεταξάμην","sourceLemma":"διατάσσω","strong":"G12990","targetText":"ordené","refs":["tit 1:5"]},{"sourceText":"εἴ","sourceLemma":"εἰ","strong":"G14870","targetText":"si","refs":["tit 1:6"]},{"sourceText":"τίς","sourceLemma":"τις","strong":"G51000","targetText":"alguno","refs":["tit 1:6"]},{"sourceText":"ἐστιν","sourceLemma":"εἰμί","strong":"G15100","targetText":"es","refs":["tit 1:6"]},{"sourceText":"ἀνέγκλητος","sourceLemma":"ἀνέγκλητος","strong":"G04100","targetText":"irreprensible","refs":["tit 1:6"]},{"sourceText":"ἀνήρ","sourceLemma":"ἀνήρ","strong":"G04350","targetText":"esposo","refs":["tit 1:6"]},{"sourceText":"μιᾶς","sourceLemma":"εἷς","strong":"G15200","targetText":"de una sola","refs":["tit 1:6"]},{"sourceText":"γυναικὸς","sourceLemma":"γυνή","strong":"G11350","targetText":"mujer","refs":["tit 1:6"]},{"sourceText":"ἔχων","sourceLemma":"ἔχω","strong":"G21920","targetText":"que tenga","refs":["tit 1:6"]},{"sourceText":"τέκνα","sourceLemma":"τέκνον","strong":"G50430","targetText":"hijos","refs":["tit 1:6"]},{"sourceText":"πιστά","sourceLemma":"πιστός","strong":"G41030","targetText":"fieles","refs":["tit 1:6"]},{"sourceText":"ἐν","sourceLemma":"ἐν","strong":"G17220","targetText":"que estén en","refs":["tit 1:6"]},{"sourceText":"μὴ","sourceLemma":"μή","strong":"G33610","targetText":"no","refs":["tit 1:6","tit 1:7","tit 1:7","tit 1:7","tit 1:7","tit 1:7","tit 1:11","tit 1:14","tit 2:5","tit 2:9","tit 3:14"]},{"sourceText":"κατηγορίᾳ","sourceLemma":"κατηγορία","strong":"G27240","targetText":"acusación","refs":["tit 1:6"]},{"sourceText":"ἀσωτίας","sourceLemma":"ἀσωτία","strong":"G08100","targetText":"de libertinaje","refs":["tit 1:6"]},{"sourceText":"ἢ","sourceLemma":"ἤ","strong":"G22280","targetText":"o","refs":["tit 1:6","tit 3:12"]},{"sourceText":"ἀνυπότακτα","sourceLemma":"ἀνυπότακτος","strong":"G05060","targetText":"rebeldía","refs":["tit 1:6"]},{"sourceText":"γὰρ","sourceLemma":"γάρ","strong":"G10630","targetText":"Porque","refs":["tit 1:7","tit 2:11"]},{"sourceText":"δεῖ","sourceLemma":"δέω","strong":"G12100","targetText":"es necesario","refs":["tit 1:7","tit 1:11"]},{"sourceText":"εἶναι","sourceLemma":"εἰμί","strong":"G15100","targetText":"que sea","refs":["tit 1:7"]},{"sourceText":"τὸν","sourceLemma":"ὁ","strong":"G35880","targetText":"el","refs":["tit 1:7","tit 3:13"]},{"sourceText":"ἐπίσκοπον","sourceLemma":"ἐπίσκοπος","strong":"G19850","targetText":"obispo","refs":["tit 1:7"]},{"sourceText":"ἀνέγκλητον","sourceLemma":"ἀνέγκλητος","strong":"G04100","targetText":"irreprensible","refs":["tit 1:7"]},{"sourceText":"οἰκονόμον","sourceLemma":"οἰκονόμος","strong":"G36230","targetText":"administrador","refs":["tit 1:7"]},{"sourceText":"αὐθάδη","sourceLemma":"αὐθάδης","strong":"G08290","targetText":"arrogante","refs":["tit 1:7"]},{"sourceText":"ὀργίλον","sourceLemma":"ὀργίλος","strong":"G37110","targetText":"iracundo","refs":["tit 1:7"]},{"sourceText":"πάροινον","sourceLemma":"πάροινος","strong":"G39430","targetText":"dado al vino","refs":["tit 1:7"]},{"sourceText":"πλήκτην","sourceLemma":"πλήκτης","strong":"G41310","targetText":"belicoso","refs":["tit 1:7"]},{"sourceText":"αἰσχροκερδῆ","sourceLemma":"αἰσχροκερδής","strong":"G01460","targetText":"codicioso de ganancias deshonestas","refs":["tit 1:7"]},{"sourceText":"ἀλλὰ","sourceLemma":"ἀλλά","strong":"G02350","targetText":"sino","refs":["tit 1:8","tit 3:5"]},{"sourceText":"φιλόξενον","sourceLemma":"φιλόξενος","strong":"G53820","targetText":"hospitalario","refs":["tit 1:8"]},{"sourceText":"φιλάγαθον","sourceLemma":"φιλάγαθος","strong":"G53580","targetText":"amante del bien","refs":["tit 1:8"]},{"sourceText":"σώφρονα","sourceLemma":"σώφρων","strong":"G49980","targetText":"prudente","refs":["tit 1:8"]},{"sourceText":"δίκαιον","sourceLemma":"δίκαιος","strong":"G13420","targetText":"justo","refs":["tit 1:8"]},{"sourceText":"ὅσιον","sourceLemma":"ὅσιος","strong":"G37410","targetText":"santo","refs":["tit 1:8"]},{"sourceText":"ἐγκρατῆ","sourceLemma":"ἐγκρατής","strong":"G14680","targetText":"dueño de sí mismo","refs":["tit 1:8"]},{"sourceText":"ἀντεχόμενον","sourceLemma":"ἀντέχω","strong":"G04720","targetText":"retenedor","refs":["tit 1:9"]},{"sourceText":"τοῦ","sourceLemma":"ὁ","strong":"G35880","targetText":"de la","refs":["tit 1:9"]},{"sourceText":"λόγου","sourceLemma":"λόγος","strong":"G30560","targetText":"palabra","refs":["tit 1:9"]},{"sourceText":"πιστοῦ","sourceLemma":"πιστός","strong":"G41030","targetText":"fiel","refs":["tit 1:9"]},{"sourceText":"τὴν","sourceLemma":"ὁ","strong":"G35880","targetText":"a la","refs":["tit 1:9","tit 2:12"]},{"sourceText":"διδαχὴν","sourceLemma":"διδαχή","strong":"G13220","targetText":"doctrina","refs":["tit 1:9"]},{"sourceText":"ᾖ","sourceLemma":"εἰμί","strong":"G15100","targetText":"sea","refs":["tit 1:9"]},{"sourceText":"δυνατὸς","sourceLemma":"δυνατός","strong":"G14150","targetText":"capaz","refs":["tit 1:9"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"también","refs":["tit 1:9","tit 1:10","tit 3:3","tit 3:8","tit 3:14"]},{"sourceText":"παρακαλεῖν","sourceLemma":"παρακαλέω","strong":"G38700","targetText":"de exhortar","refs":["tit 1:9"]},{"sourceText":"ἐν","sourceLemma":"ἐν","strong":"G17220","targetText":"con","refs":["tit 1:9"]},{"sourceText":"τῇ ὑγιαινούσῃ","sourceLemma":"ὁ ὑγιαίνω","strong":"G35880 G51980","targetText":"sana","refs":["tit 1:9"]},{"sourceText":"τῇ διδασκαλίᾳ","sourceLemma":"ὁ διδασκαλία","strong":"G35880 G13190","targetText":"enseñanza","refs":["tit 1:9"]},{"sourceText":"ἐλέγχειν","sourceLemma":"ἐλέγχω","strong":"G16510","targetText":"reprender","refs":["tit 1:9"]},{"sourceText":"τοὺς","sourceLemma":"ὁ","strong":"G35880","targetText":"a los","refs":["tit 1:9","tit 2:6"]},{"sourceText":"ἀντιλέγοντας","sourceLemma":"ἀντιλέγω","strong":"G04830","targetText":"que se oponen","refs":["tit 1:9"]},{"sourceText":"γὰρ","sourceLemma":"γάρ","strong":"G10630","targetText":"Ya que","refs":["tit 1:10"]},{"sourceText":"εἰσὶν","sourceLemma":"εἰμί","strong":"G15100","targetText":"hay","refs":["tit 1:10"]},{"sourceText":"πολλοὶ","sourceLemma":"πολλός","strong":"G41830","targetText":"muchos","refs":["tit 1:10"]},{"sourceText":"ἀνυπότακτοι","sourceLemma":"ἀνυπότακτος","strong":"G05060","targetText":"rebeldes","refs":["tit 1:10"]},{"sourceText":"ματαιολόγοι","sourceLemma":"ματαιολόγος","strong":"G31510","targetText":"habladores de vanidades","refs":["tit 1:10"]},{"sourceText":"φρεναπάται","sourceLemma":"φρεναπάτης","strong":"G54230","targetText":"engañadores","refs":["tit 1:10"]},{"sourceText":"μάλιστα","sourceLemma":"μάλιστα","strong":"G31220","targetText":"especialmente","refs":["tit 1:10"]},{"sourceText":"οἱ","sourceLemma":"ὁ","strong":"G35880","targetText":"los","refs":["tit 1:10","tit 3:8","tit 3:14"]},{"sourceText":"ἐκ","sourceLemma":"ἐκ","strong":"G15370","targetText":"de","refs":["tit 1:10"]},{"sourceText":"τῆς","sourceLemma":"ὁ","strong":"G35880","targetText":"la","refs":["tit 1:10"]},{"sourceText":"περιτομῆς","sourceLemma":"περιτομή","strong":"G40610","targetText":"circuncisión","refs":["tit 1:10"]},{"sourceText":"οὓς","sourceLemma":"ὅς","strong":"G37390","targetText":"a quienes","refs":["tit 1:11"]},{"sourceText":"ἐπιστομίζειν","sourceLemma":"ἐπιστομίζω","strong":"G19930","targetText":"mandar a callar","refs":["tit 1:11"]},{"sourceText":"οἵτινες","sourceLemma":"ὅστις","strong":"G37480","targetText":"los cuales","refs":["tit 1:11"]},{"sourceText":"ἀνατρέπουσιν","sourceLemma":"ἀνατρέπω","strong":"G03960","targetText":"trastornan","refs":["tit 1:11"]},{"sourceText":"οἴκους","sourceLemma":"οἶκος","strong":"G36240","targetText":"casas","refs":["tit 1:11"]},{"sourceText":"ὅλους","sourceLemma":"ὅλος","strong":"G36500","targetText":"enteras","refs":["tit 1:11"]},{"sourceText":"διδάσκοντες","sourceLemma":"διδάσκω","strong":"G13210","targetText":"enseñando","refs":["tit 1:11"]},{"sourceText":"ἃ","sourceLemma":"ὅς","strong":"G37390","targetText":"lo que","refs":["tit 1:11","tit 2:1"]},{"sourceText":"δεῖ","sourceLemma":"δέω","strong":"G12100","targetText":"se debe","refs":["tit 1:11"]},{"sourceText":"χάριν","sourceLemma":"χάριν","strong":"G54840","targetText":"por","refs":["tit 1:11"]},{"sourceText":"κέρδους","sourceLemma":"κέρδος","strong":"G27710","targetText":"ganancias","refs":["tit 1:11"]},{"sourceText":"αἰσχροῦ","sourceLemma":"αἰσχρός","strong":"G01500","targetText":"deshonestas","refs":["tit 1:11"]},{"sourceText":"τις","sourceLemma":"τις","strong":"G51000","targetText":"Uno","refs":["tit 1:12"]},{"sourceText":"ἐξ","sourceLemma":"ἐκ","strong":"G15370","targetText":"de","refs":["tit 1:12"]},{"sourceText":"αὐτῶν αὐτῶν","sourceLemma":"αὐτός αὐτός","strong":"G08460 G08460","targetText":"sus","refs":["tit 1:12"]},{"sourceText":"ἴδιος","sourceLemma":"ἴδιος","strong":"G23980","targetText":"propios","refs":["tit 1:12"]},{"sourceText":"προφήτης","sourceLemma":"προφήτης","strong":"G43960","targetText":"profetas","refs":["tit 1:12"]},{"sourceText":"εἶπέν","sourceLemma":"λέγω","strong":"G30040","targetText":"dijo","refs":["tit 1:12"]},{"sourceText":"Κρῆτες","sourceLemma":"Κρής","strong":"G29120","targetText":"Los cretenses","refs":["tit 1:12"]},{"sourceText":"ἀεὶ","sourceLemma":"ἀεί","strong":"G01040","targetText":"siempre","refs":["tit 1:12"]},{"sourceText":"ψεῦσται","sourceLemma":"ψεύστης","strong":"G55830","targetText":"mentirosos","refs":["tit 1:12"]},{"sourceText":"κακὰ","sourceLemma":"κακός","strong":"G25560","targetText":"malas","refs":["tit 1:12"]},{"sourceText":"θηρία","sourceLemma":"θηρίον","strong":"G23420","targetText":"bestias","refs":["tit 1:12"]},{"sourceText":"γαστέρες","sourceLemma":"γαστήρ","strong":"G10640","targetText":"vientres","refs":["tit 1:12"]},{"sourceText":"ἀργαί","sourceLemma":"ἀργός","strong":"G06920","targetText":"ociosos","refs":["tit 1:12"]},{"sourceText":"αὕτη","sourceLemma":"οὗτος","strong":"G37780","targetText":"Este","refs":["tit 1:13"]},{"sourceText":"ἡ μαρτυρία","sourceLemma":"ὁ μαρτυρία","strong":"G35880 G31410","targetText":"testimonio","refs":["tit 1:13"]},{"sourceText":"ἐστὶν","sourceLemma":"εἰμί","strong":"G15100","targetText":"es","refs":["tit 1:13"]},{"sourceText":"ἀληθής","sourceLemma":"ἀληθής","strong":"G02270","targetText":"confiable","refs":["tit 1:13"]},{"sourceText":"δι’","sourceLemma":"διά","strong":"G12230","targetText":"Por","refs":["tit 1:13"]},{"sourceText":"ἣν","sourceLemma":"ὅς","strong":"G37390","targetText":"esta","refs":["tit 1:13"]},{"sourceText":"αἰτίαν","sourceLemma":"αἰτία","strong":"G01560","targetText":"razón","refs":["tit 1:13"]},{"sourceText":"ἔλεγχε αὐτοὺς","sourceLemma":"ἐλέγχω αὐτός","strong":"G16510 G08460","targetText":"repréndelos","refs":["tit 1:13"]},{"sourceText":"ἀποτόμως","sourceLemma":"ἀποτόμως","strong":"G06640","targetText":"severamente","refs":["tit 1:13"]},{"sourceText":"ὑγιαίνωσιν","sourceLemma":"ὑγιαίνω","strong":"G51980","targetText":"sean sanos","refs":["tit 1:13"]},{"sourceText":"τῇ","sourceLemma":"ὁ","strong":"G35880","targetText":"la","refs":["tit 1:13","tit 2:1","tit 2:7"]},{"sourceText":"πίστει","sourceLemma":"πίστις","strong":"G41020","targetText":"fe","refs":["tit 1:13","tit 2:2"]},{"sourceText":"προσέχοντες","sourceLemma":"προσέχω","strong":"G43370","targetText":"prestando atención","refs":["tit 1:14"]},{"sourceText":"μύθοις","sourceLemma":"μῦθος","strong":"G34540","targetText":"a mitos","refs":["tit 1:14"]},{"sourceText":"Ἰουδαϊκοῖς","sourceLemma":"Ἰουδαϊκός","strong":"G24510","targetText":"judaicos","refs":["tit 1:14"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"ni","refs":["tit 1:14"]},{"sourceText":"ἐντολαῖς","sourceLemma":"ἐντολή","strong":"G17850","targetText":"a mandamientos","refs":["tit 1:14"]},{"sourceText":"ἀνθρώπων","sourceLemma":"ἄνθρωπος","strong":"G04440","targetText":"de hombres","refs":["tit 1:14"]},{"sourceText":"ἀποστρεφομένων","sourceLemma":"ἀποστρέφω","strong":"G06540","targetText":"que se desvían de","refs":["tit 1:14"]},{"sourceText":"τὴν","sourceLemma":"ὁ","strong":"G35880","targetText":"la","refs":["tit 1:14","tit 2:10","tit 2:10","tit 2:13"]},{"sourceText":"ἀλήθειαν","sourceLemma":"ἀλήθεια","strong":"G02250","targetText":"verdad","refs":["tit 1:14"]},{"sourceText":"πάντα","sourceLemma":"πᾶς","strong":"G39560","targetText":"Todas las cosas","refs":["tit 1:15"]},{"sourceText":"καθαρὰ","sourceLemma":"καθαρός","strong":"G25130","targetText":"son puras","refs":["tit 1:15"]},{"sourceText":"τοῖς","sourceLemma":"ὁ","strong":"G35880","targetText":"para aquellos","refs":["tit 1:15"]},{"sourceText":"καθαροῖς","sourceLemma":"καθαρός","strong":"G25130","targetText":"que son puros","refs":["tit 1:15"]},{"sourceText":"τοῖς","sourceLemma":"ὁ","strong":"G35880","targetText":"para los","refs":["tit 1:15"]},{"sourceText":"μεμιαμμένοις","sourceLemma":"μιαίνω","strong":"G33920","targetText":"corruptos","refs":["tit 1:15"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"e","refs":["tit 1:15"]},{"sourceText":"ἀπίστοις","sourceLemma":"ἄπιστος","strong":"G05710","targetText":"incrédulos","refs":["tit 1:15"]},{"sourceText":"οὐδὲν","sourceLemma":"οὐδείς","strong":"G37620","targetText":"nada","refs":["tit 1:15"]},{"sourceText":"καθαρόν","sourceLemma":"καθαρός","strong":"G25130","targetText":"es puro","refs":["tit 1:15"]},{"sourceText":"ἀλλὰ","sourceLemma":"ἀλλά","strong":"G02350","targetText":"por el contrario","refs":["tit 1:15"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"tanto","refs":["tit 1:15","tit 2:12"]},{"sourceText":"αὐτῶν","sourceLemma":"αὐτός","strong":"G08460","targetText":"sus","refs":["tit 1:15"]},{"sourceText":"ὁ νοῦς","sourceLemma":"ὁ νοῦς","strong":"G35880 G35630","targetText":"mentes","refs":["tit 1:15"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"como","refs":["tit 1:15","tit 1:16","tit 2:12"]},{"sourceText":"ἡ συνείδησις","sourceLemma":"ὁ συνείδησις","strong":"G35880 G48930","targetText":"sus conciencias","refs":["tit 1:15"]},{"sourceText":"μεμίανται","sourceLemma":"μιαίνω","strong":"G33920","targetText":"han sido corrompidas","refs":["tit 1:15"]},{"sourceText":"ὁμολογοῦσιν","sourceLemma":"ὁμολογέω","strong":"G36700","targetText":"Ellos profesan","refs":["tit 1:16"]},{"sourceText":"εἰδέναι","sourceLemma":"εἴδω","strong":"G14920","targetText":"conocer","refs":["tit 1:16"]},{"sourceText":"Θεὸν","sourceLemma":"θεός","strong":"G23160","targetText":"a Dios","refs":["tit 1:16"]},{"sourceText":"τοῖς","sourceLemma":"ὁ","strong":"G35880","targetText":"con sus","refs":["tit 1:16"]},{"sourceText":"ἔργοις","sourceLemma":"ἔργον","strong":"G20410","targetText":"acciones","refs":["tit 1:16"]},{"sourceText":"ἀρνοῦνται","sourceLemma":"ἀρνέομαι","strong":"G07200","targetText":"lo niegan","refs":["tit 1:16"]},{"sourceText":"ὄντες","sourceLemma":"εἰμί","strong":"G15100","targetText":"siendo","refs":["tit 1:16"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"tanto y","refs":["tit 1:16"]},{"sourceText":"βδελυκτοὶ","sourceLemma":"βδελυκτός","strong":"G09470","targetText":"detestables","refs":["tit 1:16"]},{"sourceText":"ἀπειθεῖς","sourceLemma":"ἀπειθής","strong":"G05450","targetText":"desobedientes","refs":["tit 1:16","tit 3:3"]},{"sourceText":"ἀδόκιμοι","sourceLemma":"ἀδόκιμος","strong":"G00960","targetText":"descalificados","refs":["tit 1:16"]},{"sourceText":"πρὸς","sourceLemma":"πρός","strong":"G43140","targetText":"para","refs":["tit 1:16","tit 3:1"]},{"sourceText":"πᾶν","sourceLemma":"πᾶς","strong":"G39560","targetText":"toda","refs":["tit 1:16","tit 3:1"]},{"sourceText":"ἀγαθὸν","sourceLemma":"ἀγαθός","strong":"G00180","targetText":"buena","refs":["tit 1:16","tit 3:1"]},{"sourceText":"ἔργον","sourceLemma":"ἔργον","strong":"G20410","targetText":"obra","refs":["tit 1:16","tit 3:1"]},{"sourceText":"δὲ","sourceLemma":"δέ","strong":"G11610","targetText":"Pero","refs":["tit 2:1","tit 3:4","tit 3:9"]},{"sourceText":"σὺ","sourceLemma":"σύ","strong":"G47710","targetText":"tú","refs":["tit 2:1"]},{"sourceText":"λάλει","sourceLemma":"λαλέω","strong":"G29800","targetText":"habla","refs":["tit 2:1","tit 2:15"]},{"sourceText":"πρέπει","sourceLemma":"πρέπω","strong":"G42410","targetText":"conviene a","refs":["tit 2:1"]},{"sourceText":"ὑγιαινούσῃ","sourceLemma":"ὑγιαίνω","strong":"G51980","targetText":"sana","refs":["tit 2:1"]},{"sourceText":"διδασκαλίᾳ","sourceLemma":"διδασκαλία","strong":"G13190","targetText":"doctrina","refs":["tit 2:1","tit 2:7"]},{"sourceText":"εἶναι","sourceLemma":"εἰμί","strong":"G15100","targetText":"que sean","refs":["tit 2:2","tit 2:9","tit 3:2"]},{"sourceText":"πρεσβύτας","sourceLemma":"πρεσβύτης","strong":"G42460","targetText":"los hombres mayores","refs":["tit 2:2"]},{"sourceText":"νηφαλίους","sourceLemma":"νηφάλιος","strong":"G35240","targetText":"sobrios","refs":["tit 2:2"]},{"sourceText":"σεμνούς","sourceLemma":"σεμνός","strong":"G45860","targetText":"dignos de respeto","refs":["tit 2:2"]},{"sourceText":"σώφρονας","sourceLemma":"σώφρων","strong":"G49980","targetText":"prudentes","refs":["tit 2:2"]},{"sourceText":"ὑγιαίνοντας","sourceLemma":"ὑγιαίνω","strong":"G51980","targetText":"sanos","refs":["tit 2:2"]},{"sourceText":"τῇ","sourceLemma":"ὁ","strong":"G35880","targetText":"en la","refs":["tit 2:2","tit 2:2"]},{"sourceText":"τῇ","sourceLemma":"ὁ","strong":"G35880","targetText":"en el","refs":["tit 2:2"]},{"sourceText":"ἀγάπῃ","sourceLemma":"ἀγάπη","strong":"G00260","targetText":"amor","refs":["tit 2:2"]},{"sourceText":"ὑπομονῇ","sourceLemma":"ὑπομονή","strong":"G52810","targetText":"paciencia","refs":["tit 2:2"]},{"sourceText":"ὡσαύτως","sourceLemma":"ὡσαύτως","strong":"G56150","targetText":"Del mismo modo","refs":["tit 2:3","tit 2:6"]},{"sourceText":"πρεσβύτιδας","sourceLemma":"πρεσβῦτις","strong":"G42470","targetText":"que las mujeres mayores sean","refs":["tit 2:3"]},{"sourceText":"ἱεροπρεπεῖς","sourceLemma":"ἱεροπρεπής","strong":"G24120","targetText":"reverentes","refs":["tit 2:3"]},{"sourceText":"ἐν","sourceLemma":"ἐν","strong":"G17220","targetText":"en su","refs":["tit 2:3"]},{"sourceText":"καταστήματι","sourceLemma":"κατάστημα","strong":"G26880","targetText":"comportamiento","refs":["tit 2:3"]},{"sourceText":"μὴ","sourceLemma":"μή","strong":"G33610","targetText":"No","refs":["tit 2:3"]},{"sourceText":"διαβόλους","sourceLemma":"διάβολος","strong":"G12280","targetText":"calumniadoras","refs":["tit 2:3"]},{"sourceText":"μηδὲ","sourceLemma":"μηδέ","strong":"G33660","targetText":"ni","refs":["tit 2:3"]},{"sourceText":"δεδουλωμένας","sourceLemma":"δουλόω","strong":"G14020","targetText":"esclavizadas","refs":["tit 2:3"]},{"sourceText":"οἴνῳ","sourceLemma":"οἶνος","strong":"G36310","targetText":"a vino","refs":["tit 2:3"]},{"sourceText":"πολλῷ","sourceLemma":"πολλός","strong":"G41830","targetText":"mucho","refs":["tit 2:3"]},{"sourceText":"καλοδιδασκάλους","sourceLemma":"καλοδιδάσκαλος","strong":"G25670","targetText":"que sean maestras de lo bueno","refs":["tit 2:3"]},{"sourceText":"σωφρονίζωσι","sourceLemma":"σωφρονίζω","strong":"G49940","targetText":"enseñen","refs":["tit 2:4"]},{"sourceText":"τὰς","sourceLemma":"ὁ","strong":"G35880","targetText":"a las","refs":["tit 2:4"]},{"sourceText":"νέας","sourceLemma":"νέος","strong":"G35010","targetText":"jóvenes","refs":["tit 2:4"]},{"sourceText":"εἶναι","sourceLemma":"εἰμί","strong":"G15100","targetText":"a que sean","refs":["tit 2:4"]},{"sourceText":"φιλάνδρους","sourceLemma":"φίλανδρος","strong":"G53620","targetText":"amadoras de sus esposos","refs":["tit 2:4"]},{"sourceText":"φιλοτέκνους","sourceLemma":"φιλότεκνος","strong":"G53880","targetText":"y amadoras de sus hijos","refs":["tit 2:4"]},{"sourceText":"σώφρονας","sourceLemma":"σώφρων","strong":"G49980","targetText":"que sean prudentes","refs":["tit 2:5"]},{"sourceText":"ἁγνάς","sourceLemma":"ἁγνός","strong":"G00530","targetText":"puras","refs":["tit 2:5"]},{"sourceText":"οἰκουργούς","sourceLemma":"οἰκουργός","strong":"G36260","targetText":"cuidadoras de su casa","refs":["tit 2:5"]},{"sourceText":"ἀγαθάς","sourceLemma":"ἀγαθός","strong":"G00180","targetText":"buenas","refs":["tit 2:5"]},{"sourceText":"ὑποτασσομένας","sourceLemma":"ὑποτάσσω","strong":"G52930","targetText":"sujetas","refs":["tit 2:5"]},{"sourceText":"τοῖς","sourceLemma":"ὁ","strong":"G35880","targetText":"a sus","refs":["tit 2:5"]},{"sourceText":"ἰδίοις","sourceLemma":"ἴδιος","strong":"G23980","targetText":"propios","refs":["tit 2:5"]},{"sourceText":"ἀνδράσιν","sourceLemma":"ἀνήρ","strong":"G04350","targetText":"maridos","refs":["tit 2:5"]},{"sourceText":"ὁ","sourceLemma":"ὁ","strong":"G35880","targetText":"la","refs":["tit 2:5"]},{"sourceText":"λόγος","sourceLemma":"λόγος","strong":"G30560","targetText":"Palabra","refs":["tit 2:5"]},{"sourceText":"τοῦ","sourceLemma":"ὁ","strong":"G35880","targetText":"de","refs":["tit 2:5","tit 2:11","tit 2:13"]},{"sourceText":"βλασφημῆται","sourceLemma":"βλασφημέω","strong":"G09870","targetText":"sea difamada","refs":["tit 2:5"]},{"sourceText":"παρακάλει","sourceLemma":"παρακαλέω","strong":"G38700","targetText":"exhorta","refs":["tit 2:6","tit 2:15"]},{"sourceText":"νεωτέρους","sourceLemma":"νεώτερος","strong":"G35125","targetText":"jóvenes","refs":["tit 2:6"]},{"sourceText":"σωφρονεῖν","sourceLemma":"σωφρονέω","strong":"G49930","targetText":"a pensar con sensatez","refs":["tit 2:6"]},{"sourceText":"παρεχόμενος","sourceLemma":"παρέχω","strong":"G39300","targetText":"presentándote","refs":["tit 2:7"]},{"sourceText":"σεαυτὸν","sourceLemma":"σεαυτοῦ","strong":"G45720","targetText":"a ti mismo","refs":["tit 2:7"]},{"sourceText":"περὶ","sourceLemma":"περί","strong":"G40120","targetText":"en","refs":["tit 2:7"]},{"sourceText":"πάντα","sourceLemma":"πᾶς","strong":"G39560","targetText":"todo","refs":["tit 2:7"]},{"sourceText":"τύπον","sourceLemma":"τύπος","strong":"G51790","targetText":"como ejemplo","refs":["tit 2:7"]},{"sourceText":"καλῶν","sourceLemma":"καλός","strong":"G25700","targetText":"de buenas","refs":["tit 2:7","tit 2:14"]},{"sourceText":"ἔργων","sourceLemma":"ἔργον","strong":"G20410","targetText":"obras","refs":["tit 2:7","tit 2:14","tit 3:5","tit 3:8"]},{"sourceText":"ἀφθορίαν","sourceLemma":"ἀφθορία","strong":"G08627","targetText":"como pureza","refs":["tit 2:7"]},{"sourceText":"σεμνότητα","sourceLemma":"σεμνότης","strong":"G45870","targetText":"como dignidad","refs":["tit 2:7"]},{"sourceText":"λόγον","sourceLemma":"λόγος","strong":"G30560","targetText":"como palabra","refs":["tit 2:8"]},{"sourceText":"ὑγιῆ","sourceLemma":"ὑγιής","strong":"G51990","targetText":"sana","refs":["tit 2:8"]},{"sourceText":"ἀκατάγνωστον","sourceLemma":"ἀκατάγνωστος","strong":"G01760","targetText":"e irreprochable","refs":["tit 2:8"]},{"sourceText":"ὁ ἐξ","sourceLemma":"ὁ ἐκ","strong":"G35880 G15370","targetText":"de parte del","refs":["tit 2:8"]},{"sourceText":"ἐναντίας","sourceLemma":"ἐναντίος","strong":"G17270","targetText":"adversario","refs":["tit 2:8"]},{"sourceText":"ἐντραπῇ","sourceLemma":"ἐντρέπω","strong":"G17880","targetText":"se avergüence","refs":["tit 2:8"]},{"sourceText":"μηδὲν","sourceLemma":"μηδείς","strong":"G33670","targetText":"no nada que","refs":["tit 2:8"]},{"sourceText":"ἔχων","sourceLemma":"ἔχω","strong":"G21920","targetText":"teniendo","refs":["tit 2:8"]},{"sourceText":"φαῦλον","sourceLemma":"φαῦλος","strong":"G53370","targetText":"malo","refs":["tit 2:8"]},{"sourceText":"λέγειν","sourceLemma":"λέγω","strong":"G30040","targetText":"decir","refs":["tit 2:8"]},{"sourceText":"περὶ","sourceLemma":"περί","strong":"G40120","targetText":"acerca de","refs":["tit 2:8","tit 3:8"]},{"sourceText":"ἡμῶν","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nosotros","refs":["tit 2:8","tit 2:14"]},{"sourceText":"ὑποτάσσεσθαι","sourceLemma":"ὑποτάσσω","strong":"G52930","targetText":"Que se sometan","refs":["tit 2:9"]},{"sourceText":"δούλους","sourceLemma":"δοῦλος","strong":"G14010","targetText":"los siervos","refs":["tit 2:9"]},{"sourceText":"ἰδίοις","sourceLemma":"ἴδιος","strong":"G23980","targetText":"a sus","refs":["tit 2:9"]},{"sourceText":"δεσπόταις","sourceLemma":"δεσπότης","strong":"G12030","targetText":"amos","refs":["tit 2:9"]},{"sourceText":"πᾶσιν","sourceLemma":"πᾶς","strong":"G39560","targetText":"todo","refs":["tit 2:9","tit 2:10"]},{"sourceText":"εὐαρέστους","sourceLemma":"εὐάρεστος","strong":"G21010","targetText":"agradables","refs":["tit 2:9"]},{"sourceText":"ἀντιλέγοντας","sourceLemma":"ἀντιλέγω","strong":"G04830","targetText":"contradiciendo","refs":["tit 2:9"]},{"sourceText":"μὴ νοσφιζομένους","sourceLemma":"μή νοσφίζω","strong":"G33610 G35570","targetText":"que no roben","refs":["tit 2:10"]},{"sourceText":"ἀλλὰ","sourceLemma":"ἀλλά","strong":"G02350","targetText":"sino que","refs":["tit 2:10"]},{"sourceText":"ἐνδεικνυμένους","sourceLemma":"ἐνδείκνυμι","strong":"G17310","targetText":"demuestren","refs":["tit 2:10"]},{"sourceText":"πᾶσαν","sourceLemma":"πᾶς","strong":"G39560","targetText":"toda","refs":["tit 2:10","tit 3:2"]},{"sourceText":"ἀγαθήν","sourceLemma":"ἀγαθός","strong":"G00180","targetText":"buena","refs":["tit 2:10"]},{"sourceText":"κοσμῶσιν","sourceLemma":"κοσμέω","strong":"G28850","targetText":"adornen","refs":["tit 2:10"]},{"sourceText":"διδασκαλίαν","sourceLemma":"διδασκαλία","strong":"G13190","targetText":"doctrina","refs":["tit 2:10"]},{"sourceText":"ἡ","sourceLemma":"ὁ","strong":"G35880","targetText":"la","refs":["tit 2:11","tit 3:4"]},{"sourceText":"χάρις","sourceLemma":"χάρις","strong":"G54850","targetText":"gracia","refs":["tit 2:11","tit 3:15"]},{"sourceText":"σωτήριος","sourceLemma":"σωτήριος","strong":"G49920","targetText":"salvadora","refs":["tit 2:11"]},{"sourceText":"ἐπεφάνη","sourceLemma":"ἐπιφαίνω","strong":"G20140","targetText":"fue manifestada","refs":["tit 2:11","tit 3:4"]},{"sourceText":"πᾶσιν","sourceLemma":"πᾶς","strong":"G39560","targetText":"a todos","refs":["tit 2:11"]},{"sourceText":"ἀνθρώποις","sourceLemma":"ἄνθρωπος","strong":"G04440","targetText":"los hombres","refs":["tit 2:11","tit 3:8"]},{"sourceText":"παιδεύουσα ἡμᾶς","sourceLemma":"παιδεύω ἐγώ","strong":"G38110 G14730","targetText":"instruyéndonos","refs":["tit 2:12"]},{"sourceText":"ἀρνησάμενοι","sourceLemma":"ἀρνέομαι","strong":"G07200","targetText":"después de haber renunciado","refs":["tit 2:12"]},{"sourceText":"ἀσέβειαν","sourceLemma":"ἀσέβεια","strong":"G07630","targetText":"impiedad","refs":["tit 2:12"]},{"sourceText":"τὰς","sourceLemma":"ὁ","strong":"G35880","targetText":"a los","refs":["tit 2:12"]},{"sourceText":"ἐπιθυμίας","sourceLemma":"ἐπιθυμία","strong":"G19390","targetText":"deseos","refs":["tit 2:12"]},{"sourceText":"κοσμικὰς","sourceLemma":"κοσμικός","strong":"G28860","targetText":"mundanos","refs":["tit 2:12"]},{"sourceText":"ζήσωμεν","sourceLemma":"ζάω","strong":"G21980","targetText":"vivamos","refs":["tit 2:12"]},{"sourceText":"τῷ","sourceLemma":"ὁ","strong":"G35880","targetText":"el","refs":["tit 2:12"]},{"sourceText":"αἰῶνι","sourceLemma":"αἰών","strong":"G01650","targetText":"tiempo","refs":["tit 2:12"]},{"sourceText":"νῦν","sourceLemma":"νῦν","strong":"G35680","targetText":"presente","refs":["tit 2:12"]},{"sourceText":"σωφρόνως","sourceLemma":"σωφρόνως","strong":"G49960","targetText":"de una forma autocontrolada","refs":["tit 2:12"]},{"sourceText":"δικαίως","sourceLemma":"δικαίως","strong":"G13460","targetText":"justa","refs":["tit 2:12"]},{"sourceText":"εὐσεβῶς","sourceLemma":"εὐσεβῶς","strong":"G21530","targetText":"piadosa","refs":["tit 2:12"]},{"sourceText":"προσδεχόμενοι","sourceLemma":"προσδέχομαι","strong":"G43270","targetText":"mientras esperamos","refs":["tit 2:13"]},{"sourceText":"ἐλπίδα","sourceLemma":"ἐλπίς","strong":"G16800","targetText":"esperanza","refs":["tit 2:13"]},{"sourceText":"μακαρίαν","sourceLemma":"μακάριος","strong":"G31070","targetText":"bienaventurada","refs":["tit 2:13"]},{"sourceText":"ἐπιφάνειαν","sourceLemma":"ἐπιφάνεια","strong":"G20150","targetText":"la manifestación","refs":["tit 2:13"]},{"sourceText":"τῆς","sourceLemma":"ὁ","strong":"G35880","targetText":"de la","refs":["tit 2:13"]},{"sourceText":"δόξης","sourceLemma":"δόξα","strong":"G13910","targetText":"gloria","refs":["tit 2:13"]},{"sourceText":"μεγάλου","sourceLemma":"μέγας","strong":"G31730","targetText":"gran","refs":["tit 2:13"]},{"sourceText":"Σωτῆρος","sourceLemma":"σωτήρ","strong":"G49900","targetText":"salvador","refs":["tit 2:13","tit 3:4"]},{"sourceText":"Ἰησοῦ Χριστοῦ","sourceLemma":"Ἰησοῦς χριστός","strong":"G24240 G55470","targetText":"Jesucristo","refs":["tit 2:13","tit 3:6"]},{"sourceText":"ὃς","sourceLemma":"ὅς","strong":"G37390","targetText":"quien","refs":["tit 2:14"]},{"sourceText":"ἔδωκεν","sourceLemma":"δίδωμι","strong":"G13250","targetText":"se dio","refs":["tit 2:14"]},{"sourceText":"ἑαυτὸν","sourceLemma":"ἑαυτοῦ","strong":"G14380","targetText":"a sí mismo","refs":["tit 2:14"]},{"sourceText":"ὑπὲρ","sourceLemma":"ὑπέρ","strong":"G52280","targetText":"por","refs":["tit 2:14"]},{"sourceText":"λυτρώσηται","sourceLemma":"λυτρόω","strong":"G30840","targetText":"Él redimiese","refs":["tit 2:14"]},{"sourceText":"ἡμᾶς","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nos nos","refs":["tit 2:14"]},{"sourceText":"πάσης","sourceLemma":"πᾶς","strong":"G39560","targetText":"toda","refs":["tit 2:14","tit 2:15"]},{"sourceText":"ἀνομίας","sourceLemma":"ἀνομία","strong":"G04580","targetText":"iniquidad","refs":["tit 2:14"]},{"sourceText":"καθαρίσῃ","sourceLemma":"καθαρίζω","strong":"G25110","targetText":"purifique","refs":["tit 2:14"]},{"sourceText":"ἑαυτῷ","sourceLemma":"ἑαυτοῦ","strong":"G14380","targetText":"para sí mismo","refs":["tit 2:14"]},{"sourceText":"λαὸν","sourceLemma":"λαός","strong":"G29920","targetText":"como un pueblo","refs":["tit 2:14"]},{"sourceText":"περιούσιον","sourceLemma":"περιούσιος","strong":"G40410","targetText":"elegido","refs":["tit 2:14"]},{"sourceText":"ζηλωτὴν","sourceLemma":"ζηλωτής","strong":"G22070","targetText":"celoso","refs":["tit 2:14"]},{"sourceText":"ταῦτα","sourceLemma":"οὗτος","strong":"G37780","targetText":"Estas cosas","refs":["tit 2:15"]},{"sourceText":"ἔλεγχε","sourceLemma":"ἐλέγχω","strong":"G16510","targetText":"reprende","refs":["tit 2:15"]},{"sourceText":"μετὰ","sourceLemma":"μετά","strong":"G33260","targetText":"con","refs":["tit 2:15"]},{"sourceText":"ἐπιταγῆς","sourceLemma":"ἐπιταγή","strong":"G20030","targetText":"autoridad","refs":["tit 2:15"]},{"sourceText":"μηδείς","sourceLemma":"μηδείς","strong":"G33670","targetText":"Nadie","refs":["tit 2:15"]},{"sourceText":"σου","sourceLemma":"σύ","strong":"G47710","targetText":"te","refs":["tit 2:15"]},{"sourceText":"περιφρονείτω","sourceLemma":"περιφρονέω","strong":"G40650","targetText":"menosprecie","refs":["tit 2:15"]},{"sourceText":"ὑπομίμνῃσκε αὐτοὺς","sourceLemma":"ὑπομιμνῄσκω αὐτός","strong":"G52790 G08460","targetText":"Recuérdales","refs":["tit 3:1"]},{"sourceText":"ὑποτάσσεσθαι","sourceLemma":"ὑποτάσσω","strong":"G52930","targetText":"que se sometan","refs":["tit 3:1"]},{"sourceText":"ἀρχαῖς","sourceLemma":"ἀρχή","strong":"G07460","targetText":"a los gobernantes","refs":["tit 3:1"]},{"sourceText":"ἐξουσίαις","sourceLemma":"ἐξουσία","strong":"G18490","targetText":"y a las autoridades","refs":["tit 3:1"]},{"sourceText":"πειθαρχεῖν","sourceLemma":"πειθαρχέω","strong":"G39800","targetText":"que obedezcan","refs":["tit 3:1"]},{"sourceText":"εἶναι","sourceLemma":"εἰμί","strong":"G15100","targetText":"que estén","refs":["tit 3:1"]},{"sourceText":"ἑτοίμους","sourceLemma":"ἕτοιμος","strong":"G20920","targetText":"dispuestos","refs":["tit 3:1"]},{"sourceText":"βλασφημεῖν","sourceLemma":"βλασφημέω","strong":"G09870","targetText":"que hablen mal","refs":["tit 3:2"]},{"sourceText":"μηδένα","sourceLemma":"μηδείς","strong":"G33670","targetText":"no de nadie","refs":["tit 3:2"]},{"sourceText":"ἀμάχους","sourceLemma":"ἄμαχος","strong":"G02690","targetText":"pacíficos","refs":["tit 3:2"]},{"sourceText":"ἐπιεικεῖς","sourceLemma":"ἐπιεικής","strong":"G19330","targetText":"amables","refs":["tit 3:2"]},{"sourceText":"ἐνδεικνυμένους","sourceLemma":"ἐνδείκνυμι","strong":"G17310","targetText":"mostrando","refs":["tit 3:2"]},{"sourceText":"πραΰτητα","sourceLemma":"πραΰτης","strong":"G42400","targetText":"consideración","refs":["tit 3:2"]},{"sourceText":"πρὸς","sourceLemma":"πρός","strong":"G43140","targetText":"para con","refs":["tit 3:2"]},{"sourceText":"πάντας","sourceLemma":"πᾶς","strong":"G39560","targetText":"todos","refs":["tit 3:2"]},{"sourceText":"ἀνθρώπους","sourceLemma":"ἄνθρωπος","strong":"G04440","targetText":"los hombres","refs":["tit 3:2"]},{"sourceText":"γάρ","sourceLemma":"γάρ","strong":"G10630","targetText":"Porque","refs":["tit 3:3"]},{"sourceText":"ἡμεῖς","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nosotros","refs":["tit 3:3"]},{"sourceText":"ποτε","sourceLemma":"ποτέ","strong":"G42180","targetText":"en otro tiempo","refs":["tit 3:3"]},{"sourceText":"ἦμεν","sourceLemma":"εἰμί","strong":"G15100","targetText":"eramos","refs":["tit 3:3"]},{"sourceText":"ἀνόητοι","sourceLemma":"ἀνόητος","strong":"G04530","targetText":"necios","refs":["tit 3:3"]},{"sourceText":"πλανώμενοι","sourceLemma":"πλανάω","strong":"G41050","targetText":"extraviados","refs":["tit 3:3"]},{"sourceText":"δουλεύοντες","sourceLemma":"δουλεύω","strong":"G13980","targetText":"siendo esclavos","refs":["tit 3:3"]},{"sourceText":"ἐπιθυμίαις","sourceLemma":"ἐπιθυμία","strong":"G19390","targetText":"de placeres","refs":["tit 3:3"]},{"sourceText":"ποικίλαις","sourceLemma":"ποικίλος","strong":"G41640","targetText":"diversos","refs":["tit 3:3"]},{"sourceText":"ἡδοναῖς","sourceLemma":"ἡδονή","strong":"G22370","targetText":"pasiones","refs":["tit 3:3"]},{"sourceText":"διάγοντες","sourceLemma":"διάγω","strong":"G12360","targetText":"viviendo","refs":["tit 3:3"]},{"sourceText":"κακίᾳ","sourceLemma":"κακία","strong":"G25490","targetText":"malicia","refs":["tit 3:3"]},{"sourceText":"φθόνῳ","sourceLemma":"φθόνος","strong":"G53550","targetText":"envidia","refs":["tit 3:3"]},{"sourceText":"στυγητοί","sourceLemma":"στυγητός","strong":"G47670","targetText":"aborrecibles","refs":["tit 3:3"]},{"sourceText":"μισοῦντες","sourceLemma":"μισέω","strong":"G34040","targetText":"odiándonos","refs":["tit 3:3"]},{"sourceText":"ἀλλήλους","sourceLemma":"ἀλλήλων","strong":"G02400","targetText":"unos a otros","refs":["tit 3:3"]},{"sourceText":"ὅτε","sourceLemma":"ὅτε","strong":"G37530","targetText":"cuando","refs":["tit 3:4"]},{"sourceText":"χρηστότης","sourceLemma":"χρηστότης","strong":"G55440","targetText":"bondad","refs":["tit 3:4"]},{"sourceText":"ἡ","sourceLemma":"ὁ","strong":"G35880","targetText":"el","refs":["tit 3:4"]},{"sourceText":"φιλανθρωπία","sourceLemma":"φιλανθρωπία","strong":"G53630","targetText":"amor por la humanidad","refs":["tit 3:4"]},{"sourceText":"τοῦ","sourceLemma":"ὁ","strong":"G35880","targetText":"de parte de","refs":["tit 3:4"]},{"sourceText":"ἡμᾶς","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nos","refs":["tit 3:5","tit 3:15"]},{"sourceText":"ἔσωσεν","sourceLemma":"σῴζω","strong":"G49820","targetText":"salvó","refs":["tit 3:5"]},{"sourceText":"οὐκ","sourceLemma":"οὐ","strong":"G37560","targetText":"no","refs":["tit 3:5"]},{"sourceText":"ἐξ","sourceLemma":"ἐκ","strong":"G15370","targetText":"por","refs":["tit 3:5"]},{"sourceText":"τῶν ἐν δικαιοσύνῃ","sourceLemma":"ὁ ἐν δικαιοσύνη","strong":"G35880 G17220 G13430","targetText":"de justicia","refs":["tit 3:5"]},{"sourceText":"ἃ","sourceLemma":"ὅς","strong":"G37390","targetText":"que","refs":["tit 3:5"]},{"sourceText":"ἐποιήσαμεν ἡμεῖς","sourceLemma":"ποιέω ἐγώ","strong":"G41600 G14730","targetText":"hubiéramos hecho","refs":["tit 3:5"]},{"sourceText":"κατὰ","sourceLemma":"κατά","strong":"G25960","targetText":"por","refs":["tit 3:5"]},{"sourceText":"τὸ ἔλεος","sourceLemma":"ὁ ἔλεος","strong":"G35880 G16560","targetText":"misericordia","refs":["tit 3:5"]},{"sourceText":"διὰ","sourceLemma":"διά","strong":"G12230","targetText":"por medio del","refs":["tit 3:5"]},{"sourceText":"λουτροῦ","sourceLemma":"λουτρόν","strong":"G30670","targetText":"lavamiento","refs":["tit 3:5"]},{"sourceText":"παλινγενεσίας","sourceLemma":"παλινγενεσία","strong":"G38240","targetText":"de la regeneración","refs":["tit 3:5"]},{"sourceText":"ἀνακαινώσεως","sourceLemma":"ἀνακαίνωσις","strong":"G03420","targetText":"la renovación","refs":["tit 3:5"]},{"sourceText":"Πνεύματος","sourceLemma":"πνεῦμα","strong":"G41510","targetText":"del Espíritu","refs":["tit 3:5"]},{"sourceText":"Ἁγίου","sourceLemma":"ἅγιος","strong":"G00400","targetText":"Santo","refs":["tit 3:5"]},{"sourceText":"οὗ","sourceLemma":"ὅς","strong":"G37390","targetText":"el cual","refs":["tit 3:6"]},{"sourceText":"ἐξέχεεν","sourceLemma":"ἐκχέω","strong":"G16320","targetText":"derramó","refs":["tit 3:6"]},{"sourceText":"ἐφ’","sourceLemma":"ἐπί","strong":"G19090","targetText":"sobre","refs":["tit 3:6"]},{"sourceText":"ἡμᾶς","sourceLemma":"ἐγώ","strong":"G14730","targetText":"nosotros","refs":["tit 3:6"]},{"sourceText":"πλουσίως","sourceLemma":"πλουσίως","strong":"G41460","targetText":"ricamente","refs":["tit 3:6"]},{"sourceText":"διὰ","sourceLemma":"διά","strong":"G12230","targetText":"por medio de","refs":["tit 3:6"]},{"sourceText":"τοῦ Σωτῆρος","sourceLemma":"ὁ σωτήρ","strong":"G35880 G49900","targetText":"Salvador","refs":["tit 3:6"]},{"sourceText":"δικαιωθέντες","sourceLemma":"δικαιόω","strong":"G13440","targetText":"habiendo sido justificados","refs":["tit 3:7"]},{"sourceText":"τῇ","sourceLemma":"ὁ","strong":"G35880","targetText":"por","refs":["tit 3:7"]},{"sourceText":"ἐκείνου","sourceLemma":"ἐκεῖνος","strong":"G15650","targetText":"aquella su","refs":["tit 3:7"]},{"sourceText":"χάριτι","sourceLemma":"χάρις","strong":"G54850","targetText":"gracia","refs":["tit 3:7"]},{"sourceText":"γενηθῶμεν","sourceLemma":"γίνομαι","strong":"G10960","targetText":"lleguemos a ser","refs":["tit 3:7"]},{"sourceText":"κληρονόμοι","sourceLemma":"κληρονόμος","strong":"G28180","targetText":"herederos","refs":["tit 3:7"]},{"sourceText":"κατ’","sourceLemma":"κατά","strong":"G25960","targetText":"conforme a","refs":["tit 3:7"]},{"sourceText":"ἐλπίδα","sourceLemma":"ἐλπίς","strong":"G16800","targetText":"la esperanza","refs":["tit 3:7"]},{"sourceText":"ὁ","sourceLemma":"ὁ","strong":"G35880","targetText":"Este","refs":["tit 3:8"]},{"sourceText":"λόγος","sourceLemma":"λόγος","strong":"G30560","targetText":"mensaje","refs":["tit 3:8"]},{"sourceText":"πιστὸς","sourceLemma":"πιστός","strong":"G41030","targetText":"es digno de confianza","refs":["tit 3:8"]},{"sourceText":"βούλομαί","sourceLemma":"βούλομαι","strong":"G10140","targetText":"quiero que","refs":["tit 3:8"]},{"sourceText":"σε","sourceLemma":"σύ","strong":"G47710","targetText":"tú","refs":["tit 3:8"]},{"sourceText":"διαβεβαιοῦσθαι","sourceLemma":"διαβεβαιόομαι","strong":"G12260","targetText":"insistas","refs":["tit 3:8"]},{"sourceText":"τούτων","sourceLemma":"οὗτος","strong":"G37780","targetText":"estas cosas","refs":["tit 3:8"]},{"sourceText":"πεπιστευκότες","sourceLemma":"πιστεύω","strong":"G41000","targetText":"que han creído","refs":["tit 3:8"]},{"sourceText":"Θεῷ","sourceLemma":"θεός","strong":"G23160","targetText":"en Dios","refs":["tit 3:8"]},{"sourceText":"φροντίζωσιν","sourceLemma":"φροντίζω","strong":"G54310","targetText":"se preocupen en","refs":["tit 3:8"]},{"sourceText":"προΐστασθαι","sourceLemma":"προΐστημι","strong":"G42910","targetText":"practicar","refs":["tit 3:8"]},{"sourceText":"καλῶν","sourceLemma":"καλός","strong":"G25700","targetText":"buenas","refs":["tit 3:8","tit 3:14"]},{"sourceText":"ταῦτά","sourceLemma":"οὗτος","strong":"G37780","targetText":"Estas cosas","refs":["tit 3:8"]},{"sourceText":"ἐστιν","sourceLemma":"εἰμί","strong":"G15100","targetText":"son","refs":["tit 3:8"]},{"sourceText":"καλὰ","sourceLemma":"καλός","strong":"G25700","targetText":"buenas","refs":["tit 3:8"]},{"sourceText":"ὠφέλιμα","sourceLemma":"ὠφέλιμος","strong":"G56240","targetText":"beneficiosas","refs":["tit 3:8"]},{"sourceText":"τοῖς","sourceLemma":"ὁ","strong":"G35880","targetText":"para","refs":["tit 3:8"]},{"sourceText":"περιΐστασο","sourceLemma":"περιΐστημι","strong":"G40260","targetText":"evita","refs":["tit 3:9"]},{"sourceText":"ζητήσεις","sourceLemma":"ζήτησις","strong":"G22140","targetText":"discusiones","refs":["tit 3:9"]},{"sourceText":"μωρὰς","sourceLemma":"μωρός","strong":"G34740","targetText":"necias","refs":["tit 3:9"]},{"sourceText":"γενεαλογίας","sourceLemma":"γενεαλογία","strong":"G10760","targetText":"genealogías","refs":["tit 3:9"]},{"sourceText":"καὶ","sourceLemma":"καί","strong":"G25320","targetText":"así como","refs":["tit 3:9"]},{"sourceText":"ἔρεις","sourceLemma":"ἔρις","strong":"G20540","targetText":"las contiendas","refs":["tit 3:9"]},{"sourceText":"μάχας","sourceLemma":"μάχη","strong":"G31630","targetText":"las disputas","refs":["tit 3:9"]},{"sourceText":"νομικὰς","sourceLemma":"νομικός","strong":"G35440","targetText":"acerca de la ley","refs":["tit 3:9"]},{"sourceText":"γὰρ","sourceLemma":"γάρ","strong":"G10630","targetText":"porque","refs":["tit 3:9","tit 3:12"]},{"sourceText":"εἰσὶν","sourceLemma":"εἰμί","strong":"G15100","targetText":"son","refs":["tit 3:9"]},{"sourceText":"ἀνωφελεῖς","sourceLemma":"ἀνωφελής","strong":"G05120","targetText":"inútiles","refs":["tit 3:9"]},{"sourceText":"μάταιοι","sourceLemma":"μάταιος","strong":"G31520","targetText":"sin valor","refs":["tit 3:9"]},{"sourceText":"ἄνθρωπον","sourceLemma":"ἄνθρωπος","strong":"G04440","targetText":"Al hombre","refs":["tit 3:10"]},{"sourceText":"αἱρετικὸν","sourceLemma":"αἱρετικός","strong":"G01410","targetText":"que cause divisiones","refs":["tit 3:10"]},{"sourceText":"μετὰ","sourceLemma":"μετά","strong":"G33260","targetText":"después de","refs":["tit 3:10"]},{"sourceText":"μίαν","sourceLemma":"εἷς","strong":"G15200","targetText":"la primera","refs":["tit 3:10"]},{"sourceText":"δευτέραν","sourceLemma":"δεύτερος","strong":"G12080","targetText":"segunda","refs":["tit 3:10"]},{"sourceText":"νουθεσίαν","sourceLemma":"νουθεσία","strong":"G35590","targetText":"amonestación","refs":["tit 3:10"]},{"sourceText":"παραιτοῦ","sourceLemma":"παραιτέομαι","strong":"G38680","targetText":"recházalo","refs":["tit 3:10"]},{"sourceText":"ὅτι","sourceLemma":"ὅτι","strong":"G37540","targetText":"puesto que","refs":["tit 3:11"]},{"sourceText":"εἰδὼς","sourceLemma":"εἴδω","strong":"G14920","targetText":"sabes","refs":["tit 3:11"]},{"sourceText":"ἐξέστραπται","sourceLemma":"ἐκστρέφω","strong":"G16120","targetText":"que se ha pervertido","refs":["tit 3:11"]},{"sourceText":"ὁ","sourceLemma":"ὁ","strong":"G35880","targetText":"el","refs":["tit 3:11"]},{"sourceText":"τοιοῦτος","sourceLemma":"τοιοῦτος","strong":"G51080","targetText":"tal","refs":["tit 3:11"]},{"sourceText":"ἁμαρτάνει","sourceLemma":"ἁμαρτάνω","strong":"G02640","targetText":"peca","refs":["tit 3:11"]},{"sourceText":"ὢν","sourceLemma":"εἰμί","strong":"G15100","targetText":"de modo que","refs":["tit 3:11"]},{"sourceText":"αὐτοκατάκριτος","sourceLemma":"αὐτοκατάκριτος","strong":"G08430","targetText":"se condena a sí mismo","refs":["tit 3:11"]},{"sourceText":"ὅταν","sourceLemma":"ὅταν","strong":"G37520","targetText":"Cuando","refs":["tit 3:12"]},{"sourceText":"πρὸς σὲ","sourceLemma":"πρός σύ","strong":"G43140 G47710","targetText":"te","refs":["tit 3:12"]},{"sourceText":"πέμψω","sourceLemma":"πέμπω","strong":"G39920","targetText":"envíe","refs":["tit 3:12"]},{"sourceText":"Ἀρτεμᾶν","sourceLemma":"Ἀρτεμᾶς","strong":"G07340","targetText":"a Artemas","refs":["tit 3:12"]},{"sourceText":"Τυχικόν","sourceLemma":"Τυχικός","strong":"G51900","targetText":"Tíquico","refs":["tit 3:12"]},{"sourceText":"σπούδασον","sourceLemma":"σπουδάζω","strong":"G47040","targetText":"haz todo lo posible para","refs":["tit 3:12"]},{"sourceText":"ἐλθεῖν","sourceLemma":"ἔρχομαι","strong":"G20640","targetText":"venir","refs":["tit 3:12"]},{"sourceText":"πρός","sourceLemma":"πρός","strong":"G43140","targetText":"a","refs":["tit 3:12"]},{"sourceText":"με","sourceLemma":"ἐγώ","strong":"G14730","targetText":"mí","refs":["tit 3:12"]},{"sourceText":"εἰς","sourceLemma":"εἰς","strong":"G15190","targetText":"hasta","refs":["tit 3:12"]},{"sourceText":"Νικόπολιν","sourceLemma":"Νικόπολις","strong":"G35330","targetText":"Nicopolis","refs":["tit 3:12"]},{"sourceText":"κέκρικα","sourceLemma":"κρίνω","strong":"G29190","targetText":"he decidido","refs":["tit 3:12"]},{"sourceText":"παραχειμάσαι","sourceLemma":"παραχειμάζω","strong":"G39140","targetText":"pasar el invierno","refs":["tit 3:12"]},{"sourceText":"ἐκεῖ","sourceLemma":"ἐκεῖ","strong":"G15630","targetText":"allí","refs":["tit 3:12"]},{"sourceText":"πρόπεμψον","sourceLemma":"προπέμπω","strong":"G43110","targetText":"Provee lo necesario para el viaje","refs":["tit 3:13"]},{"sourceText":"σπουδαίως","sourceLemma":"σπουδαίως","strong":"G47090","targetText":"diligentemente","refs":["tit 3:13"]},{"sourceText":"Ζηνᾶν","sourceLemma":"Ζηνᾶς","strong":"G22110","targetText":"a Zenas","refs":["tit 3:13"]},{"sourceText":"νομικὸν","sourceLemma":"νομικός","strong":"G35440","targetText":"intérprete de la ley","refs":["tit 3:13"]},{"sourceText":"Ἀπολλῶν","sourceLemma":"Ἀπολλῶς","strong":"G06250","targetText":"a Apolos","refs":["tit 3:13"]},{"sourceText":"μηδὲν","sourceLemma":"μηδείς","strong":"G33670","targetText":"no nada","refs":["tit 3:13"]},{"sourceText":"αὐτοῖς","sourceLemma":"αὐτός","strong":"G08460","targetText":"les","refs":["tit 3:13"]},{"sourceText":"λείπῃ","sourceLemma":"λείπω","strong":"G30070","targetText":"falte","refs":["tit 3:13"]},{"sourceText":"δὲ","sourceLemma":"δέ","strong":"G11610","targetText":"Y","refs":["tit 3:14"]},{"sourceText":"μανθανέτωσαν","sourceLemma":"μανθάνω","strong":"G31290","targetText":"que aprendan","refs":["tit 3:14"]},{"sourceText":"ἡμέτεροι","sourceLemma":"ἡμέτερος","strong":"G22510","targetText":"nuestros","refs":["tit 3:14"]},{"sourceText":"προΐστασθαι","sourceLemma":"προΐστημι","strong":"G42910","targetText":"a practicar","refs":["tit 3:14"]},{"sourceText":"ἔργων","sourceLemma":"ἔργον","strong":"G20410","targetText":"las obras","refs":["tit 3:14"]},{"sourceText":"εἰς","sourceLemma":"εἰς","strong":"G15190","targetText":"para","refs":["tit 3:14"]},{"sourceText":"τὰς","sourceLemma":"ὁ","strong":"G35880","targetText":"las","refs":["tit 3:14"]},{"sourceText":"χρείας","sourceLemma":"χρεία","strong":"G55320","targetText":"necesidades","refs":["tit 3:14"]},{"sourceText":"ἀναγκαίας","sourceLemma":"ἀναγκαῖος","strong":"G03160","targetText":"urgentes","refs":["tit 3:14"]},{"sourceText":"ὦσιν","sourceLemma":"εἰμί","strong":"G15100","targetText":"estén","refs":["tit 3:14"]},{"sourceText":"ἄκαρποι","sourceLemma":"ἄκαρπος","strong":"G01750","targetText":"sin frutos","refs":["tit 3:14"]},{"sourceText":"σε","sourceLemma":"σύ","strong":"G47710","targetText":"Te","refs":["tit 3:15"]},{"sourceText":"ἀσπάζονταί","sourceLemma":"ἀσπάζομαι","strong":"G07820","targetText":"saludan","refs":["tit 3:15"]},{"sourceText":"πάντες","sourceLemma":"πᾶς","strong":"G39560","targetText":"todos","refs":["tit 3:15"]},{"sourceText":"οἱ","sourceLemma":"ὁ","strong":"G35880","targetText":"los que","refs":["tit 3:15"]},{"sourceText":"μετ’ ἐμοῦ","sourceLemma":"μετά ἐγώ","strong":"G33260 G14730","targetText":"están conmigo","refs":["tit 3:15"]},{"sourceText":"ἄσπασαι","sourceLemma":"ἀσπάζομαι","strong":"G07820","targetText":"Saluda","refs":["tit 3:15"]},{"sourceText":"τοὺς","sourceLemma":"ὁ","strong":"G35880","targetText":"a los que","refs":["tit 3:15"]},{"sourceText":"φιλοῦντας","sourceLemma":"φιλέω","strong":"G53680","targetText":"aman","refs":["tit 3:15"]},{"sourceText":"πίστει","sourceLemma":"πίστις","strong":"G41020","targetText":"la fe","refs":["tit 3:15"]},{"sourceText":"ἡ","sourceLemma":"ὁ","strong":"G35880","targetText":"La","refs":["tit 3:15"]},{"sourceText":"μετὰ","sourceLemma":"μετά","strong":"G33260","targetText":"esté con","refs":["tit 3:15"]},{"sourceText":"πάντων","sourceLemma":"πᾶς","strong":"G39560","targetText":"todos","refs":["tit 3:15"]},{"sourceText":"ὑμῶν","sourceLemma":"σύ","strong":"G47710","targetText":"ustedes","refs":["tit 3:15"]}],"lemmaAlignments":{"alignments":{"Παῦλος":[0],"δοῦλος":[1,283],"θεός":[2,20,50,199,419],"δέ":[3,26,212,477],"ἀπόστολος":[4],"Ἰησοῦς χριστός":[5,323],"κατά":[6,13,37,44,66,388,409],"πίστις":[7,46,170,496],"ἐκλεκτός":[8],"καί":[9,115,174,186,191,194,204,432],"ἐπίγνωσις":[10],"ἀλήθεια":[11,179],"ὁ":[12,62,91,108,111,121,130,132,169,178,182,184,200,224,225,241,251,254,256,296,305,309,319,378,380,404,411,427,450,483,491,494,497],"εὐσέβεια":[14],"ἐπί":[15,398],"ἐλπίς":[16,316,410],"ζωή":[17],"αἰώνιος":[18,25],"ὅς":[19,34,134,141,164,324,386,396],"ὁ ἀψευδής":[21],"ἐπαγγέλλω":[22],"πρό":[23],"χρόνος":[24],"φανερόω":[27],"καιρός":[28],"ἴδιος":[29,149,252,284],"αὐτός":[30,192,475],"ὁ λόγος":[31],"ἐν":[32,58,82,117,231],"κήρυγμα":[33],"ἐγώ":[35,39,69,281,329,361,381,399,463],"πιστεύω":[36,418],"ἐπιταγή":[38,340],"ὁ σωτήρ":[40,402],"Τίτος":[41],"γνήσιος":[42],"τέκνον":[43,80],"κοινός":[45],"χάρις":[47,297,406],"εἰρήνη":[48],"ἀπό":[49],"πατήρ":[51],"χριστός":[52],"Ἰησοῦς":[53],"χάριν":[54,143],"οὗτος":[55,159,337,417,423],"σύ":[56,70,213,342,415,488,500],"ἀπολίπω":[57],"Κρήτη":[59],"ἵνα":[60],"ἐπιδιορθόω":[61],"λείπω":[63,476],"καθίστημι":[64],"πρεσβύτερος":[65],"πόλις":[67],"ὡς":[68],"διατάσσω":[71],"εἰ":[72],"τις":[73,146],"εἰμί":[74,90,113,124,161,203,218,243,349,363,424,437,453,486],"ἀνέγκλητος":[75,93],"ἀνήρ":[76,253],"εἷς":[77,443],"γυνή":[78],"ἔχω":[79,277],"πιστός":[81,110,413],"μή":[83,233],"κατηγορία":[84],"ἀσωτία":[85],"ἤ":[86],"ἀνυπότακτος":[87,126],"γάρ":[88,123,360,436],"δέω":[89,142],"ἐπίσκοπος":[92],"οἰκονόμος":[94],"αὐθάδης":[95],"ὀργίλος":[96],"πάροινος":[97],"πλήκτης":[98],"αἰσχροκερδής":[99],"ἀλλά":[100,190,290],"φιλόξενος":[101],"φιλάγαθος":[102],"σώφρων":[103,222,246],"δίκαιος":[104],"ὅσιος":[105],"ἐγκρατής":[106],"ἀντέχω":[107],"λόγος":[109,255,270,412],"διδαχή":[112],"δυνατός":[114],"παρακαλέω":[116,258],"ὁ ὑγιαίνω":[118],"ὁ διδασκαλία":[119],"ἐλέγχω":[120,338],"ἀντιλέγω":[122,288],"πολλός":[125,238],"ματαιολόγος":[127],"φρεναπάτης":[128],"μάλιστα":[129],"ἐκ":[131,147,384],"περιτομή":[133],"ἐπιστομίζω":[135],"ὅστις":[136],"ἀνατρέπω":[137],"οἶκος":[138],"ὅλος":[139],"διδάσκω":[140],"κέρδος":[144],"αἰσχρός":[145],"αὐτός αὐτός":[148],"προφήτης":[150],"λέγω":[151,279],"Κρής":[152],"ἀεί":[153],"ψεύστης":[154],"κακός":[155],"θηρίον":[156],"γαστήρ":[157],"ἀργός":[158],"ὁ μαρτυρία":[160],"ἀληθής":[162],"διά":[163,390,401],"αἰτία":[165],"ἐλέγχω αὐτός":[166],"ἀποτόμως":[167],"ὑγιαίνω":[168,216,223],"προσέχω":[171],"μῦθος":[172],"Ἰουδαϊκός":[173],"ἐντολή":[175],"ἄνθρωπος":[176,301,359,440],"ἀποστρέφω":[177],"πᾶς":[180,209,264,286,292,300,330,358,490,499],"καθαρός":[181,183,189],"μιαίνω":[185,196],"ἄπιστος":[187],"οὐδείς":[188],"ὁ νοῦς":[193],"ὁ συνείδησις":[195],"ὁμολογέω":[197],"εἴδω":[198,448],"ἔργον":[201,211,267,481],"ἀρνέομαι":[202,303],"βδελυκτός":[205],"ἀπειθής":[206],"ἀδόκιμος":[207],"πρός":[208,357,462],"ἀγαθός":[210,249,293],"λαλέω":[214],"πρέπω":[215],"διδασκαλία":[217,295],"πρεσβύτης":[219],"νηφάλιος":[220],"σεμνός":[221],"ἀγάπη":[226],"ὑπομονή":[227],"ὡσαύτως":[228],"πρεσβῦτις":[229],"ἱεροπρεπής":[230],"κατάστημα":[232],"διάβολος":[234],"μηδέ":[235],"δουλόω":[236],"οἶνος":[237],"καλοδιδάσκαλος":[239],"σωφρονίζω":[240],"νέος":[242],"φίλανδρος":[244],"φιλότεκνος":[245],"ἁγνός":[247],"οἰκουργός":[248],"ὑποτάσσω":[250,282,345],"βλασφημέω":[257,351],"νεώτερος":[259],"σωφρονέω":[260],"παρέχω":[261],"σεαυτοῦ":[262],"περί":[263,280],"τύπος":[265],"καλός":[266,422,425],"ἀφθορία":[268],"σεμνότης":[269],"ὑγιής":[271],"ἀκατάγνωστος":[272],"ὁ ἐκ":[273],"ἐναντίος":[274],"ἐντρέπω":[275],"μηδείς":[276,341,352,474],"φαῦλος":[278],"δεσπότης":[285],"εὐάρεστος":[287],"μή νοσφίζω":[289],"ἐνδείκνυμι":[291,355],"κοσμέω":[294],"σωτήριος":[298],"ἐπιφαίνω":[299],"παιδεύω ἐγώ":[302],"ἀσέβεια":[304],"ἐπιθυμία":[306,367],"κοσμικός":[307],"ζάω":[308],"αἰών":[310],"νῦν":[311],"σωφρόνως":[312],"δικαίως":[313],"εὐσεβῶς":[314],"προσδέχομαι":[315],"μακάριος":[317],"ἐπιφάνεια":[318],"δόξα":[320],"μέγας":[321],"σωτήρ":[322],"δίδωμι":[325],"ἑαυτοῦ":[326,333],"ὑπέρ":[327],"λυτρόω":[328],"ἀνομία":[331],"καθαρίζω":[332],"λαός":[334],"περιούσιος":[335],"ζηλωτής":[336],"μετά":[339,442,498],"περιφρονέω":[343],"ὑπομιμνῄσκω αὐτός":[344],"ἀρχή":[346],"ἐξουσία":[347],"πειθαρχέω":[348],"ἕτοιμος":[350],"ἄμαχος":[353],"ἐπιεικής":[354],"πραΰτης":[356],"ποτέ":[362],"ἀνόητος":[364],"πλανάω":[365],"δουλεύω":[366],"ποικίλος":[368],"ἡδονή":[369],"διάγω":[370],"κακία":[371],"φθόνος":[372],"στυγητός":[373],"μισέω":[374],"ἀλλήλων":[375],"ὅτε":[376],"χρηστότης":[377],"φιλανθρωπία":[379],"σῴζω":[382],"οὐ":[383],"ὁ ἐν δικαιοσύνη":[385],"ποιέω ἐγώ":[387],"ὁ ἔλεος":[389],"λουτρόν":[391],"παλινγενεσία":[392],"ἀνακαίνωσις":[393],"πνεῦμα":[394],"ἅγιος":[395],"ἐκχέω":[397],"πλουσίως":[400],"δικαιόω":[403],"ἐκεῖνος":[405],"γίνομαι":[407],"κληρονόμος":[408],"βούλομαι":[414],"διαβεβαιόομαι":[416],"φροντίζω":[420],"προΐστημι":[421,480],"ὠφέλιμος":[426],"περιΐστημι":[428],"ζήτησις":[429],"μωρός":[430],"γενεαλογία":[431],"ἔρις":[433],"μάχη":[434],"νομικός":[435,472],"ἀνωφελής":[438],"μάταιος":[439],"αἱρετικός":[441],"δεύτερος":[444],"νουθεσία":[445],"παραιτέομαι":[446],"ὅτι":[447],"ἐκστρέφω":[449],"τοιοῦτος":[451],"ἁμαρτάνω":[452],"αὐτοκατάκριτος":[454],"ὅταν":[455],"πρός σύ":[456],"πέμπω":[457],"Ἀρτεμᾶς":[458],"Τυχικός":[459],"σπουδάζω":[460],"ἔρχομαι":[461],"εἰς":[464,482],"Νικόπολις":[465],"κρίνω":[466],"παραχειμάζω":[467],"ἐκεῖ":[468],"προπέμπω":[469],"σπουδαίως":[470],"Ζηνᾶς":[471],"Ἀπολλῶς":[473],"μανθάνω":[478],"ἡμέτερος":[479],"χρεία":[484],"ἀναγκαῖος":[485],"ἄκαρπος":[487],"ἀσπάζομαι":[489,493],"μετά ἐγώ":[492],"φιλέω":[495]},"keys":["ἀγαθός","ἀγάπη","ἅγιος","ἁγνός","ἀδόκιμος","ἀεί","αἱρετικός","αἰσχροκερδής","αἰσχρός","αἰτία","αἰών","αἰώνιος","ἄκαρπος","ἀκατάγνωστος","ἀλήθεια","ἀληθής","ἀλλά","ἀλλήλων","ἁμαρτάνω","ἄμαχος","ἀναγκαῖος","ἀνακαίνωσις","ἀνατρέπω","ἀνέγκλητος","ἀνήρ","ἄνθρωπος","ἀνόητος","ἀνομία","ἀντέχω","ἀντιλέγω","ἀνυπότακτος","ἀνωφελής","ἀπειθής","ἄπιστος","ἀπό","ἀπολίπω","Ἀπολλῶς","ἀπόστολος","ἀποστρέφω","ἀποτόμως","ἀργός","ἀρνέομαι","Ἀρτεμᾶς","ἀρχή","ἀσέβεια","ἀσπάζομαι","ἀσωτία","αὐθάδης","αὐτοκατάκριτος","αὐτός","αὐτός αὐτός","ἀφθορία","βδελυκτός","βλασφημέω","βούλομαι","γάρ","γαστήρ","γενεαλογία","γίνομαι","γνήσιος","γυνή","δέ","δεσπότης","δεύτερος","δέω","διά","διαβεβαιόομαι","διάβολος","διάγω","διατάσσω","διδασκαλία","διδάσκω","διδαχή","δίδωμι","δίκαιος","δικαιόω","δικαίως","δόξα","δουλεύω","δοῦλος","δουλόω","δυνατός","ἑαυτοῦ","ἐγκρατής","ἐγώ","εἰ","εἴδω","εἰμί","εἰρήνη","εἷς","εἰς","ἐκ","ἐκεῖ","ἐκεῖνος","ἐκλεκτός","ἐκστρέφω","ἐκχέω","ἐλέγχω","ἐλέγχω αὐτός","ἐλπίς","ἐν","ἐναντίος","ἐνδείκνυμι","ἐντολή","ἐντρέπω","ἐξουσία","ἐπαγγέλλω","ἐπί","ἐπίγνωσις","ἐπιδιορθόω","ἐπιεικής","ἐπιθυμία","ἐπίσκοπος","ἐπιστομίζω","ἐπιταγή","ἐπιφαίνω","ἐπιφάνεια","ἔργον","ἔρις","ἔρχομαι","ἕτοιμος","εὐάρεστος","εὐσέβεια","εὐσεβῶς","ἔχω","ζάω","ζηλωτής","Ζηνᾶς","ζήτησις","ζωή","ἤ","ἡδονή","ἡμέτερος","θεός","θηρίον","ἴδιος","ἱεροπρεπής","Ἰησοῦς","Ἰησοῦς χριστός","ἵνα","Ἰουδαϊκός","καθαρίζω","καθαρός","καθίστημι","καί","καιρός","κακία","κακός","καλοδιδάσκαλος","καλός","κατά","κατάστημα","κατηγορία","κέρδος","κήρυγμα","κληρονόμος","κοινός","κοσμέω","κοσμικός","Κρής","Κρήτη","κρίνω","λαλέω","λαός","λέγω","λείπω","λόγος","λουτρόν","λυτρόω","μακάριος","μάλιστα","μανθάνω","ματαιολόγος","μάταιος","μάχη","μέγας","μετά","μετά ἐγώ","μή","μή νοσφίζω","μηδέ","μηδείς","μιαίνω","μισέω","μῦθος","μωρός","νέος","νεώτερος","νηφάλιος","Νικόπολις","νομικός","νουθεσία","νῦν","ὁ","ὁ ἀψευδής","ὁ διδασκαλία","ὁ ἐκ","ὁ ἔλεος","ὁ ἐν δικαιοσύνη","ὁ λόγος","ὁ μαρτυρία","ὁ νοῦς","ὁ συνείδησις","ὁ σωτήρ","ὁ ὑγιαίνω","οἰκονόμος","οἶκος","οἰκουργός","οἶνος","ὅλος","ὁμολογέω","ὀργίλος","ὅς","ὅσιος","ὅστις","ὅταν","ὅτε","ὅτι","οὐ","οὐδείς","οὗτος","παιδεύω ἐγώ","παλινγενεσία","παραιτέομαι","παρακαλέω","παραχειμάζω","παρέχω","πάροινος","πᾶς","πατήρ","Παῦλος","πειθαρχέω","πέμπω","περί","περιΐστημι","περιούσιος","περιτομή","περιφρονέω","πιστεύω","πίστις","πιστός","πλανάω","πλήκτης","πλουσίως","πνεῦμα","ποιέω ἐγώ","ποικίλος","πόλις","πολλός","ποτέ","πραΰτης","πρέπω","πρεσβύτερος","πρεσβύτης","πρεσβῦτις","πρό","προΐστημι","προπέμπω","πρός","πρός σύ","προσδέχομαι","προσέχω","προφήτης","σεαυτοῦ","σεμνός","σεμνότης","σπουδάζω","σπουδαίως","στυγητός","σύ","σῴζω","σωτήρ","σωτήριος","σωφρονέω","σωφρονίζω","σωφρόνως","σώφρων","τέκνον","τις","Τίτος","τοιοῦτος","τύπος","Τυχικός","ὑγιαίνω","ὑγιής","ὑπέρ","ὑπομιμνῄσκω αὐτός","ὑπομονή","ὑποτάσσω","φανερόω","φαῦλος","φθόνος","φιλάγαθος","φίλανδρος","φιλανθρωπία","φιλέω","φιλόξενος","φιλότεκνος","φρεναπάτης","φροντίζω","χάριν","χάρις","χρεία","χρηστότης","χριστός","χρόνος","ψεύστης","ὡς","ὡσαύτως","ὠφέλιμος"]},"targetAlignments":{"alignments":{"Pablo":[0],"siervo":[1],"de Dios":[2],"y":[3,9],"apóstol":[4],"de Jesucristo":[5],"conforme":[6,37],"a la fe":[7],"de los elegidos":[8],"al conocimiento":[10],"de la verdad":[11],"que":[12,34,386],"es de acuerdo con":[13],"la piedad":[14],"sobre":[15,398],"la esperanza":[16,410],"de la vida":[17],"eterna":[18],"la cual":[19],"Dios":[20,50],"que no miente":[21],"prometió":[22],"antes":[23],"de los tiempos":[24],"de los siglos":[25],"pero":[26],"reveló":[27],"en los tiempos":[28],"apropiados":[29],"su":[30],"palabra":[31,109],"por":[32,143,327,384,388,404],"la predicación":[33],"me":[35],"fue confiada":[36],"al mandato":[38],"nuestro":[39],"salvador":[40,322],"a Tito":[41],"verdadero":[42],"hijo":[43],"conforme a":[44,409],"nuestra común":[45],"fe":[46,170],"Gracia":[47],"paz":[48],"de":[49,131,147,256],"Padre":[51],"de Cristo":[52],"Jesús":[53],"Por causa":[54],"esta":[55,164],"te":[56,70,342,456],"dejé":[57],"en":[58,263],"Creta":[59],"para que":[60],"pusieras en orden":[61],"lo":[62],"que falta":[63],"designaras":[64],"ancianos":[65],"según":[66],"la ciudad":[67],"como":[68,194],"yo":[69],"ordené":[71],"si":[72],"alguno":[73],"es":[74,161],"irreprensible":[75,93],"esposo":[76],"de una sola":[77],"mujer":[78],"que tenga":[79],"hijos":[80],"fieles":[81],"que estén en":[82],"no":[83,383],"acusación":[84],"de libertinaje":[85],"o":[86],"rebeldía":[87],"Porque":[88,360],"es necesario":[89],"que sea":[90],"el":[91,309,378,450],"obispo":[92],"administrador":[94],"arrogante":[95],"iracundo":[96],"dado al vino":[97],"belicoso":[98],"codicioso de ganancias deshonestas":[99],"sino":[100],"hospitalario":[101],"amante del bien":[102],"prudente":[103],"justo":[104],"santo":[105],"dueño de sí mismo":[106],"retenedor":[107],"de la":[108,319],"fiel":[110],"a la":[111],"doctrina":[112,217,295],"sea":[113],"capaz":[114],"también":[115],"de exhortar":[116],"con":[117,339],"sana":[118,216,271],"enseñanza":[119],"reprender":[120],"a los":[121,305],"que se oponen":[122],"Ya que":[123],"hay":[124],"muchos":[125],"rebeldes":[126],"habladores de vanidades":[127],"engañadores":[128],"especialmente":[129],"los":[130],"la":[132,169,178,254,296],"circuncisión":[133],"a quienes":[134],"mandar a callar":[135],"los cuales":[136],"trastornan":[137],"casas":[138],"enteras":[139],"enseñando":[140],"lo que":[141],"se debe":[142],"ganancias":[144],"deshonestas":[145],"Uno":[146],"sus":[148,192],"propios":[149,252],"profetas":[150],"dijo":[151],"Los cretenses":[152],"siempre":[153],"mentirosos":[154],"malas":[155],"bestias":[156],"vientres":[157],"ociosos":[158],"Este":[159,411],"testimonio":[160],"confiable":[162],"Por":[163],"razón":[165],"repréndelos":[166],"severamente":[167],"sean sanos":[168],"prestando atención":[171],"a mitos":[172],"judaicos":[173],"ni":[174,235],"a mandamientos":[175],"de hombres":[176],"que se desvían de":[177],"verdad":[179],"Todas las cosas":[180],"son puras":[181],"para aquellos":[182],"que son puros":[183],"para los":[184],"corruptos":[185],"e":[186],"incrédulos":[187],"nada":[188],"es puro":[189],"por el contrario":[190],"tanto":[191],"mentes":[193],"sus conciencias":[195],"han sido corrompidas":[196],"Ellos profesan":[197],"conocer":[198],"a Dios":[199],"con sus":[200],"acciones":[201],"lo niegan":[202],"siendo":[203],"tanto y":[204],"detestables":[205],"desobedientes":[206],"descalificados":[207],"para":[208,427,482],"toda":[209,292,330],"buena":[210,293],"obra":[211],"Pero":[212],"tú":[213,415],"habla":[214],"conviene a":[215],"que sean":[218],"los hombres mayores":[219],"sobrios":[220],"dignos de respeto":[221],"prudentes":[222],"sanos":[223],"en la":[224],"en el":[225],"amor":[226],"paciencia":[227],"Del mismo modo":[228],"que las mujeres mayores sean":[229],"reverentes":[230],"en su":[231],"comportamiento":[232],"No":[233],"calumniadoras":[234],"esclavizadas":[236],"a vino":[237],"mucho":[238],"que sean maestras de lo bueno":[239],"enseñen":[240],"a las":[241],"jóvenes":[242,259],"a que sean":[243],"amadoras de sus esposos":[244],"y amadoras de sus hijos":[245],"que sean prudentes":[246],"puras":[247],"cuidadoras de su casa":[248],"buenas":[249,422,425],"sujetas":[250],"a sus":[251,284],"maridos":[253],"Palabra":[255],"sea difamada":[257],"exhorta":[258],"a pensar con sensatez":[260],"presentándote":[261],"a ti mismo":[262],"todo":[264,286],"como ejemplo":[265],"de buenas":[266],"obras":[267],"como pureza":[268],"como dignidad":[269],"como palabra":[270],"e irreprochable":[272],"de parte del":[273],"adversario":[274],"se avergüence":[275],"no nada que":[276],"teniendo":[277],"malo":[278],"decir":[279],"acerca de":[280],"nosotros":[281,361,399],"Que se sometan":[282],"los siervos":[283],"amos":[285],"agradables":[287],"contradiciendo":[288],"que no roben":[289],"sino que":[290],"demuestren":[291],"adornen":[294],"gracia":[297,406],"salvadora":[298],"fue manifestada":[299],"a todos":[300],"los hombres":[301,359],"instruyéndonos":[302],"después de haber renunciado":[303],"impiedad":[304],"deseos":[306],"mundanos":[307],"vivamos":[308],"tiempo":[310],"presente":[311],"de una forma autocontrolada":[312],"justa":[313],"piadosa":[314],"mientras esperamos":[315],"esperanza":[316],"bienaventurada":[317],"la manifestación":[318],"gloria":[320],"gran":[321],"Jesucristo":[323],"quien":[324],"se dio":[325],"a sí mismo":[326],"Él redimiese":[328],"nos nos":[329],"iniquidad":[331],"purifique":[332],"para sí mismo":[333],"como un pueblo":[334],"elegido":[335],"celoso":[336],"Estas cosas":[337,423],"reprende":[338],"autoridad":[340],"Nadie":[341],"menosprecie":[343],"Recuérdales":[344],"que se sometan":[345],"a los gobernantes":[346],"y a las autoridades":[347],"que obedezcan":[348],"que estén":[349],"dispuestos":[350],"que hablen mal":[351],"no de nadie":[352],"pacíficos":[353],"amables":[354],"mostrando":[355],"consideración":[356],"para con":[357],"todos":[358,490,499],"en otro tiempo":[362],"eramos":[363],"necios":[364],"extraviados":[365],"siendo esclavos":[366],"de placeres":[367],"diversos":[368],"pasiones":[369],"viviendo":[370],"malicia":[371],"envidia":[372],"aborrecibles":[373],"odiándonos":[374],"unos a otros":[375],"cuando":[376],"bondad":[377],"amor por la humanidad":[379],"de parte de":[380],"nos":[381],"salvó":[382],"de justicia":[385],"hubiéramos hecho":[387],"misericordia":[389],"por medio del":[390],"lavamiento":[391],"de la regeneración":[392],"la renovación":[393],"del Espíritu":[394],"Santo":[395],"el cual":[396],"derramó":[397],"ricamente":[400],"por medio de":[401],"Salvador":[402],"habiendo sido justificados":[403],"aquella su":[405],"lleguemos a ser":[407],"herederos":[408],"mensaje":[412],"es digno de confianza":[413],"quiero que":[414],"insistas":[416],"estas cosas":[417],"que han creído":[418],"en Dios":[419],"se preocupen en":[420],"practicar":[421],"son":[424,437],"beneficiosas":[426],"evita":[428],"discusiones":[429],"necias":[430],"genealogías":[431],"así como":[432],"las contiendas":[433],"las disputas":[434],"acerca de la ley":[435],"porque":[436],"inútiles":[438],"sin valor":[439],"Al hombre":[440],"que cause divisiones":[441],"después de":[442],"la primera":[443],"segunda":[444],"amonestación":[445],"recházalo":[446],"puesto que":[447],"sabes":[448],"que se ha pervertido":[449],"tal":[451],"peca":[452],"de modo que":[453],"se condena a sí mismo":[454],"Cuando":[455],"envíe":[457],"a Artemas":[458],"Tíquico":[459],"haz todo lo posible para":[460],"venir":[461],"a":[462],"mí":[463],"hasta":[464],"Nicopolis":[465],"he decidido":[466],"pasar el invierno":[467],"allí":[468],"Provee lo necesario para el viaje":[469],"diligentemente":[470],"a Zenas":[471],"intérprete de la ley":[472],"a Apolos":[473],"no nada":[474],"les":[475],"falte":[476],"Y":[477],"que aprendan":[478],"nuestros":[479],"a practicar":[480],"las obras":[481],"las":[483],"necesidades":[484],"urgentes":[485],"estén":[486],"sin frutos":[487],"Te":[488],"saludan":[489],"los que":[491],"están conmigo":[492],"Saluda":[493],"a los que":[494],"aman":[495],"la fe":[496],"La":[497],"esté con":[498],"ustedes":[500]},"keys":["a","a Apolos","a Artemas","a Dios","a la","a la fe","a las","a los","a los gobernantes","a los que","a mandamientos","a mitos","a pensar con sensatez","a practicar","a que sean","a quienes","a sí mismo","a sus","a ti mismo","a Tito","a todos","a vino","a Zenas","aborrecibles","acciones","acerca de","acerca de la ley","acusación","administrador","adornen","adversario","agradables","al conocimiento","Al hombre","al mandato","alguno","allí","amables","amadoras de sus esposos","aman","amante del bien","amonestación","amor","amor por la humanidad","amos","ancianos","antes","apóstol","apropiados","aquella su","arrogante","así como","autoridad","belicoso","beneficiosas","bestias","bienaventurada","bondad","buena","buenas","calumniadoras","capaz","casas","celoso","circuncisión","codicioso de ganancias deshonestas","como","como dignidad","como ejemplo","como palabra","como pureza","como un pueblo","comportamiento","con","con sus","confiable","conforme","conforme a","conocer","consideración","contradiciendo","conviene a","corruptos","Creta","cuando","Cuando","cuidadoras de su casa","dado al vino","de","de buenas","de Cristo","de Dios","de exhortar","de hombres","de Jesucristo","de justicia","de la","de la regeneración","de la verdad","de la vida","de libertinaje","de los elegidos","de los siglos","de los tiempos","de modo que","de parte de","de parte del","de placeres","de una forma autocontrolada","de una sola","decir","dejé","del Espíritu","Del mismo modo","demuestren","derramó","descalificados","deseos","deshonestas","designaras","desobedientes","después de","después de haber renunciado","detestables","dignos de respeto","dijo","diligentemente","Dios","discusiones","dispuestos","diversos","doctrina","dueño de sí mismo","e","e irreprochable","el","el cual","Él redimiese","elegido","Ellos profesan","en","en Dios","en el","en la","en los tiempos","en otro tiempo","en su","engañadores","enseñando","enseñanza","enseñen","enteras","envidia","envíe","eramos","es","es de acuerdo con","es digno de confianza","es necesario","es puro","esclavizadas","especialmente","esperanza","esposo","esta","están conmigo","Estas cosas","estas cosas","Este","esté con","estén","eterna","evita","exhorta","extraviados","falte","fe","fiel","fieles","fue confiada","fue manifestada","ganancias","genealogías","gloria","Gracia","gracia","gran","habiendo sido justificados","habla","habladores de vanidades","han sido corrompidas","hasta","hay","haz todo lo posible para","he decidido","herederos","hijo","hijos","hospitalario","hubiéramos hecho","impiedad","incrédulos","iniquidad","insistas","instruyéndonos","intérprete de la ley","inútiles","iracundo","irreprensible","Jesucristo","Jesús","jóvenes","judaicos","justa","justo","la","La","la ciudad","la cual","la esperanza","la fe","la manifestación","la piedad","la predicación","la primera","la renovación","las","las contiendas","las disputas","las obras","lavamiento","les","lleguemos a ser","lo","lo niegan","lo que","los","Los cretenses","los cuales","los hombres","los hombres mayores","los que","los siervos","malas","malicia","malo","mandar a callar","maridos","me","menosprecie","mensaje","mentes","mentirosos","mí","mientras esperamos","misericordia","mostrando","mucho","muchos","mujer","mundanos","nada","Nadie","necesidades","necias","necios","ni","Nicopolis","no","No","no de nadie","no nada","no nada que","nos","nos nos","nosotros","nuestra común","nuestro","nuestros","o","obispo","obra","obras","ociosos","odiándonos","ordené","Pablo","paciencia","pacíficos","Padre","palabra","Palabra","para","para aquellos","para con","para los","para que","para sí mismo","pasar el invierno","pasiones","paz","peca","pero","Pero","piadosa","por","Por","Por causa","por el contrario","por medio de","por medio del","Porque","porque","practicar","presentándote","presente","prestando atención","profetas","prometió","propios","Provee lo necesario para el viaje","prudente","prudentes","puesto que","puras","purifique","pusieras en orden","que","que aprendan","que cause divisiones","que estén","que estén en","que falta","que hablen mal","que han creído","que las mujeres mayores sean","que no miente","que no roben","que obedezcan","que se desvían de","que se ha pervertido","que se oponen","Que se sometan","que se sometan","que sea","que sean","que sean maestras de lo bueno","que sean prudentes","que son puros","que tenga","quien","quiero que","razón","rebeldes","rebeldía","recházalo","Recuérdales","reprende","repréndelos","reprender","retenedor","reveló","reverentes","ricamente","sabes","Saluda","saludan","salvador","Salvador","salvadora","salvó","sana","sanos","santo","Santo","se avergüence","se condena a sí mismo","se debe","se dio","se preocupen en","sea","sea difamada","sean sanos","según","segunda","severamente","si","siempre","siendo","siendo esclavos","siervo","sin frutos","sin valor","sino","sino que","sobre","sobrios","son","son puras","su","sujetas","sus","sus conciencias","tal","también","tanto","tanto y","te","Te","teniendo","testimonio","tiempo","Tíquico","toda","Todas las cosas","todo","todos","trastornan","tú","Uno","unos a otros","urgentes","ustedes","venir","verdad","verdadero","vientres","vivamos","viviendo","y","Y","y a las autoridades","y amadoras de sus hijos","Ya que","yo"]},"sourceAlignments":{"alignments":{"Παῦλος":[0],"δοῦλος":[1],"Θεοῦ":[2,50],"δὲ":[3,26,212,477],"ἀπόστολος":[4],"Ἰησοῦ Χριστοῦ":[5,323],"κατὰ":[6,44,66,388],"πίστιν":[7,46],"ἐκλεκτῶν":[8],"καὶ":[9,115,174,186,191,194,204,432],"ἐπίγνωσιν":[10],"ἀληθείας":[11],"τῆς":[12,132,319],"κατ’":[13,37,409],"εὐσέβειαν":[14],"ἐπ’":[15],"ἐλπίδι":[16],"ζωῆς":[17],"αἰωνίου":[18],"ἣν":[19,164],"Θεὸς":[20],"ὁ ἀψευδὴς":[21],"ἐπηγγείλατο":[22],"πρὸ":[23],"χρόνων":[24],"αἰωνίων":[25],"ἐφανέρωσεν":[27],"καιροῖς":[28],"ἰδίοις":[29,252,284],"αὐτοῦ":[30],"τὸν λόγον":[31],"ἐν":[32,58,82,117,231],"κηρύγματι":[33],"ὃ":[34],"ἐγὼ":[35],"ἐπιστεύθην":[36],"ἐπιταγὴν":[38],"ἡμῶν":[39,281],"τοῦ Σωτῆρος":[40,402],"Τίτῳ":[41],"γνησίῳ":[42],"τέκνῳ":[43],"κοινὴν":[45],"χάρις":[47,297],"εἰρήνη":[48],"ἀπὸ":[49],"Πατρὸς":[51],"Χριστοῦ":[52],"Ἰησοῦ":[53],"χάριν":[54,143],"τούτου":[55],"σε":[56,415,488],"ἀπέλιπόν":[57],"Κρήτῃ":[59],"ἵνα":[60],"ἐπιδιορθώσῃ":[61],"τὰ":[62],"λείποντα":[63],"καταστήσῃς":[64],"πρεσβυτέρους":[65],"πόλιν":[67],"ὡς":[68],"ἐγώ":[69],"σοι":[70],"διεταξάμην":[71],"εἴ":[72],"τίς":[73],"ἐστιν":[74,424],"ἀνέγκλητος":[75],"ἀνήρ":[76],"μιᾶς":[77],"γυναικὸς":[78],"ἔχων":[79,277],"τέκνα":[80],"πιστά":[81],"μὴ":[83,233],"κατηγορίᾳ":[84],"ἀσωτίας":[85],"ἢ":[86],"ἀνυπότακτα":[87],"γὰρ":[88,123,436],"δεῖ":[89,142],"εἶναι":[90,218,243,349],"τὸν":[91],"ἐπίσκοπον":[92],"ἀνέγκλητον":[93],"οἰκονόμον":[94],"αὐθάδη":[95],"ὀργίλον":[96],"πάροινον":[97],"πλήκτην":[98],"αἰσχροκερδῆ":[99],"ἀλλὰ":[100,190,290],"φιλόξενον":[101],"φιλάγαθον":[102],"σώφρονα":[103],"δίκαιον":[104],"ὅσιον":[105],"ἐγκρατῆ":[106],"ἀντεχόμενον":[107],"τοῦ":[108,256,380],"λόγου":[109],"πιστοῦ":[110],"τὴν":[111,178],"διδαχὴν":[112],"ᾖ":[113],"δυνατὸς":[114],"παρακαλεῖν":[116],"τῇ ὑγιαινούσῃ":[118],"τῇ διδασκαλίᾳ":[119],"ἐλέγχειν":[120],"τοὺς":[121,494],"ἀντιλέγοντας":[122,288],"εἰσὶν":[124,437],"πολλοὶ":[125],"ἀνυπότακτοι":[126],"ματαιολόγοι":[127],"φρεναπάται":[128],"μάλιστα":[129],"οἱ":[130,491],"ἐκ":[131],"περιτομῆς":[133],"οὓς":[134],"ἐπιστομίζειν":[135],"οἵτινες":[136],"ἀνατρέπουσιν":[137],"οἴκους":[138],"ὅλους":[139],"διδάσκοντες":[140],"ἃ":[141,386],"κέρδους":[144],"αἰσχροῦ":[145],"τις":[146],"ἐξ":[147,384],"αὐτῶν αὐτῶν":[148],"ἴδιος":[149],"προφήτης":[150],"εἶπέν":[151],"Κρῆτες":[152],"ἀεὶ":[153],"ψεῦσται":[154],"κακὰ":[155],"θηρία":[156],"γαστέρες":[157],"ἀργαί":[158],"αὕτη":[159],"ἡ μαρτυρία":[160],"ἐστὶν":[161],"ἀληθής":[162],"δι’":[163],"αἰτίαν":[165],"ἔλεγχε αὐτοὺς":[166],"ἀποτόμως":[167],"ὑγιαίνωσιν":[168],"τῇ":[169,224,225,404],"πίστει":[170,496],"προσέχοντες":[171],"μύθοις":[172],"Ἰουδαϊκοῖς":[173],"ἐντολαῖς":[175],"ἀνθρώπων":[176],"ἀποστρεφομένων":[177],"ἀλήθειαν":[179],"πάντα":[180,264],"καθαρὰ":[181],"τοῖς":[182,184,200,251,427],"καθαροῖς":[183],"μεμιαμμένοις":[185],"ἀπίστοις":[187],"οὐδὲν":[188],"καθαρόν":[189],"αὐτῶν":[192],"ὁ νοῦς":[193],"ἡ συνείδησις":[195],"μεμίανται":[196],"ὁμολογοῦσιν":[197],"εἰδέναι":[198],"Θεὸν":[199],"ἔργοις":[201],"ἀρνοῦνται":[202],"ὄντες":[203],"βδελυκτοὶ":[205],"ἀπειθεῖς":[206],"ἀδόκιμοι":[207],"πρὸς":[208,357],"πᾶν":[209],"ἀγαθὸν":[210],"ἔργον":[211],"σὺ":[213],"λάλει":[214],"πρέπει":[215],"ὑγιαινούσῃ":[216],"διδασκαλίᾳ":[217],"πρεσβύτας":[219],"νηφαλίους":[220],"σεμνούς":[221],"σώφρονας":[222,246],"ὑγιαίνοντας":[223],"ἀγάπῃ":[226],"ὑπομονῇ":[227],"ὡσαύτως":[228],"πρεσβύτιδας":[229],"ἱεροπρεπεῖς":[230],"καταστήματι":[232],"διαβόλους":[234],"μηδὲ":[235],"δεδουλωμένας":[236],"οἴνῳ":[237],"πολλῷ":[238],"καλοδιδασκάλους":[239],"σωφρονίζωσι":[240],"τὰς":[241,305,483],"νέας":[242],"φιλάνδρους":[244],"φιλοτέκνους":[245],"ἁγνάς":[247],"οἰκουργούς":[248],"ἀγαθάς":[249],"ὑποτασσομένας":[250],"ἀνδράσιν":[253],"ὁ":[254,411,450],"λόγος":[255,412],"βλασφημῆται":[257],"παρακάλει":[258],"νεωτέρους":[259],"σωφρονεῖν":[260],"παρεχόμενος":[261],"σεαυτὸν":[262],"περὶ":[263,280],"τύπον":[265],"καλῶν":[266,422],"ἔργων":[267,481],"ἀφθορίαν":[268],"σεμνότητα":[269],"λόγον":[270],"ὑγιῆ":[271],"ἀκατάγνωστον":[272],"ὁ ἐξ":[273],"ἐναντίας":[274],"ἐντραπῇ":[275],"μηδὲν":[276,474],"φαῦλον":[278],"λέγειν":[279],"ὑποτάσσεσθαι":[282,345],"δούλους":[283],"δεσπόταις":[285],"πᾶσιν":[286,300],"εὐαρέστους":[287],"μὴ νοσφιζομένους":[289],"ἐνδεικνυμένους":[291,355],"πᾶσαν":[292],"ἀγαθήν":[293],"κοσμῶσιν":[294],"διδασκαλίαν":[295],"ἡ":[296,378,497],"σωτήριος":[298],"ἐπεφάνη":[299],"ἀνθρώποις":[301],"παιδεύουσα ἡμᾶς":[302],"ἀρνησάμενοι":[303],"ἀσέβειαν":[304],"ἐπιθυμίας":[306],"κοσμικὰς":[307],"ζήσωμεν":[308],"τῷ":[309],"αἰῶνι":[310],"νῦν":[311],"σωφρόνως":[312],"δικαίως":[313],"εὐσεβῶς":[314],"προσδεχόμενοι":[315],"ἐλπίδα":[316,410],"μακαρίαν":[317],"ἐπιφάνειαν":[318],"δόξης":[320],"μεγάλου":[321],"Σωτῆρος":[322],"ὃς":[324],"ἔδωκεν":[325],"ἑαυτὸν":[326],"ὑπὲρ":[327],"λυτρώσηται":[328],"ἡμᾶς":[329,381,399],"πάσης":[330],"ἀνομίας":[331],"καθαρίσῃ":[332],"ἑαυτῷ":[333],"λαὸν":[334],"περιούσιον":[335],"ζηλωτὴν":[336],"ταῦτα":[337],"ἔλεγχε":[338],"μετὰ":[339,442,498],"ἐπιταγῆς":[340],"μηδείς":[341],"σου":[342],"περιφρονείτω":[343],"ὑπομίμνῃσκε αὐτοὺς":[344],"ἀρχαῖς":[346],"ἐξουσίαις":[347],"πειθαρχεῖν":[348],"ἑτοίμους":[350],"βλασφημεῖν":[351],"μηδένα":[352],"ἀμάχους":[353],"ἐπιεικεῖς":[354],"πραΰτητα":[356],"πάντας":[358],"ἀνθρώπους":[359],"γάρ":[360],"ἡμεῖς":[361],"ποτε":[362],"ἦμεν":[363],"ἀνόητοι":[364],"πλανώμενοι":[365],"δουλεύοντες":[366],"ἐπιθυμίαις":[367],"ποικίλαις":[368],"ἡδοναῖς":[369],"διάγοντες":[370],"κακίᾳ":[371],"φθόνῳ":[372],"στυγητοί":[373],"μισοῦντες":[374],"ἀλλήλους":[375],"ὅτε":[376],"χρηστότης":[377],"φιλανθρωπία":[379],"ἔσωσεν":[382],"οὐκ":[383],"τῶν ἐν δικαιοσύνῃ":[385],"ἐποιήσαμεν ἡμεῖς":[387],"τὸ ἔλεος":[389],"διὰ":[390,401],"λουτροῦ":[391],"παλινγενεσίας":[392],"ἀνακαινώσεως":[393],"Πνεύματος":[394],"Ἁγίου":[395],"οὗ":[396],"ἐξέχεεν":[397],"ἐφ’":[398],"πλουσίως":[400],"δικαιωθέντες":[403],"ἐκείνου":[405],"χάριτι":[406],"γενηθῶμεν":[407],"κληρονόμοι":[408],"πιστὸς":[413],"βούλομαί":[414],"διαβεβαιοῦσθαι":[416],"τούτων":[417],"πεπιστευκότες":[418],"Θεῷ":[419],"φροντίζωσιν":[420],"προΐστασθαι":[421,480],"ταῦτά":[423],"καλὰ":[425],"ὠφέλιμα":[426],"περιΐστασο":[428],"ζητήσεις":[429],"μωρὰς":[430],"γενεαλογίας":[431],"ἔρεις":[433],"μάχας":[434],"νομικὰς":[435],"ἀνωφελεῖς":[438],"μάταιοι":[439],"ἄνθρωπον":[440],"αἱρετικὸν":[441],"μίαν":[443],"δευτέραν":[444],"νουθεσίαν":[445],"παραιτοῦ":[446],"ὅτι":[447],"εἰδὼς":[448],"ἐξέστραπται":[449],"τοιοῦτος":[451],"ἁμαρτάνει":[452],"ὢν":[453],"αὐτοκατάκριτος":[454],"ὅταν":[455],"πρὸς σὲ":[456],"πέμψω":[457],"Ἀρτεμᾶν":[458],"Τυχικόν":[459],"σπούδασον":[460],"ἐλθεῖν":[461],"πρός":[462],"με":[463],"εἰς":[464,482],"Νικόπολιν":[465],"κέκρικα":[466],"παραχειμάσαι":[467],"ἐκεῖ":[468],"πρόπεμψον":[469],"σπουδαίως":[470],"Ζηνᾶν":[471],"νομικὸν":[472],"Ἀπολλῶν":[473],"αὐτοῖς":[475],"λείπῃ":[476],"μανθανέτωσαν":[478],"ἡμέτεροι":[479],"χρείας":[484],"ἀναγκαίας":[485],"ὦσιν":[486],"ἄκαρποι":[487],"ἀσπάζονταί":[489],"πάντες":[490],"μετ’ ἐμοῦ":[492],"ἄσπασαι":[493],"φιλοῦντας":[495],"πάντων":[499],"ὑμῶν":[500]},"keys":["ἃ","ἀγαθάς","ἀγαθήν","ἀγαθὸν","ἀγάπῃ","Ἁγίου","ἁγνάς","ἀδόκιμοι","ἀεὶ","αἱρετικὸν","αἰσχροκερδῆ","αἰσχροῦ","αἰτίαν","αἰῶνι","αἰωνίου","αἰωνίων","ἄκαρποι","ἀκατάγνωστον","ἀλήθειαν","ἀληθείας","ἀληθής","ἀλλὰ","ἀλλήλους","ἁμαρτάνει","ἀμάχους","ἀναγκαίας","ἀνακαινώσεως","ἀνατρέπουσιν","ἀνδράσιν","ἀνέγκλητον","ἀνέγκλητος","ἀνήρ","ἀνθρώποις","ἄνθρωπον","ἀνθρώπους","ἀνθρώπων","ἀνόητοι","ἀνομίας","ἀντεχόμενον","ἀντιλέγοντας","ἀνυπότακτα","ἀνυπότακτοι","ἀνωφελεῖς","ἀπειθεῖς","ἀπέλιπόν","ἀπίστοις","ἀπὸ","Ἀπολλῶν","ἀπόστολος","ἀποστρεφομένων","ἀποτόμως","ἀργαί","ἀρνησάμενοι","ἀρνοῦνται","Ἀρτεμᾶν","ἀρχαῖς","ἀσέβειαν","ἀσπάζονταί","ἄσπασαι","ἀσωτίας","αὐθάδη","αὕτη","αὐτοῖς","αὐτοκατάκριτος","αὐτοῦ","αὐτῶν","αὐτῶν αὐτῶν","ἀφθορίαν","βδελυκτοὶ","βλασφημεῖν","βλασφημῆται","βούλομαί","γὰρ","γάρ","γαστέρες","γενεαλογίας","γενηθῶμεν","γνησίῳ","γυναικὸς","δὲ","δεδουλωμένας","δεῖ","δεσπόταις","δευτέραν","δι’","διὰ","διαβεβαιοῦσθαι","διαβόλους","διάγοντες","διδασκαλίᾳ","διδασκαλίαν","διδάσκοντες","διδαχὴν","διεταξάμην","δίκαιον","δικαιωθέντες","δικαίως","δόξης","δουλεύοντες","δοῦλος","δούλους","δυνατὸς","ἑαυτὸν","ἑαυτῷ","ἐγκρατῆ","ἐγὼ","ἐγώ","ἔδωκεν","εἴ","εἰδέναι","εἰδὼς","εἶναι","εἶπέν","εἰρήνη","εἰς","εἰσὶν","ἐκ","ἐκεῖ","ἐκείνου","ἐκλεκτῶν","ἔλεγχε","ἔλεγχε αὐτοὺς","ἐλέγχειν","ἐλθεῖν","ἐλπίδα","ἐλπίδι","ἐν","ἐναντίας","ἐνδεικνυμένους","ἐντολαῖς","ἐντραπῇ","ἐξ","ἐξέστραπται","ἐξέχεεν","ἐξουσίαις","ἐπ’","ἐπεφάνη","ἐπηγγείλατο","ἐπίγνωσιν","ἐπιδιορθώσῃ","ἐπιεικεῖς","ἐπιθυμίαις","ἐπιθυμίας","ἐπίσκοπον","ἐπιστεύθην","ἐπιστομίζειν","ἐπιταγὴν","ἐπιταγῆς","ἐπιφάνειαν","ἐποιήσαμεν ἡμεῖς","ἔργοις","ἔργον","ἔργων","ἔρεις","ἐστιν","ἐστὶν","ἔσωσεν","ἑτοίμους","εὐαρέστους","εὐσέβειαν","εὐσεβῶς","ἐφ’","ἐφανέρωσεν","ἔχων","ζηλωτὴν","Ζηνᾶν","ζήσωμεν","ζητήσεις","ζωῆς","ἢ","ᾖ","ἡ","ἡ μαρτυρία","ἡ συνείδησις","ἡδοναῖς","ἡμᾶς","ἡμεῖς","ἦμεν","ἡμέτεροι","ἡμῶν","ἣν","Θεὸν","Θεὸς","Θεοῦ","Θεῷ","θηρία","ἰδίοις","ἴδιος","ἱεροπρεπεῖς","Ἰησοῦ","Ἰησοῦ Χριστοῦ","ἵνα","Ἰουδαϊκοῖς","καθαρὰ","καθαρίσῃ","καθαροῖς","καθαρόν","καὶ","καιροῖς","κακὰ","κακίᾳ","καλὰ","καλοδιδασκάλους","καλῶν","κατ’","κατὰ","καταστήματι","καταστήσῃς","κατηγορίᾳ","κέκρικα","κέρδους","κηρύγματι","κληρονόμοι","κοινὴν","κοσμικὰς","κοσμῶσιν","Κρῆτες","Κρήτῃ","λάλει","λαὸν","λέγειν","λείπῃ","λείποντα","λόγον","λόγος","λόγου","λουτροῦ","λυτρώσηται","μακαρίαν","μάλιστα","μανθανέτωσαν","μάταιοι","ματαιολόγοι","μάχας","με","μεγάλου","μεμιαμμένοις","μεμίανται","μετ’ ἐμοῦ","μετὰ","μὴ","μὴ νοσφιζομένους","μηδὲ","μηδείς","μηδὲν","μηδένα","μίαν","μιᾶς","μισοῦντες","μύθοις","μωρὰς","νέας","νεωτέρους","νηφαλίους","Νικόπολιν","νομικὰς","νομικὸν","νουθεσίαν","νῦν","ὃ","ὁ","ὁ ἀψευδὴς","ὁ ἐξ","ὁ νοῦς","οἱ","οἰκονόμον","οἰκουργούς","οἴκους","οἴνῳ","οἵτινες","ὅλους","ὁμολογοῦσιν","ὄντες","ὀργίλον","ὃς","ὅσιον","ὅταν","ὅτε","ὅτι","οὗ","οὐδὲν","οὐκ","οὓς","παιδεύουσα ἡμᾶς","παλινγενεσίας","πᾶν","πάντα","πάντας","πάντες","πάντων","παραιτοῦ","παρακάλει","παρακαλεῖν","παραχειμάσαι","παρεχόμενος","πάροινον","πᾶσαν","πάσης","πᾶσιν","Πατρὸς","Παῦλος","πειθαρχεῖν","πέμψω","πεπιστευκότες","περὶ","περιΐστασο","περιούσιον","περιτομῆς","περιφρονείτω","πιστά","πίστει","πίστιν","πιστὸς","πιστοῦ","πλανώμενοι","πλήκτην","πλουσίως","Πνεύματος","ποικίλαις","πόλιν","πολλοὶ","πολλῷ","ποτε","πραΰτητα","πρέπει","πρεσβύτας","πρεσβυτέρους","πρεσβύτιδας","πρὸ","προΐστασθαι","πρόπεμψον","πρὸς","πρός","πρὸς σὲ","προσδεχόμενοι","προσέχοντες","προφήτης","σε","σεαυτὸν","σεμνότητα","σεμνούς","σοι","σου","σπουδαίως","σπούδασον","στυγητοί","σὺ","σωτήριος","Σωτῆρος","σώφρονα","σώφρονας","σωφρονεῖν","σωφρονίζωσι","σωφρόνως","τὰ","τὰς","ταῦτα","ταῦτά","τέκνα","τέκνῳ","τῇ","τῇ διδασκαλίᾳ","τῇ ὑγιαινούσῃ","τὴν","τῆς","τίς","τις","Τίτῳ","τὸ ἔλεος","τοιοῦτος","τοῖς","τὸν","τὸν λόγον","τοῦ","τοῦ Σωτῆρος","τοὺς","τούτου","τούτων","τύπον","Τυχικόν","τῷ","τῶν ἐν δικαιοσύνῃ","ὑγιαίνοντας","ὑγιαινούσῃ","ὑγιαίνωσιν","ὑγιῆ","ὑμῶν","ὑπὲρ","ὑπομίμνῃσκε αὐτοὺς","ὑπομονῇ","ὑποτάσσεσθαι","ὑποτασσομένας","φαῦλον","φθόνῳ","φιλάγαθον","φιλάνδρους","φιλανθρωπία","φιλόξενον","φιλοτέκνους","φιλοῦντας","φρεναπάται","φροντίζωσιν","χάριν","χάρις","χάριτι","χρείας","χρηστότης","Χριστοῦ","χρόνων","ψεῦσται","ὢν","ὡς","ὡσαύτως","ὦσιν","ὠφέλιμα"]},"strongAlignments":{"alignments":{"G39720":[0],"G14010":[1,283],"G23160":[2,20,50,199,419],"G11610":[3,26,212,477],"G06520":[4],"G24240 G55470":[5,323],"G25960":[6,13,37,44,66,388,409],"G41020":[7,46,170,496],"G15880":[8],"G25320":[9,115,174,186,191,194,204,432],"G19220":[10],"G02250":[11,179],"G35880":[12,62,91,108,111,121,130,132,169,178,182,184,200,224,225,241,251,254,256,296,305,309,319,378,380,404,411,427,450,483,491,494,497],"G21500":[14],"G19090":[15,398],"G16800":[16,316,410],"G22220":[17],"G01660":[18,25],"G37390":[19,34,134,141,164,324,386,396],"G35880 G08930":[21],"G18610":[22],"G42530":[23],"G55500":[24],"G53190":[27],"G25400":[28],"G23980":[29,149,252,284],"G08460":[30,192,475],"G35880 G30560":[31],"G17220":[32,58,82,117,231],"G27820":[33],"G14730":[35,39,69,281,329,361,381,399,463],"G41000":[36,418],"G20030":[38,340],"G35880 G49900":[40,402],"G51030":[41],"G11030":[42],"G50430":[43,80],"G28390":[45],"G54850":[47,297,406],"G15150":[48],"G05750":[49],"G39620":[51],"G55470":[52],"G24240":[53],"G54840":[54,143],"G37780":[55,159,337,417,423],"G47710":[56,70,213,342,415,488,500],"G06200":[57],"G29140":[59],"G24430":[60],"G19300":[61],"G30070":[63,476],"G25250":[64],"G42450":[65],"G41720":[67],"G56130":[68],"G12990":[71],"G14870":[72],"G51000":[73,146],"G15100":[74,90,113,124,161,203,218,243,349,363,424,437,453,486],"G04100":[75,93],"G04350":[76,253],"G15200":[77,443],"G11350":[78],"G21920":[79,277],"G41030":[81,110,413],"G33610":[83,233],"G27240":[84],"G08100":[85],"G22280":[86],"G05060":[87,126],"G10630":[88,123,360,436],"G12100":[89,142],"G19850":[92],"G36230":[94],"G08290":[95],"G37110":[96],"G39430":[97],"G41310":[98],"G01460":[99],"G02350":[100,190,290],"G53820":[101],"G53580":[102],"G49980":[103,222,246],"G13420":[104],"G37410":[105],"G14680":[106],"G04720":[107],"G30560":[109,255,270,412],"G13220":[112],"G14150":[114],"G38700":[116,258],"G35880 G51980":[118],"G35880 G13190":[119],"G16510":[120,338],"G04830":[122,288],"G41830":[125,238],"G31510":[127],"G54230":[128],"G31220":[129],"G15370":[131,147,384],"G40610":[133],"G19930":[135],"G37480":[136],"G03960":[137],"G36240":[138],"G36500":[139],"G13210":[140],"G27710":[144],"G01500":[145],"G08460 G08460":[148],"G43960":[150],"G30040":[151,279],"G29120":[152],"G01040":[153],"G55830":[154],"G25560":[155],"G23420":[156],"G10640":[157],"G06920":[158],"G35880 G31410":[160],"G02270":[162],"G12230":[163,390,401],"G01560":[165],"G16510 G08460":[166],"G06640":[167],"G51980":[168,216,223],"G43370":[171],"G34540":[172],"G24510":[173],"G17850":[175],"G04440":[176,301,359,440],"G06540":[177],"G39560":[180,209,264,286,292,300,330,358,490,499],"G25130":[181,183,189],"G33920":[185,196],"G05710":[187],"G37620":[188],"G35880 G35630":[193],"G35880 G48930":[195],"G36700":[197],"G14920":[198,448],"G20410":[201,211,267,481],"G07200":[202,303],"G09470":[205],"G05450":[206],"G00960":[207],"G43140":[208,357,462],"G00180":[210,249,293],"G29800":[214],"G42410":[215],"G13190":[217,295],"G42460":[219],"G35240":[220],"G45860":[221],"G00260":[226],"G52810":[227],"G56150":[228],"G42470":[229],"G24120":[230],"G26880":[232],"G12280":[234],"G33660":[235],"G14020":[236],"G36310":[237],"G25670":[239],"G49940":[240],"G35010":[242],"G53620":[244],"G53880":[245],"G00530":[247],"G36260":[248],"G52930":[250,282,345],"G09870":[257,351],"G35125":[259],"G49930":[260],"G39300":[261],"G45720":[262],"G40120":[263,280],"G51790":[265],"G25700":[266,422,425],"G08627":[268],"G45870":[269],"G51990":[271],"G01760":[272],"G35880 G15370":[273],"G17270":[274],"G17880":[275],"G33670":[276,341,352,474],"G53370":[278],"G12030":[285],"G21010":[287],"G33610 G35570":[289],"G17310":[291,355],"G28850":[294],"G49920":[298],"G20140":[299],"G38110 G14730":[302],"G07630":[304],"G19390":[306,367],"G28860":[307],"G21980":[308],"G01650":[310],"G35680":[311],"G49960":[312],"G13460":[313],"G21530":[314],"G43270":[315],"G31070":[317],"G20150":[318],"G13910":[320],"G31730":[321],"G49900":[322],"G13250":[325],"G14380":[326,333],"G52280":[327],"G30840":[328],"G04580":[331],"G25110":[332],"G29920":[334],"G40410":[335],"G22070":[336],"G33260":[339,442,498],"G40650":[343],"G52790 G08460":[344],"G07460":[346],"G18490":[347],"G39800":[348],"G20920":[350],"G02690":[353],"G19330":[354],"G42400":[356],"G42180":[362],"G04530":[364],"G41050":[365],"G13980":[366],"G41640":[368],"G22370":[369],"G12360":[370],"G25490":[371],"G53550":[372],"G47670":[373],"G34040":[374],"G02400":[375],"G37530":[376],"G55440":[377],"G53630":[379],"G49820":[382],"G37560":[383],"G35880 G17220 G13430":[385],"G41600 G14730":[387],"G35880 G16560":[389],"G30670":[391],"G38240":[392],"G03420":[393],"G41510":[394],"G00400":[395],"G16320":[397],"G41460":[400],"G13440":[403],"G15650":[405],"G10960":[407],"G28180":[408],"G10140":[414],"G12260":[416],"G54310":[420],"G42910":[421,480],"G56240":[426],"G40260":[428],"G22140":[429],"G34740":[430],"G10760":[431],"G20540":[433],"G31630":[434],"G35440":[435,472],"G05120":[438],"G31520":[439],"G01410":[441],"G12080":[444],"G35590":[445],"G38680":[446],"G37540":[447],"G16120":[449],"G51080":[451],"G02640":[452],"G08430":[454],"G37520":[455],"G43140 G47710":[456],"G39920":[457],"G07340":[458],"G51900":[459],"G47040":[460],"G20640":[461],"G15190":[464,482],"G35330":[465],"G29190":[466],"G39140":[467],"G15630":[468],"G43110":[469],"G47090":[470],"G22110":[471],"G06250":[473],"G31290":[478],"G22510":[479],"G55320":[484],"G03160":[485],"G01750":[487],"G07820":[489,493],"G33260 G14730":[492],"G53680":[495]},"keys":["G00180","G00260","G00400","G00530","G00960","G01040","G01410","G01460","G01500","G01560","G01650","G01660","G01750","G01760","G02250","G02270","G02350","G02400","G02640","G02690","G03160","G03420","G03960","G04100","G04350","G04440","G04530","G04580","G04720","G04830","G05060","G05120","G05450","G05710","G05750","G06200","G06250","G06520","G06540","G06640","G06920","G07200","G07340","G07460","G07630","G07820","G08100","G08290","G08430","G08460","G08460 G08460","G08627","G09470","G09870","G10140","G10630","G10640","G10760","G10960","G11030","G11350","G11610","G12030","G12080","G12100","G12230","G12260","G12280","G12360","G12990","G13190","G13210","G13220","G13250","G13420","G13440","G13460","G13910","G13980","G14010","G14020","G14150","G14380","G14680","G14730","G14870","G14920","G15100","G15150","G15190","G15200","G15370","G15630","G15650","G15880","G16120","G16320","G16510","G16510 G08460","G16800","G17220","G17270","G17310","G17850","G17880","G18490","G18610","G19090","G19220","G19300","G19330","G19390","G19850","G19930","G20030","G20140","G20150","G20410","G20540","G20640","G20920","G21010","G21500","G21530","G21920","G21980","G22070","G22110","G22140","G22220","G22280","G22370","G22510","G23160","G23420","G23980","G24120","G24240","G24240 G55470","G24430","G24510","G25110","G25130","G25250","G25320","G25400","G25490","G25560","G25670","G25700","G25960","G26880","G27240","G27710","G27820","G28180","G28390","G28850","G28860","G29120","G29140","G29190","G29800","G29920","G30040","G30070","G30560","G30670","G30840","G31070","G31220","G31290","G31510","G31520","G31630","G31730","G33260","G33260 G14730","G33610","G33610 G35570","G33660","G33670","G33920","G34040","G34540","G34740","G35010","G35125","G35240","G35330","G35440","G35590","G35680","G35880","G35880 G08930","G35880 G13190","G35880 G15370","G35880 G16560","G35880 G17220 G13430","G35880 G30560","G35880 G31410","G35880 G35630","G35880 G48930","G35880 G49900","G35880 G51980","G36230","G36240","G36260","G36310","G36500","G36700","G37110","G37390","G37410","G37480","G37520","G37530","G37540","G37560","G37620","G37780","G38110 G14730","G38240","G38680","G38700","G39140","G39300","G39430","G39560","G39620","G39720","G39800","G39920","G40120","G40260","G40410","G40610","G40650","G41000","G41020","G41030","G41050","G41310","G41460","G41510","G41600 G14730","G41640","G41720","G41830","G42180","G42400","G42410","G42450","G42460","G42470","G42530","G42910","G43110","G43140","G43140 G47710","G43270","G43370","G43960","G45720","G45860","G45870","G47040","G47090","G47670","G47710","G49820","G49900","G49920","G49930","G49940","G49960","G49980","G50430","G51000","G51030","G51080","G51790","G51900","G51980","G51990","G52280","G52790 G08460","G52810","G52930","G53190","G53370","G53550","G53580","G53620","G53630","G53680","G53820","G53880","G54230","G54310","G54840","G54850","G55320","G55440","G55470","G55500","G55830","G56130","G56150","G56240"]}} diff --git a/src/js/actions/PopoverActions.js b/src/js/actions/PopoverActions.js index f98cbc90e4..436e7cc51f 100644 --- a/src/js/actions/PopoverActions.js +++ b/src/js/actions/PopoverActions.js @@ -1,10 +1,13 @@ import consts from './ActionTypes'; -export const showPopover = (title, bodyText, positionCoord) => ({ +export const showPopover = (title, bodyText, positionCoord, style = {}, titleStyle = {}, bodyStyle = {}) => ({ type: consts.SHOW_POPOVER, title, bodyText, positionCoord, + style, + titleStyle, + bodyStyle, }); export const closePopover = () => ({ type: consts.CLOSE_POPOVER }); diff --git a/src/js/common/constants.js b/src/js/common/constants.js index d8e1c317b2..d40b18663d 100644 --- a/src/js/common/constants.js +++ b/src/js/common/constants.js @@ -55,6 +55,7 @@ export const SETTINGS_FOLDER = path.join(env.data(), TC_PATH); export const SETTINGS_PATH = path.join(SETTINGS_FOLDER, 'settings.json'); export const ASSETS_PATH = isProduction ? path.join(STATIC_FOLDER_PATH, 'assets') : path.join('./src/assets'); export const PROJECT_LICENSES_PATH = isProduction ? path.join(STATIC_FOLDER_PATH, 'projectLicenses') : path.join('./src/assets/projectLicenses'); +export const ALIGNMENT_DATA_PATH = path.join(USER_HOME, 'translationCore', 'alignmentData'); // string names export const TC_VERSION = 'tc_version'; export const SOURCE_CONTENT_UPDATER_MANIFEST = 'source-content-updater-manifest.json'; diff --git a/src/js/components/Popover.js b/src/js/components/Popover.js index b2414c8ac9..e312610c60 100644 --- a/src/js/components/Popover.js +++ b/src/js/components/Popover.js @@ -4,19 +4,48 @@ import PropTypes from 'prop-types'; import { Popover, Divider } from 'material-ui'; import { Glyphicon } from 'react-bootstrap'; +const defaultPopoverStyle = { + padding: '0.75em', maxWidth: '400px', backgroundColor: 'var(--background-color-light)', +}; + +const defaultTitleStyle = { + fontSize: '1.2em', fontWeight: 'bold', marginBottom: 10, marginTop: 0, paddingTop: 0, +}; + +const defaultBodyStyle = { padding: '10px 0 15px 0' }; + +/** + * apply styles to defaultStyles + * @param {object} style + * @param {object} defaultStyle + * @returns {*} + */ +function addStyles(style, defaultStyle) { + let newStyle = defaultStyle; + + if (style && Object.keys(style)?.length) { + newStyle = { + ...defaultStyle, + ...style, + }; + } + return newStyle; +} + class PopoverComponent extends Component { render() { let { - popoverVisibility, title, bodyText, positionCoord, onClosePopover, + popoverVisibility, title, bodyText, positionCoord, onClosePopover, style, titleStyle, bodyStyle, } = this.props; + const style_ = addStyles(style, defaultPopoverStyle); + const titleStyle_ = addStyles(titleStyle, defaultTitleStyle); + const bodyStyle_ = addStyles(bodyStyle, defaultBodyStyle); return (
- + {title}
- + {bodyText} @@ -58,6 +85,9 @@ PopoverComponent.propTypes = { bodyText: PropTypes.any, positionCoord: PropTypes.any, onClosePopover: PropTypes.func, + style: PropTypes.object, + titleStyle: PropTypes.object, + bodyStyle: PropTypes.object, }; export default PopoverComponent; diff --git a/src/js/components/dialogComponents/BaseDialog.js b/src/js/components/dialogComponents/BaseDialog.js index 4a8a53930d..9c52fb48ac 100644 --- a/src/js/components/dialogComponents/BaseDialog.js +++ b/src/js/components/dialogComponents/BaseDialog.js @@ -95,6 +95,8 @@ class BaseDialog extends React.Component { children, actions, scrollableContent, + style, + contentStyle, } = this.props; let dialogActions = actions @@ -131,6 +133,8 @@ class BaseDialog extends React.Component { autoScrollBodyContent={scrollableContent} onRequestClose={onClose} actions={dialogActions} + style={style} + contentStyle={contentStyle} > {children} @@ -154,6 +158,8 @@ BaseDialog.propTypes = { titleStyle: PropTypes.object, children: PropTypes.any, bodyStyle: PropTypes.object, + style: PropTypes.object, + contentStyle: PropTypes.object, }; BaseDialog.defaultProps = { diff --git a/src/js/containers/AlignmentSearchDialogContainer.js b/src/js/containers/AlignmentSearchDialogContainer.js new file mode 100644 index 0000000000..fc78b62552 --- /dev/null +++ b/src/js/containers/AlignmentSearchDialogContainer.js @@ -0,0 +1,2046 @@ +/* eslint-disable react/display-name,object-curly-newline */ +/* eslint-disable no-await-in-loop */ +import React, { forwardRef } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import fs from 'fs-extra'; +import path from 'path-extra'; +import { + Divider, + Menu, + MenuItem, + SelectField, + Subheader, + TextField, +} from 'material-ui'; +import MaterialTable from 'material-table'; +import env from 'tc-electron-env'; +import _ from 'lodash'; +import AddBox from '@material-ui/icons/AddBox'; +import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; +import ArrowDownward from '@material-ui/icons/ArrowDownward'; +// import ArrowUpward from '@material-ui/icons/ArrowUpward'; +import Check from '@material-ui/icons/Check'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Clear from '@material-ui/icons/Clear'; +import DeleteIcon from '@material-ui/icons/Delete'; +import DeleteOutline from '@material-ui/icons/DeleteOutline'; +import Edit from '@material-ui/icons/Edit'; +import FilterList from '@material-ui/icons/FilterList'; +import FirstPage from '@material-ui/icons/FirstPage'; +import LastPage from '@material-ui/icons/LastPage'; +import NavigateBeforeIcon from '@material-ui/icons/NavigateBefore'; +import NavigateNextIcon from '@material-ui/icons/NavigateNext'; +import Remove from '@material-ui/icons/Remove'; +import RemoveCircleOutlineIcon from '@material-ui/icons/RemoveCircleOutline'; +import SaveAlt from '@material-ui/icons/SaveAlt'; +import Search from '@material-ui/icons/Search'; +import ViewColumn from '@material-ui/icons/ViewColumn'; +import IconButton from '@material-ui/core/IconButton'; + +import usfm from 'usfm-js'; + +// selectors +import { getProjectManifest, getSetting } from '../selectors'; +// actions +// components +import BaseDialog from '../components/dialogComponents/BaseDialog'; +// helpers +import { + addTwordsInfoToResource, + ALIGNMENT_DATA_DIR, + ALIGNMENTS_KEY, + checkForHelpsForBible, + deleteCachedAlignmentData, + downloadBible, + getAlignmentsFromResource, + getAvailableBibles, + getKeyForBible, + getSearchableAlignments, + getTwordALignments, + getTwordsIndex, + getTwordsKey, + getVerseForKey, + highlightSelectedTextInVerse, + indexTwords, + isMasterResourceDownloaded, + loadAlignments, + multiSearchAlignments, + NT_BOOKS, + OT_BOOKS, + parseResourceKey, + readDirectory, + removeIndices, + saveTwordsIndex, + TWORDS_KEY, +} from '../helpers/alignmentSearchHelpers'; +import { + ALIGNMENT_DATA_PATH, + USER_HOME, + USER_RESOURCES_PATH, +} from '../common/constants'; +import { delay } from '../common/utils'; +import { + closeAlertDialog, + openAlertDialog, + openOptionDialog, +} from '../actions/AlertModalActions'; +import { setSetting } from '../actions/SettingsActions'; +import { + BIBLE_BOOKS, + BIBLES_ABBRV_INDEX, + NT_ORIG_LANG, +} from '../common/BooksOfTheBible'; +import { showPopover } from '../actions/PopoverActions'; +import * as OnlineModeConfirmActions from '../actions/OnlineModeConfirmActions'; +import * as exportHelpers from '../helpers/exportHelpers'; + +Menu.defaultProps.disableAutoFocus = true; // to prevent auto-scrolling + +const tableIcons = { + Add: forwardRef((props, ref) => ), + Check: forwardRef((props, ref) => ), + Clear: forwardRef((props, ref) => ), + Delete: forwardRef((props, ref) => ), + DetailPanel: forwardRef((props, ref) => ), + Edit: forwardRef((props, ref) => ), + Export: forwardRef((props, ref) => ), + Filter: forwardRef((props, ref) => ), + FirstPage: forwardRef((props, ref) => ), + LastPage: forwardRef((props, ref) => ), + NextPage: forwardRef((props, ref) => ), + PreviousPage: forwardRef((props, ref) => ), + ResetSearch: forwardRef((props, ref) => ), + Search: forwardRef((props, ref) => ), + SortArrow: forwardRef((props, ref) => ), + ThirdStateCheck: forwardRef((props, ref) => ), + ViewColumn: forwardRef((props, ref) => ), +}; + +const OkButton = 'OK'; +const CancelButton = 'CANCEL'; + +const SEARCH_SOURCE = 'search_source'; +const SEARCH_LEMMA = 'search_lemma'; +const SEARCH_TARGET = 'search_target'; +const SEARCH_STRONG = 'search_strong'; +const SEARCH_REFS = 'search_refs'; +const searchFieldOptions = [ + SEARCH_SOURCE, + SEARCH_LEMMA, + SEARCH_TARGET, + SEARCH_STRONG, + SEARCH_REFS, +]; +const searchFieldOptionsForTwords = [ + SEARCH_LEMMA, + SEARCH_SOURCE, + SEARCH_STRONG, + SEARCH_TARGET, +]; +const searchFieldLabels = { + [SEARCH_SOURCE]: 'Search Source Words', + [SEARCH_LEMMA]: 'Search Lemma Words', + [SEARCH_TARGET]: 'Search Target Words', + [SEARCH_STRONG]: 'Search Strong\'s Numbers', + [SEARCH_REFS]: 'Search References', +}; + +// Results column options +const SHOW_SOURCE_TEXT = 'sourceText'; +const SHOW_SOURCE_LEMMA = 'sourceLemma'; +const SHOW_STRONGS = 'strong'; +const SHOW_MORPH = 'morph'; +const SHOW_TARGET_TEXT = 'targetText'; +const SHOW_MATCH_COUNT = 'count'; +const SHOW_REFERENCES = 'refStr'; +const ALIGNED_TEXT = 'alignedText'; +const ALIGNED_TEXT2 = 'alignedText2'; + +const SOURCE_TEXT_LABEL = 'Source Text Column'; +const SOURCE_LEMMA_LABEL = 'Source Lemma Column'; +const STRONGS_LABEL = 'Source Strong\'s Column'; +const SOURCE_MORPH_LABEL = 'Source Morph Column'; +const TARGET_TEXT_LABEL = 'Target Text Column'; +const MATCH_COUNT_LABEL = 'Match Count Column'; +const REFERENCES_LABEL = 'References Column'; +// const ALIGNED_TEXT_LABEL = 'Aligned Text Column'; +const SEARCH_DUAL = 'search_dual'; +const SEARCH_CASE_SENSITIVE = 'search_case_sensitive'; +const SEARCH_MATCH_WHOLE_WORD = 'search_match_whole_word'; +const SEARCH_MATCH_WORDS_IN_ORDER = 'search_match_words_in_order'; +const SEARCH_HIDE_USFM = 'search_hide_usfm'; +const SEARCH_TWORDS = 'search_twords'; +const SEARCH_MASTER = 'search_master'; +const REFRESH_MASTER = 'refresh_master'; +const CLEAR_INDEX_DATA = 'clear_index_data'; + +const SEARCH_DUAL_LABEL = 'Dual Repo Searching'; +const SEARCH_CASE_SENSITIVE_LABEL = 'Case Sensitive'; +const SEARCH_MATCH_WHOLE_WORD_LABEL = 'Match Whole Word'; +const SEARCH_MATCH_WORDS_IN_ORDER_LABEL = 'Match Multiple Words in Order'; +const SEARCH_HIDE_USFM_LABEL = 'Hide USFM Markers'; +const SEARCH_TWORDS_LABEL = 'Search Translation Words'; +const SEARCH_MASTER_LABEL = 'Search Master Branch'; +const REFRESH_MASTER_LABEL = 'Refresh Master Branch'; +const CLEAR_INDEX_DATA_LABEL = 'Clear Index Data'; + +const searchOptions = [ + { + key: SEARCH_CASE_SENSITIVE, + label: SEARCH_CASE_SENSITIVE_LABEL, + stateKey: 'caseSensitive', + }, + { + key: SEARCH_MATCH_WHOLE_WORD, + label: SEARCH_MATCH_WHOLE_WORD_LABEL, + stateKey: 'matchWholeWord', + }, + { + key: SEARCH_MATCH_WORDS_IN_ORDER, + label: SEARCH_MATCH_WORDS_IN_ORDER_LABEL, + stateKey: 'inOrder', + }, + { + key: SEARCH_TWORDS, + label: SEARCH_TWORDS_LABEL, + stateKey: 'searchTwords', + }, + { + key: SEARCH_DUAL, + label: SEARCH_DUAL_LABEL, + stateKey: 'dualSearch', + }, + { + key: SEARCH_HIDE_USFM, + label: SEARCH_HIDE_USFM_LABEL, + stateKey: 'hideUsfmMarkers', + }, + { + key: SEARCH_MASTER, + label: SEARCH_MASTER_LABEL, + stateKey: 'searchMaster', + }, +]; + +const searchOptionRefreshMaster = { + key: REFRESH_MASTER, + label: REFRESH_MASTER_LABEL, + stateKey: 'refreshMaster', +}; + +const searchOptionClearIndexData = { + key: CLEAR_INDEX_DATA, + label: CLEAR_INDEX_DATA_LABEL, + stateKey: 'clearIndex', +}; + +const SEARCH_SETTINGS_KEY = 'searchSettingsKey'; + +const getShowTitle = label => `Show ${label}`; +const showMenuItems = [ + { + key: SHOW_SOURCE_TEXT, + label: getShowTitle(SOURCE_TEXT_LABEL), + }, + { + key: SHOW_MORPH, + label: getShowTitle(SOURCE_MORPH_LABEL), + }, + { + key: SHOW_SOURCE_LEMMA, + label: getShowTitle(SOURCE_LEMMA_LABEL), + }, + { + key: SHOW_STRONGS, + label: getShowTitle(STRONGS_LABEL), + }, + { + key: SHOW_TARGET_TEXT, + label: getShowTitle(TARGET_TEXT_LABEL), + }, + { + key: SHOW_MATCH_COUNT, + label: getShowTitle(MATCH_COUNT_LABEL), + }, + { + key: SHOW_REFERENCES, + label: getShowTitle(REFERENCES_LABEL), + }, +]; + +let bibles = {}; +let popupLocation = null; + +/** + * Renders a dialog for user to do alignment search. + * + * @class + * + * @property {func} translate - the localization function + * @property {func} onClose - callback when the dialog is closed + * @property {bool} open - controls whether the dialog is open or closed + */ +class AlignmentSearchDialogContainer extends React.Component { + constructor(props) { + super(props); + this.state = { + alignmentData: null, + searchStr: '', + searchType: [ + SEARCH_SOURCE, + SEARCH_TARGET, + ], + caseSensitive: false, + matchWholeWord: false, + found: null, + alignedBibles: [], + alignedBible: null, + alignedBible2: null, + alignmentData2: null, + inOrder: false, + hide: {}, + dualSearch: false, + updatingMaster: false, + selectingAlignments: false, + }; + this.setSearchStr = this.setSearchStr.bind(this); + this.startSearch = this.startSearch.bind(this); + this.showResults = this.showResults.bind(this); + this.setSearchFields = this.setSearchFields.bind(this); + this.isSearchFieldSelected = this.isSearchFieldSelected.bind(this); + this.handleClose = this.handleClose.bind(this); + this.setSearchTypes = this.setSearchTypes.bind(this); + this.getSelectedOptions = this.getSelectedOptions.bind(this); + this.setSearchAlignedBible = this.setSearchAlignedBible.bind(this); + this.setSearchAlignedBible2 = this.setSearchAlignedBible2.bind(this); + this.showMessage = this.showMessage.bind(this); + this.showColumnHidesMenu = this.showColumnHidesMenu.bind(this); + this.selectColumnHides = this.selectColumnHides.bind(this); + this.toggleBook = this.toggleBook.bind(this); + this.setAll = this.setAll.bind(this); + this.addBible = this.addBible.bind(this); + this.getVerseContent = this.getVerseContent.bind(this); + } + + componentDidUpdate(prevProps, prevState) { + if (this.props.open) { + if (!prevProps.open) { // when dialog is opened + const savedState = this.props.savedSettings; + + if (savedState && Object.keys(savedState).length) { + const newState = _.cloneDeep(savedState); + + if (!newState.hide) { + newState.hide = {}; + } + + newState.found = null; + newState.updatingMaster = false; + newState.selectingAlignments = false; + this.setState(newState); + } + + delay(100).then(() => { + this.loadAlignmentSearchOptionsWithUI(); + }); + } + } else { + if (prevProps.open) { // if dialog closed + this.props.saveSettings(_.cloneDeep(this.state)); + } + } + } + + /** + * update message and delay for screen to update + * @param message + * @param loading + * @returns {Promise} + */ + async showMessage(message, loading) { + this.props.openAlertDialog(message, loading); + await delay(100); + } + + /** + * index downloaded bible resources to get available aligned bibles + */ + async loadAlignmentSearchOptionsWithUI() { + console.log('loadAlignmentSearchOptions() - starting'); + await this.showMessage('Loading Available Aligned Bibles', true).then(async () => { + this.loadAlignmentSearchOptions(this.state.searchMaster); + const unsupported = this.isSupportedAlignmentKey(this.state.alignedBible) || this.isSupportedAlignmentKey(this.state.alignedBible2); + + if (unsupported) { // if either keys not supported then clear them + this.setState({ alignedBible: null, alignedBible2: null }); + } else { + await this.selectAlignedBookToSearch(this.state.alignedBible); + await this.selectAlignedBookToSearch(this.state.alignedBible2, 2); + } + console.log('loadAlignmentSearchOptions() - finished'); + }); + } + + /** + * make sure alignment key is for current build + * @param {string} alignment + * @returns {boolean} + */ + isSupportedAlignmentKey(alignment) { + let unsupported = true; + + // if alignment key is set make sure it is compatible with current build + if (!alignment || alignment?.indexOf(ALIGNMENTS_KEY) > 0 || alignment?.indexOf(TWORDS_KEY) > 0) { + unsupported = false; + } + return unsupported; + } + + /** + * index downloaded bible resources to get available aligned bibles + * @param {boolean} useMaster - if true then include master branch alignments + */ + loadAlignmentSearchOptions(useMaster) { + const tCorePath = path.join(env.home(), 'translationCore'); + let alignedBibles = getSearchableAlignments(tCorePath, useMaster); + console.log(`loadAlignmentSearchOptions() - found ${alignedBibles?.length} aligned bible testaments`); + const alignedBibles_ = alignedBibles.map(bible => { + if (this.state.searchTwords) { + if (bible.owner !== 'Door43-Catalog') { // for now only door43 supported + if (!checkForHelpsForBible(bible)) { + return null; + } + } + } + + if (!useMaster) { + if (bible.version === 'master') { + return null; + } + } + + const key = getKeyForBible(bible, ALIGNMENTS_KEY); + const label = this.getLabelForBible(bible); + bible.key = key; + bible.label = label; + bible.isNT = bible.origLang === NT_ORIG_LANG; + return bible; + }); + + alignedBibles = alignedBibles_.filter(bible => bible); + alignedBibles = alignedBibles.sort((a, b) => a.key < b.key ? -1 : 1); + this.props.closeAlertDialog(); + this.setState({ alignedBibles }); + console.log(`loadAlignmentSearchOptions() - current aligned bible ${this.state.alignedBible}`); + } + + /** + * get user label for bible + * @param {object} bible + * @param {boolean} short - if true, remove original language + * @returns {string} + */ + getLabelForBible(bible, short = false) { + let label; + const bibleId = bible.resourceId || bible.bibleId; + + if (short) { + label = `${bible.languageId}_${bibleId}/${bible.owner} - ${bible.version}`; + } else { + label = `${bible.languageId}_${bibleId}/${bible.owner} - ${(bible.origLang)} - ${bible.version}`; + } + return label; + } + + /** + * checks if selected bible has an index saved. If not then an index is first created and saved. Then the index is + * loaded for searching + * @param {string} selectedBibleKey + * @param {function} callback - when finished + * @param {number} searchNum - if number is two, then load for second search + */ + async loadAlignmentData(selectedBibleKey, callback = null, searchNum = 1) { + if (selectedBibleKey) { + console.log(`loadAlignmentData() - loading index for '${selectedBibleKey}'`); + await this.showMessage('Loading index of Bible alignments for Search', true).then(async () => { + const resource = this.getResourceForBible(selectedBibleKey); + + if (resource) { + if (!resource.alignmentCount) { + console.log(`loadAlignmentData() - Doing one-time indexing of Bible for Search of '${selectedBibleKey}'`); + const indexingMsg = `Doing one-time indexing of Bible for Search of '${selectedBibleKey}':`; + await this.showMessage(indexingMsg, true); + const alignmentData = await getAlignmentsFromResource(USER_RESOURCES_PATH, resource, async (percent) => { + await this.showMessage(<> {indexingMsg}
{`${100 - percent}% left`} , true); + }); + + if (alignmentData?.alignments?.length) { + console.log(`loadAlignmentData() - found ${alignmentData?.alignments?.length} alignments`); + resource.alignmentCount = alignmentData?.alignments?.length; + this.setState({ alignedBibles: this.state.alignedBibles }); + await this.showMessage(indexingMsg, true); + const success = this.loadIndexedAlignmentData(resource, searchNum); + callback && await callback(success, `Failed loading index for '${selectedBibleKey}'`); + } else { + console.error(`loadAlignmentData() - no alignments for ${selectedBibleKey}`); + callback && await callback(false, `No Alignments found in '${selectedBibleKey}'`); + } + } else { + console.log(`loadAlignmentData() loaded cached alignment index for ${selectedBibleKey}`); + const success = this.loadIndexedAlignmentData(resource, searchNum); + callback && await callback(success, `Failed loading index for '${selectedBibleKey}'`); + } + } else { + console.log(`loadAlignmentData() no aligned bible match found for ${selectedBibleKey}`); + callback && await callback(false, `Could not find aligned bible for '${selectedBibleKey}'`); + } + }); + } else { + console.log('loadAlignmentData() no aligned bible'); + callback && await callback(false, `Invalid aligned bible ID: '${selectedBibleKey}'`); + } + } + + selectColumnHides(key) { + const newHide = { + ...(this.state?.hide || {}), + }; + let currentState = newHide[key]; + newHide[key] = !currentState; + this.setState({ hide: newHide }); + } + + showColumnHidesMenu() { + const hide = this.state?.hide || {}; + + this.props.openAlertDialog( + <> +
{'Enable Columns'}
+ + {showMenuItems.map(item => { + const enabled = !hide[item.key]; + return ( + this.selectColumnHides(item.key)} + checked={enabled} + /> + ); + })} + + + , false); + } + + /** + * searches aligned Bibles list for selected bible and returns resource data. + * @param {string} selectedBibleKey + * @returns {object|null} resource + */ + getResourceForBible(selectedBibleKey) { + const resource = this.state.alignedBibles?.find(item => item.key === selectedBibleKey); + return resource; + } + + /** + * loads the indexed alignment data for resource and saves in state + * @param {object} resource + * @param {number} searchNum - if number is two, then load for second search + * @return {boolean} true if success + */ + loadIndexedAlignmentData(resource, searchNum = 1) { + const alignmentKey = (searchNum === 2) ? 'alignmentData2' : 'alignmentData'; + + try { + // check alignment data folder for indexed search data + const resourcesIndexed = readDirectory(ALIGNMENT_DATA_PATH, false, true, '.json'); + let alignmentData; + const foundIndexPath = resourcesIndexed?.find((file) => file.indexOf(resource.key) === 0); + + if (foundIndexPath) { + alignmentData = loadAlignments(path.join(ALIGNMENT_DATA_PATH, foundIndexPath)); + } else { + console.log('loadIndexedAlignmentData() - did not find index'); + } + + if (alignmentData) { + console.log('loadIndexedAlignmentData() - loaded alignment data'); + this.setState({ + [alignmentKey]: alignmentData, + found: null, + }); + return true; + } else { + this.props.openAlertDialog(`No Aligned Bible found for ${resource.label}`); + console.log('loadIndexedAlignmentData() - FAILED to load alignment data'); + this.setState({ + alignmentData: null, + found: null, + }); + return false; + } + } catch (e) { + console.error('loadIndexedAlignmentData() - ERROR loading alignment data', e); + return false; + } + } + + getSearchFieldOptions() { + const currentsearchFieldOptions = this.state.searchTwords ? searchFieldOptionsForTwords : searchFieldOptions; + return currentsearchFieldOptions; + } + + saveToJsonfile(data) { + const DOCUMENTS_PATH = path.join(USER_HOME, 'Documents'); + const defaultFields = [ + { id: 'count', source: 'count' }, + { id: 'morph', source: 'morph' }, + { id: 'refs', source: 'refs' }, + { id: 'lemma', source: 'sourceLemma' }, + { id: 'sourceText', source: 'sourceText' }, + { id: 'strong', source: 'strong' }, + { id: 'targetText', source: 'targetText' }, + { id: 'targetsPos', source: 'targetsPos' }, + ]; + const tWordsFields = [ + { id: 'refs', source: 'refs' }, + { id: 'lemma', source: 'lemma' }, + { id: 'strong', source: 'strong' }, + { id: 'morph', source: 'morph' }, + { id: 'alignedText', source: 'alignedText' }, + { id: 'targetText', source: 'targetText' }, + { id: 'targetAlignment', source: 'alignedText' }, + { id: 'sourceAlignment', source: 'sourceText' }, + { id: 'category', source: 'category' }, + { id: 'tWord', source: 'contextId.groupId' }, + { id: 'quote', source: 'contextId.quote' }, + { id: 'count', source: 'count' }, + ]; + + const newData = []; + const fields = this.state.searchTwords ? tWordsFields : defaultFields; + + for (const item of data) { + const newItem = {}; + + for (const field of fields) { + const id = field.id; + let source = field.source?.split('.') || []; + let value = item; + + for (const key of source) { + value = value[key] || ''; + } + + newItem[id] = value; + } + newData.push(newItem); + } + + const dataStr = JSON.stringify(newData, null, 2); + + exportHelpers.getFilePath('searchResults', DOCUMENTS_PATH, 'json').then(pdfPath => { + console.log(`doPrint() - have TSV save path: ${pdfPath}`); + + fs.writeFile(pdfPath, dataStr, (error) => { + if (error) { + console.error(`saveTofile() - save error`, error); + } else { + console.log(`Wrote TSV successfully to ${pdfPath}`); + } + }); + }).catch(error => { + console.log(`Failed to select PDF path: `, error); + }); + } + + saveToTsvfile(data) { + const DOCUMENTS_PATH = path.join(USER_HOME, 'Documents'); + const defaultFields = [ + { id: 'sourceText', title: 'Source Text' }, + { id: 'sourceLemma', title: 'Source Lemma' }, + { id: 'morph', title: 'Source Morph' }, + { id: 'strong', title: 'Source Strongs' }, + { id: 'targetText', title: 'Target Text' }, + { id: 'alignedText', title: 'Aligned Text' }, + { id: 'count', title: 'Match Count' }, + { id: 'refs', title: 'References' }, + { id: 'config', title: 'Configuration' }, + ]; + const tWordsFields = [ + { id: 'sourceText', title: 'Source Text' }, + { id: 'sourceLemma', title: 'Source Lemma' }, + { id: 'morph', title: 'Source Morph' }, + { id: 'strong', title: 'Source Strongs' }, + { id: 'targetText', title: 'Translation Words' }, + { id: 'alignedText', title: 'Aligned Text' }, + { id: 'category', title: 'Category' }, + { id: 'count', title: 'Match Count' }, + { id: 'refs', title: 'References' }, + { id: 'config', title: 'Configuration' }, + ]; + + const fields = this.state.searchTwords ? tWordsFields : defaultFields; + + const dataLines = []; + const configFields = [ 'alignedBible', 'alignedBible2', 'caseSensitive', 'dualSearch', 'hideUsfmMarkers', 'matchWholeword', 'searchMaster', 'searchStr', 'searchTwords', 'searchType']; + const config = {}; + + configFields.forEach(id => { + const value = this.state[id]; + config[id] = `${value}`; + }); + + dataLines.push(fields.map(f => f.title).join('\t')); + + for (let i = 0; i < data.length; i++) { + const item = data[i]; + + const fieldData = fields.map(field => { + let value = ''; + const id = field.id; + + if (id === 'config') { + if (i === 0) { + value = JSON.stringify(config); + } + } else { + value = (item[id] || ''); + + if (Array.isArray(value)) { + value = value.join('; '); + } + } + + return `${value}`; + }); + + dataLines.push(fieldData.join('\t')); + } + + const dataStr = dataLines.join('\n'); + + exportHelpers.getFilePath('searchResults', DOCUMENTS_PATH, 'tsv').then(pdfPath => { + console.log(`doPrint() - have TSV save path: ${pdfPath}`); + + fs.writeFile(pdfPath, dataStr, (error) => { + if (error) { + console.error(`saveTofile() - save error`, error); + } else { + console.log(`Wrote TSV successfully to ${pdfPath}`); + } + }); + }).catch(error => { + console.log(`Failed to select PDF path: `, error); + }); + } + + /** + * show search results in table + * @returns {JSX.Element} + */ + showResults() { + if (this.props.open && this.state.found) { // if we have search results + if (this.state.found?.length) { // if search results are not empty, create table to show results + const ignoreBooks = this.ignoreBooksForTestament(); + const data = this.formatData(ignoreBooks); + const columnStyles = { + cellStyle: { + fontSize: '16px', + fontFamily: 'Ezra, Noto Sans', + }, + }; + const originalLang = this.state.alignmentData?.origLang; + const originalStyles = { + cellStyle: { + fontSize: (originalLang === 'hbo') ? '27px' : '19px', + fontFamily: 'Ezra, Noto Sans', + }, + }; + const localization = { + toolbar: { + searchTooltip: 'Filter Results', + searchPlaceholder: 'Filter Results', + }, + }; + const hide = this.state?.hide || {}; + const alignedColumn = this.getColumnTitle(this.state.alignedBible); + const alignedColumn2 = this.state.alignedBible2 ? this.getColumnTitle(this.state.alignedBible2) : ''; + const searchColumns = [ + !hide[SHOW_SOURCE_TEXT] && { title: (SOURCE_TEXT_LABEL), field: SHOW_SOURCE_TEXT, ...originalStyles }, + !hide[SHOW_SOURCE_LEMMA] && { title: (SOURCE_LEMMA_LABEL), field: SHOW_SOURCE_LEMMA, ...originalStyles }, + !hide[SHOW_MORPH] && { title: (SOURCE_MORPH_LABEL), field: SHOW_MORPH, ...columnStyles }, + !hide[SHOW_STRONGS] && { title: (STRONGS_LABEL), field: SHOW_STRONGS, ...columnStyles }, + !hide[SHOW_TARGET_TEXT] && { title: (TARGET_TEXT_LABEL), field: SHOW_TARGET_TEXT, ...columnStyles }, + this.state.searchTwords && { title: (alignedColumn), field: ALIGNED_TEXT, ...columnStyles }, + this.state.searchTwords && alignedColumn2 && { title: (alignedColumn2), field: ALIGNED_TEXT2, ...columnStyles }, + !hide[SHOW_MATCH_COUNT] && { title: (MATCH_COUNT_LABEL), field: SHOW_MATCH_COUNT, ...columnStyles }, + !hide[SHOW_REFERENCES] && { + title: (REFERENCES_LABEL), + field: SHOW_REFERENCES, + ...columnStyles, + render: rowData => <> { this.renderRefs(rowData.refs, rowData.targetText) } , + }, + ]; + let message = `Found ${data?.length || 0} matches`; + + if (ignoreBooks?.length) { + message += ` - ignored books: ${ignoreBooks.join(',')}`; + } + + const buttonStyle = { alignSelf: 'center', marginTop: '20px', width: 'auto', paddingLeft: '4px', paddingRight: '4px' }; + + return ( + <> +
{message}
+ + + item)} + data={data} + title={'Search Results:'} + icons={tableIcons} + style={{ fontSize: '16px' }} + options={{ + actionsCellStyle: { fontSize: '16px' }, + filterCellStyle: { fontSize: '16px' }, + headerStyle: { + fontSize: '16px', + fontWeight: 'bold', + }, + rowStyle: { fontSize: '16px' }, + searchFieldStyle: { fontSize: '16px' }, + paging: false, + }} + localization={localization} + /> + + ); + } else { // if search results are empty, show message + return ( + <> +
+
+ {'No results found!'} + + ); + } + } else { // if we don't have search results yet, prompt user that they need to do search + return ( + <> +
+
+ {'Need to Start Search!'} + + ); + } + } + + /** + * get title from bible key + * @param bibleKey + * @returns {string} + */ + getColumnTitle(bibleKey) { + const bible_ = bibleKey?.split('_') || []; + const alignedBible = bible_.length > 1 ? bible_.slice(0, 2).join('_') : bible_[0]; + const alignedColumn = `Aligned ${alignedBible} Column`; + return alignedColumn; + } + + /** + * format data for display + * @param ignoreBooks + * @returns {*} + */ + formatData(ignoreBooks) { + let data = this.state.found || []; + let hidden = this.state.hide || {}; + hidden = Object.keys(hidden).map(key => hidden[key] && key).filter(i => i); + + if (data?.length) { + const mergedData = {}; + const remainingColumns = [SHOW_SOURCE_TEXT, SHOW_MORPH, SHOW_SOURCE_LEMMA, SHOW_STRONGS, SHOW_TARGET_TEXT, ALIGNED_TEXT, ALIGNED_TEXT2].filter(item => !hidden.includes(item)); + + for (let i = 0; i < data.length; i ++) { + const row = data[i]; + const key = remainingColumns.map(key => (row[key] || '').toString()).join('&'); + const mergedDataItem = mergedData[key]; + + if (!mergedDataItem) { + mergedData[key] = row; + const newRefs = []; + + for (const ref of row.refs) { + if (!newRefs.includes(ref)) { + newRefs.push(ref); + } + } + + row.refs = newRefs; + } else { + let mergedRefs = mergedDataItem.refs; + + if (!mergedRefs) { + mergedDataItem.refs = []; + mergedRefs = mergedDataItem.refs; + } + + for (const ref of row.refs) { + if (!mergedRefs.includes(ref)) { + mergedRefs.push(ref); + } + } + } + } + data = Object.keys(mergedData).map(key => mergedData[key]); + } + + function zeroAdjustLength(text, len) { + while (text.length < len) { + text = '0' + text; + } + return text; + } + + function normalizeRef(ref) { + let [bookId, ref_] = (ref || '').trim().split(' '); + + if ( bookId && ref_ ) { + let [chapter, verse] = ref_.split(':'); + + if (chapter && verse) { + chapter = zeroAdjustLength(chapter, 3); + verse = zeroAdjustLength(verse, 3); + const bookNum = zeroAdjustLength(BIBLES_ABBRV_INDEX[bookId] || '', 3); + return `${bookNum}_${chapter}_${verse}`; + } + } + return null; + } + + function bibleRefSort(a, b) { + const akey = normalizeRef(a); + const bkey = normalizeRef(b); + // eslint-disable-next-line no-nested-ternary + return akey < bkey ? -1 : akey > bkey ? 1 : 0; + } + + // ignore books from other testament also + const ignoreBooks_ = ignoreBooks.concat(this.state?.ignoreBooks || []); + + data = data.map(item => { + let refs = item.refs; + let refs_ = refs.filter(refStr => refStr && !ignoreBooks_.includes(refStr.split(' ')[0])); + refs = refs_.sort(bibleRefSort); // sort refs in canonical order + + if (!refs || !refs.length) { // if no references left, then ignore + return null; + } + + // eslint-disable-next-line react/jsx-key + // const refSpans = refs.map(refStr => { refStr } ); + // const refStr = <> { refSpans.join('; ') } ; + const newItem = { + ...item, + refs, + count: refs?.length, + }; + return newItem; + }).filter(item => item); + return data; + } + + /** + * format refs array for output + * @param {array} refs + * @param {string} targetText - aligned target text + * @returns {JSX.Element} + */ + renderRefs(refs, targetText) { + return
+ { refs.map((ref, i) => { + this.showPopUpVerse(refs, i, e, targetText); + }} + > + {ref + ';\u00A0'} + )} +
; + } + + /** + * scale the base font size by factor + * @param factor + * @returns {string} + */ + getFontSize(factor) { + const baseFontSize = this.state.baseFontSize || 1; + const scaledFont = factor * baseFontSize; + return `${scaledFont.toFixed(2)}em`; + } + + /** + * redraw the last popup with the new settings + */ + refreshPopUp() { + if (popupLocation) { + delay(100).then(() => { + this.showPopUpVerse(popupLocation.refs, popupLocation.index, popupLocation.e, popupLocation.targetText); + }); + } + } + + /** + * show a popup verse for at element + * @param {string[]} refs - bible references to display + * @param {number} index - current + * @param {object} e - element to show popup for + * @param {string} targetText - aligned target text + */ + showPopUpVerse(refs, index, e, targetText) { + popupLocation = { refs, index, e, targetText }; + const popupTitlefontSize = this.getFontSize(1.1); + const subTitleFontSize = `1.3em`; + const verseFontSize = this.getFontSize(1); + const extraBibleKeys = this.state.extraBibleKeys || []; + const bibleKeys = [this.state.alignedBible]; + const bibleChoices = this.getAvailableBibles(); + + for (const key of extraBibleKeys) { + if (!bibleKeys.includes(key)) { + const found = bibleChoices?.find(bible => bible.key === key); + + if (found) { + bibleKeys.push(key); + } + } + } + + const versesContent = bibleKeys.map((bibleKey, i) => { + const content = this.getVerseContent(bibleKey, popupTitlefontSize, refs[index], !i && targetText); + return content; + }); + const contentStyle = { + display: 'flex', + alignItems: 'left', + flexDirection: 'column', + fontSize: verseFontSize, + }; + const content =
+ {/* eslint-disable-next-line arrow-body-style */} + {versesContent.map((item, pos) => { + return <> + {(pos > 0) && + <> +
+ + {`${item.bibleLabel}`} + { // delete the bible key + const key = item.bibleKey; + let extraBibleKeys = [...(this.state.extraBibleKeys || [])]; + extraBibleKeys = extraBibleKeys.filter(key_ => key_ !== key); + this.setState({ extraBibleKeys }); + this.refreshPopUp(); + }} + > + + + + + } +
{item.verseContent}
+ ; + })} + +
; + const style = { maxWidth: '600px' }; + const titleStyle = { display: 'flex', width: '-webkit-fill-available' }; + this.props.showPopover(versesContent[0].Title, content, e.target, style, titleStyle); + } + + addBible(bibleChoices) { + console.log('addBible() - Add Bible'); + + this.props.openAlertDialog( + <> +
{'Choose new Bible'}
+ { + const extraBibleKeys = [...(this.state.extraBibleKeys || [])]; + + if (!extraBibleKeys.includes(menuItem.key)) { + extraBibleKeys.push(menuItem.key); + this.setState({ extraBibleKeys }); + } + this.props.closeAlertDialog(); + this.refreshPopUp(); + }} + > + {bibleChoices.map(item => ( + + ))} + + , + false, + 'CANCEL', + () => this.props.closeAlertDialog(), + ); + } + + /** + * get list of available bibles for viewing content + * @returns {Object[]} + */ + getAvailableBibles() { + function keySort(a, b) { + const akey = a?.key; + const bkey = b?.key; + // eslint-disable-next-line no-nested-ternary + return akey < bkey ? -1 : akey > bkey ? 1 : 0; + } + + const resourceDir = path.join(env.home(), 'translationCore', 'resources'); + let bibleOptions = getAvailableBibles(resourceDir, false) || []; + + bibleOptions = bibleOptions.map(bible => { + const key = getKeyForBible(bible, ALIGNMENTS_KEY); + const label = this.getLabelForBible(bible, true); + + return { + ...bible, + key, + label, + }; + }); + return bibleOptions.sort(keySort); + } + + /** + * change font size for popup text + * @param up + */ + changeFontSize(up) { + let baseFontSize = this.state.baseFontSize || 1; + + if (up) { + baseFontSize *= 1.1; + } else { + baseFontSize /= 1.1; + } + console.log(`changeFontSize() - fontSize now ${baseFontSize}`); + this.setState({ baseFontSize }); + this.refreshPopUp(); + } + + /** + * generate verse content to show for bible + * @param {string} bibleKey + * @param {string} fontSize + * @param {string} ref + * @param {string} targetText - aligned target text + * @returns {{verseContent: unknown[], Title: JSX.Element}} + */ + getVerseContent(bibleKey, fontSize, ref, targetText) { + const bible = parseResourceKey(bibleKey); + const bibleLabel = this.getLabelForBible(bible, true); + const Title = ( + <> + {`${ref} - ${bibleLabel}`} + { + console.log('increase font'); + this.changeFontSize(true); + }} + > + + + { + console.log('decrease font'); + this.changeFontSize(false); + }} + > + + + { + console.log('previous ref'); + let i = (popupLocation.index || 0) - 1; + + if (i < 0) { + i = 0; + } + this.showPopUpVerse(popupLocation.refs, i, popupLocation.e, popupLocation.targetText); + }} + > + + + + = popupLocation.refs?.length - 1} + style = {{ + justifySelf: 'flex-right', + marginRight: '10px', + }} + onClick={() => { + console.log('next ref'); + const refsCount = popupLocation.refs?.length || 0; + let i = (popupLocation.index || 0) + 1; + + if (i >= refsCount) { + i = refsCount -1; + } + this.showPopUpVerse(popupLocation.refs, i, popupLocation.e, popupLocation.targetText); + }} + > + + + + ); + let verseText = ''; + const verseData = getVerseForKey(bibleKey, ref, bibles); + + if (Array.isArray(verseData)) { + if (verseData.length > 1) { + for (const verseItem of verseData) { + verseText += `${verseItem.chapter}:${verseItem.verse} - ${verseItem.verseData}\n`; + } + } else { + verseText = verseData[0].verseData; + } + } + + if (!verseText) { + verseText = 'Error: no data found'; + } else if (this.state.hideUsfmMarkers) { + const filtered = usfm.removeMarker(verseText).trim(); + verseText = filtered; + } + + const verseContent = highlightSelectedTextInVerse(verseText, targetText); + + return { + Title, + verseContent, + bibleLabel, + bibleKey, + }; + } + + /** + * find any ignored books for current testament + * @returns {string[]} + */ + ignoreBooksForTestament() { + let ignoreBooks_ = this.state?.ignore || []; + let testament = this.state?.books || []; + const ignoreBooks = ignoreBooks_.filter(bookId => testament.includes(bookId)); + return ignoreBooks; + } + + /** + * when user enters search string, save in state + * @param {string} search - new search string + */ + setSearchStr(search) { + this.setState({ searchStr: search }); + } + + /** + * when user selects bible to search, save in state + * @param {object} event - unused + * @param index - unused + * @param {string} value - new selection + */ + setSearchAlignedBible(event, index, value) { + this.selectAlignedBookToSearch(value); + } + + /** + * when user selects bible to search, save in state + * @param {object} event - unused + * @param index - unused + * @param {string} value - new selection + */ + setSearchAlignedBible2(event, index, value) { + if (this.state.searchMaster) { + this.setState({ alignedBible2: value }); + value && this.state.searchMaster && delay(100).then(() => { + this.downloadMasterIfMissing(); + this.selectAlignedBookToSearch(value, 2); + }); + } else { + this.selectAlignedBookToSearch(value, 2); + } + } + + /** + * select book and testament + * @param {string} key + * @param {number} searchNum - if number is two, then load for second search + */ + async selectAlignedBookToSearch(key, searchNum = 1) { + console.log(`selectAlignedBookToSearch(${key}) for ${searchNum}`); + const alignmentKey = (searchNum === 2) ? 'alignedBible2' : 'alignedBible'; + + if (key) { + if (!this.state.selectingAlignments) { + this.setState({ selectingAlignments: true }); + await delay(100); + await this.loadAlignmentData(key, async (success, errorMessage) => { + bibles = {}; + + if (success) { + //const key = `${bible.languageId}_${bible.resourceId}_${(encodeParam(bible.owner))}_${bible.origLang}_testament_${encodeParam(bible.version)}`; + const [, , , origLang] = key.split('_'); + let books = null; + let ignoreBooks = null; + const isNT = origLang === NT_ORIG_LANG; + + if (this.state.dualSearch) { + books = [...OT_BOOKS, ...NT_BOOKS]; + ignoreBooks = []; + } else if (isNT) { + books = NT_BOOKS; + ignoreBooks = OT_BOOKS; + } else { + books = OT_BOOKS; + ignoreBooks = NT_BOOKS; + } + + console.log(`selectAlignedBookToSearch(${key}) - setting bible`); + this.setState({ + [alignmentKey]: key, + books, + ignoreBooks, + isNT, + }); + this.props.closeAlertDialog(); + console.log(`selectAlignedBookToSearch(${key}) - loading twords index`); + await this.loadTWordsIndex(key, false, searchNum === 2); + console.log(`selectAlignedBookToSearch(${key}) - loaded twords index`); + this.state.searchMaster && this.downloadMasterIfMissing(); + } else { + console.warn(`selectAlignedBookToSearch(${key}) - ERROR setting bible: ${errorMessage}`); + + if (errorMessage) { + this.props.openAlertDialog(errorMessage); + } + } + console.log(`selectAlignedBookToSearch(${key}) - finished loading alignment data`); + }, searchNum); + console.log(`selectAlignedBookToSearch(${key}) for ${searchNum} - indexing done`); + this.setState({ selectingAlignments: false }); + } else { + console.log(`selectAlignedBookToSearch(${key}) for ${searchNum} - already selecting`); + } + } + } + + downloadMasterIfMissing() { + const message = 'Do you want to download the master branch of the aligned bibles currently selected?'; + this.updateMaster(message, false); + } + + /** + * get twords index + * @param {string} alignmentsKey - alignments key + * @param {boolean} force - force index generation + * @param {boolean} secondAlignmentKey - if true then we load second alignments key + */ + async loadTWordsIndex(alignmentsKey, force = false, secondAlignmentKey = false) { + const stateKey = secondAlignmentKey ? 'tWordsIndex2' :'tWordsIndex'; + console.log(`loadTWordsIndex(${alignmentsKey}) - starting`); + + if (alignmentsKey && this.state.searchTwords) { + const resource = parseResourceKey(alignmentsKey); + const res = addTwordsInfoToResource(resource, USER_RESOURCES_PATH); + + if (!res) { // resource no longer present + this.setState({ alignedBible: null }); + this.loadAlignmentSearchOptionsWithUI(); + console.log(`loadTWordsIndex(${alignmentsKey}) - resource no longer present`); + return; + } + + const tWordsKey = getTwordsKey(res); + let tWordsIndex = getTwordsIndex(tWordsKey); + + if (tWordsIndex && !force) { + console.log(`loadTWordsIndex(${alignmentsKey}) - already have index`); + this.setState({ [stateKey]: tWordsIndex }); + } else { + console.log(`loadTWordsIndex(${alignmentsKey}) - indexing tWords`); + const indexingMsg = `Indexing translationWords for '${alignmentsKey}':`; + + const tWordsIndex = await indexTwords(USER_RESOURCES_PATH, resource, async (percent) => { + await this.showMessage(<> {indexingMsg}
{`${100 - percent}% left`} , true); + }); + + console.log(`loadTWordsIndex(${alignmentsKey}) - tWords index finished`); + this.props.closeAlertDialog(); + saveTwordsIndex(tWordsKey, tWordsIndex); + this.setState({ [stateKey]: tWordsIndex }); + } + } + } + + /** + * when user updates fields to search, then save in state + * @param {object} event - unused + * @param {number} index - unused + * @param {string[]} values + */ + setSearchFields(event, index, values) { + this.setState({ searchType: values }); + } + + /** + * iterate through search options to find a match for key + * @param {string} key + * @returns {{label: string, key: string, stateKey: string} | {label: string, key: string, stateKey: string}} + */ + findSearchItem(key) { + const found = this.getSearchOptions().find(item => item.key === key); + return found; + } + + /** + * when user updates search options (such as whole word or case insensitive), save in state + * @param event + * @param index + * @param values + */ + setSearchTypes(event, index, values) { + const hide = {}; + const searchType = []; + // hide = this.state?.hide || {}; + + for (const item of showMenuItems) { // see what columns to display + const selected = this.isItemPresent(values, item.key); + hide[item.key] = !selected; + } + + for (const item of this.getSearchFieldOptions()) { // see what fields to search + const selected = this.isItemPresent(values, item); + + if (selected) { + searchType.push(item); + } + } + + // basic options + const basicOptions = {}; + + searchOptions.forEach(item => { + const searchOption = this.findSearchItem(item.key); + const selected = this.isItemPresent(values, item.key); + basicOptions[searchOption.stateKey] = selected; + }); + + if (values.includes(SEARCH_DUAL) && !this.state.dualSearch) { // if toggling on dual search + // on toggle on of dual search, clear any previous bible 2 selections + basicOptions.alignedBible2 = null; + basicOptions.alignmentData2 = null; + } + + if (values.includes(SEARCH_MASTER) && !this.state.searchMaster) { // if toggling on searching of master branch + this.loadAlignmentSearchOptions(true); // update the list first + + delay(100).then(() => { + this.downloadMasterIfMissing(); + }); + } + + if (values.includes(REFRESH_MASTER)) { + const message = 'Do you want to refresh the master branch of the aligned bibles currently selected?'; + this.updateMaster(message, true); + } + + if (values.includes(CLEAR_INDEX_DATA)) { + this.props.openOptionDialog('Do you want do delete all cached alignment search data? This clears out all alignment search data (new as well as old) and downloaded master branch data to free up disk space. Alignments will be recreated for bibles you select for search.', + (buttonPressed) => { + if (buttonPressed === OkButton) { + deleteCachedAlignmentData(); + this.handleClose(); + this.props.closeAlertDialog(); + } else { // did not want to delete + this.props.closeAlertDialog(); + } + }, + OkButton, + CancelButton); + } + + const types = { + ...basicOptions, + hide, + searchType, + }; + + this.setState(types); + + if (types.searchTwords !== this.state.searchTwords) { // if changed, reload + delay(100).then(async () => { + this.state.searchMaster && this.downloadMasterIfMissing(); + await this.loadAlignmentSearchOptionsWithUI(); + await this.loadTWordsIndex(this.state.alignedBible); + await this.loadTWordsIndex(this.state.alignedBible, false, true); + }); + } + } + + /** + * if we downloaded or selected master + * @param bibleKey + * @param {boolean} isPrimarySearchBible + * @param {boolean} removeOld + * @param {boolean} updateAlways - if true, update key always, otherwise only update if current key in=s not master + */ + async updateBibleKeyToMaster(bibleKey, isPrimarySearchBible, removeOld = false, updateAlways = false) { + const resource = bibleKey && parseResourceKey(bibleKey); + + if (removeOld) { + removeIndices(resource); + } + + if (resource && (updateAlways || (resource.version !== 'master'))) { + resource.version = 'master'; + const newBiblekey = getKeyForBible(resource, ALIGNMENTS_KEY); + + if (isMasterResourceDownloaded(resource)) { + if (isPrimarySearchBible) { + await this.selectAlignedBookToSearch(newBiblekey); + } else { + this.setState({ alignedBible2: newBiblekey }); + } + } + } + } + + /** + * confirm before download of resources + * @param message + * @param download - if true, always download + */ + updateMaster(message, download) { + if (!this.state.updatingMaster) { + this.setState({ updatingMaster: true }); + + delay(100).then(async () => { + this.downloadMasterIfMissing(); + + const resources = []; + const bibles = [this.state.alignedBible]; + + if (this.state.alignedBible2 && (this.state.searchTwords || this.state.dualSearch) + && (this.state.alignedBible !== this.state.alignedBible2)) { + bibles.push(this.state.alignedBible2); + } + + for (const bibleKey of bibles) { + const resource = bibleKey && parseResourceKey(bibleKey); + + if (resource) { + if ((bibleKey === this.state.alignedBible) || (bibleKey === this.state.alignedBible2)) { + resource.isPrimarySearchBible = true; + } + resource.version = 'master'; + resource.bibleKey = getKeyForBible(resource); + + if (!download) { + if (isMasterResourceDownloaded(resource)) { + continue; // skip if already downloaded + } + } + + resources.push(resource); + } + } + + if (!resources.length) { // we didn't need to download, but make sure alignments selected + for (const bibleKey of bibles) { + await this.updateBibleKeyToMaster(bibleKey, (bibleKey === this.state.alignedBible) || (bibleKey === this.state.alignedBible2)); + } + this.setState({ updatingMaster: false }); + return; + } + + this.props.openOptionDialog(message, + (buttonPressed) => { + if (buttonPressed === OkButton) { + this.props.confirmOnlineAction( + async () => { + let error; + const folder = path.join(ALIGNMENT_DATA_DIR); + + for (const resource_ of resources) { + console.log('Downloading', resource_); + await this.showMessage(`Downloading: ${resource_.bibleKey}`, true); + error = await downloadBible(resource_, folder); + + if (error) { + console.log(`Error downloading ${resource_.bibleKey}`, error); + await this.showMessage(`Download Error: ${resource_.bibleKey}`); + break; + } + + this.loadAlignmentSearchOptions(true); // update the options + await delay(100); + this.updateBibleKeyToMaster(resource_.bibleKey, resource_.isPrimarySearchBible, true, true); + await this.showMessage(`Downloading: ${resource_.bibleKey}`, true); + } + + this.setState({ updatingMaster: false }); + + if (!error) { + this.props.closeAlertDialog(); + } else { + this.props.openOptionDialog( + 'Download error', + () => this.props.closeAlertDialog(), + 'OK', + ); + } + }, + () => { // we do not want to go online + this.setState({ updatingMaster: false }); + this.props.closeAlertDialog(); + }, + ); + } else { // did not want to download now + this.setState({ updatingMaster: false }); + this.props.closeAlertDialog(); + } + }, + OkButton, + CancelButton); + }); + } else { + console.log(`updateMaster() - already updating`); + } + } + + /** + * when user clicks close, clear search results and call onClose + */ + handleClose() { + this.setState({ + alignmentData: null, + alignmentData2: null, + alignedBibles: null, + tWordsIndex: null, + found: null, + }); // clear data + + const onClose = this.props.onClose; + onClose && onClose(); + bibles = {}; + } + + /** + * when user clicks search button, do search of indexed data + */ + startSearch() { + console.log('AlignmentSearchDialogContainer - start search'); + this.showMessage('Doing Search', true).then(() => { + this.setState({ found: null }); + const state = this.state; + const searchTwords = state.searchTwords; + const config = { + fullWord: state.matchWholeWord, + inOrder: state.inOrder, + caseInsensitive: !state.caseSensitive, + searchTwords, + searchLemma: this.isSearchFieldSelected(SEARCH_LEMMA), + searchSource: this.isSearchFieldSelected(SEARCH_SOURCE), + searchTarget: this.isSearchFieldSelected(SEARCH_TARGET), + searchStrong: this.isSearchFieldSelected(SEARCH_STRONG), + searchRefs: !searchTwords && this.isSearchFieldSelected(SEARCH_REFS), + }; + let found = []; + const alignmentData2 = state.dualSearch && state.alignmentData2 || null; + + try { + found = multiSearchAlignments(state.alignmentData, state.tWordsIndex, state.searchStr, config, alignmentData2, state.tWordsIndex2) || []; + } catch (e) { + console.error('AlignmentSearchDialogContainer - search error', e); + this.showMessage(`Search Error`); + } + + if (config.searchTwords) { + getTwordALignments(found, state.alignedBible, bibles, ALIGNED_TEXT); + getTwordALignments(found, state.alignedBible2, bibles, ALIGNED_TEXT2); + } + + console.log(`AlignmentSearchDialogContainer - finished search, found ${found.length} items`); + delay(100).then(() => { + this.setState({ found }); + this.props.closeAlertDialog(); + }); + }); + } + + /** + * check state data to see if search option is selected + * @param {string} key + * @returns {boolean} + */ + isSearchFieldSelected(key) { + const searchType = this.state.searchType; + return this.isItemPresent(searchType, key); + } + + /** + * check state data to see if search option is selected + * @param {string} key + * @returns {boolean} + */ + isBookEnabled(key) { + const ignoredBooks = this.state.ignore || []; + const found = this.isItemPresent(ignoredBooks, key); + return !found; + } + + /** + * check state data to see if search option is selected + * @param {string} key + * @returns {boolean} + */ + toggleBook(key) { + const ignoredBooks = this.state.ignore || []; + const found = this.isItemPresent(ignoredBooks, key); + let newIgnore = [...ignoredBooks]; + + if (found) { + newIgnore = ignoredBooks.filter(item => item !== key); + } else { + newIgnore.push(key); + } + this.setState({ ignore: newIgnore }); + } + + /** + * set all or clear all books in bible + * @param {boolean} enable - if true then all books are enabled, otherwise all books are cleared + */ + setAll(enable) { + let newIgnore = [...(this.state.ignore || [])]; + + for (const bible of this.state.books) { + if (enable) { + newIgnore = newIgnore.filter(item => item !== bible); + } else if (!newIgnore.includes(bible)) { + newIgnore.push(bible); + } + } + this.setState({ ignore: newIgnore }); + } + + /** + * search array to see if it contains key + * @param {string[]} array - to search + * @param {string} key + * @returns {boolean} + */ + isItemPresent(array, key) { + return array && array.indexOf(key) >= 0; + } + + /** + * generate list of currently selected search options + * @param {string} options + * @returns {string[]} + */ + getSelectedOptions(options) { + const hide = this.state?.hide || {}; + let selections = options.map(item => !!this.state[item.stateKey] && item.key); + selections = selections.filter(item => item); + let selections2 = showMenuItems.map(item => !hide[item.key] && item.key); + selections2 = selections2.filter(item => item); + const searchType = this.state?.searchType || []; + let selections3 = searchFieldOptions.map(item => searchType.includes(item) && item); + selections3 = selections3.filter(item => item); + selections = selections.concat(selections2); + selections = selections.concat(selections3); + return selections; + } + + // get list of bibles for second aligned bible + getAlignedBibles2() { + let alignedBibles = [{ + key: '', + label: '', + }]; + + if (this.state.alignedBibles?.length) { + alignedBibles = alignedBibles.concat(this.state.alignedBibles); + } + return alignedBibles; + } + + render() { + const { + open, + translate, + } = this.props; + + const fullScreen = { maxWidth: '100%', width: '100%' }; + // const partialScreen = { maxWidth: '768px', width: '75%' }; + const contentStyle = fullScreen; + const testament = this.state?.books || []; + const bookLabels = this.state?.isNT ? BIBLE_BOOKS.newTestament : BIBLE_BOOKS.oldTestament; + + return ( + +
+ this.setSearchStr(e.target.value)} + autoFocus={true} + style={{ width: '100%' }} + /> +
+
+ + { + this.state.alignedBibles?.map(item => ( + + )) + } + +
+
+ + {this.getSearchOptions().map(item => ( + + ))} + + {'Select Columns to Show:'} + {showMenuItems.map(item => { + const hide = this.state?.hide || {}; + return ( + + ); + })} + + {'Select Fields to Search:'} + { + this.getSearchFieldOptions().map(item => ( + + )) + } + + {'Select Books to Search:'} + this.setAll(true)} + /> + this.setAll(false)} + /> + { + testament.map(item => ( + this.toggleBook(item)} + /> + )) + } + +
+
+ {(this.state.searchTwords || this.state.dualSearch) && +
+ + { + this.getAlignedBibles2()?.map(item => ( + + )) + } + +
+ } + { this.showResults() } +
+
+ ); + } + + getSearchOptions() { + const searchOptions_ = [...searchOptions]; + + if (this.state.searchMaster) { + searchOptions_.push(searchOptionRefreshMaster); + } + + searchOptions_.push(searchOptionClearIndexData); + return searchOptions_; + } +} + +AlignmentSearchDialogContainer.propTypes = { + translate: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + open: PropTypes.bool.isRequired, + manifest: PropTypes.object.isRequired, + openAlertDialog: PropTypes.func.isRequired, + openOptionDialog: PropTypes.func.isRequired, + closeAlertDialog: PropTypes.func.isRequired, + saveSettings: PropTypes.func.isRequired, + savedSettings: PropTypes.object.isRequired, + showPopover: PropTypes.func.isRequired, + confirmOnlineAction: PropTypes.func.isRequired, +}; + +const mapStateToProps = (state) => ({ + manifest: getProjectManifest(state), + savedSettings: getSetting(state, SEARCH_SETTINGS_KEY), +}); + +const mapDispatchToProps = { + openAlertDialog: (alertMessage, loading, buttonText = null, callback = null) => openAlertDialog(alertMessage, loading, buttonText, callback), + openOptionDialog: (alertMessage, callback, button1Text, button2Text, buttonLinkText = null, callback2 = null, notCloseableAlert = false) => openOptionDialog(alertMessage, callback, button1Text, button2Text, buttonLinkText, callback2, notCloseableAlert), + closeAlertDialog: () => closeAlertDialog(), + saveSettings: (value) => setSetting(SEARCH_SETTINGS_KEY, value), + showPopover: (title, bodyText, positionCoord, style = {}, titleStyle = {}, bodyStyle = {}) => showPopover(title, bodyText, positionCoord, style, titleStyle, bodyStyle), + confirmOnlineAction: (onConfirm, onCancel) => OnlineModeConfirmActions.confirmOnlineAction(onConfirm, onCancel), +}; + +export default connect(mapStateToProps, mapDispatchToProps)(AlignmentSearchDialogContainer); diff --git a/src/js/containers/AppMenu.js b/src/js/containers/AppMenu.js index a380cb028e..5911712a26 100644 --- a/src/js/containers/AppMenu.js +++ b/src/js/containers/AppMenu.js @@ -5,17 +5,20 @@ import FeedbackIcon from 'material-ui/svg-icons/action/question-answer'; import SyncIcon from 'material-ui/svg-icons/notification/sync'; import UpdateIcon from 'material-ui/svg-icons/action/update'; import SettingsIcon from 'material-ui/svg-icons/action/settings'; +import SearchIcon from '@material-ui/icons/Search'; import MenuItem from 'material-ui/MenuItem'; import PopoverMenu from '../components/PopoverMenu'; import LocaleSettingsDialogContainer from './LocaleSettingsDialogContainer'; import FeedbackDialogContainer from './FeedbackDialogContainer'; import SoftwareUpdatesDialog from './SoftwareUpdateDialog'; import SourceContentUpdatesDialogContainer from './SourceContentUpdatesDialogContainer'; +import AlignmentSearchDialogContainer from './AlignmentSearchDialogContainer'; const APP_UPDATE = 'app_update'; const CONTENT_UPDATE = 'content_update'; const FEEDBACK = 'feedback'; const APP_LOCALE = 'app_locale'; +const APP_SEARCH = 'app_search'; /** * This component renders the global application menu. @@ -112,6 +115,9 @@ class AppMenu extends React.Component { }/> + }/> + + ); diff --git a/src/js/helpers/FileConversionHelpers/UsfmFileConversionHelpers.js b/src/js/helpers/FileConversionHelpers/UsfmFileConversionHelpers.js index ce3340847d..be1d6472e7 100644 --- a/src/js/helpers/FileConversionHelpers/UsfmFileConversionHelpers.js +++ b/src/js/helpers/FileConversionHelpers/UsfmFileConversionHelpers.js @@ -103,7 +103,7 @@ export const getOriginalLanguageChapterResources = function (projectBibleID, cha * remove single trailing newline from end of string * @param text */ -const trimNewLine = function (text) { +export const trimNewLine = function (text) { if (text && text.length) { let lastChar = text.substr(-1); diff --git a/src/js/helpers/__tests__/alignmentSearchHelpers.test.js b/src/js/helpers/__tests__/alignmentSearchHelpers.test.js new file mode 100644 index 0000000000..daedd81072 --- /dev/null +++ b/src/js/helpers/__tests__/alignmentSearchHelpers.test.js @@ -0,0 +1,178 @@ + +import path from 'path-extra'; +import { findBestMatchesForTargetText, indexTwords } from '../alignmentSearchHelpers'; + +jest.unmock('fs-extra'); + +describe('test findBestMatchesForTargetText', () => { + it('test discontiguous jdg 13:23', () => { + // given + const expectedPos = [ 207, 213, 216, 225, 233, 236, 239 ]; + const targetText = 'would he have allowed us to hear'; + const verseText = 'But his wife replied to him, “If Yahweh had desired to kill us, he would not have taken from our hand the whole burnt offering and the offering. He would not have shown us all these things, and at this time would he have not allowed us to hear about this.”'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with quotes jdg 13:23', () => { + // given + const expectedPos = [30, 33, 239, 244 ]; + const targetText = 'If Yahweh hear about this'; + const verseText = 'But his wife replied to him, “If Yahweh had desired to kill us, he would not have taken from our hand the whole burnt offering and the offering. He would not have shown us all these things, and at this time would he have not allowed us to hear about this”'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with exclamation 1sa 13:3', () => { + // given + const expectedPos = [ 155, 171 ]; + const targetText = 'Let hear'; + const verseText = 'And Jonathan struck down the garrison of the Philistines that was at Geba and the Philistines heard. And Saul blew with the horn in all the land, saying, “Let the Hebrews hear!”'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with {} gen 41:15', () => { + // given + const expectedPos = [ 111, 117, 121 ]; + const targetText = 'that you hear'; + const verseText = 'Then Pharaoh said to Joseph, "I dreamed a dream, but no one could interpret it. But I heard about you, saying {that} you hear a dream {and are able} to interpret it.”'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with ? jdg 5:16', () => { + // given + const expectedPos = [ 54, 64, 68 ]; + const targetText = 'signaling for flocks'; + const verseText = 'Why did you sit among the campfires,\n' + + 'in order to hear signaling for flocks?\n' + + 'As for the divisions of Reuben\n' + + 'there were great resolutions of heart.'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with period jdg 5:16', () => { + // given + const expectedPos = [ 124, 136, 139 ]; + const targetText = 'resolutions of heart'; + const verseText = 'Why did you sit among the campfires,\n' + + 'in order to hear signaling for flocks?\n' + + 'As for the divisions of Reuben\n' + + 'there were great resolutions of heart.'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with comma jdg 5:16', () => { + // given + const expectedPos = [ 16, 22, 26 ]; + const targetText = 'among the campfires'; + const verseText = 'Why did you sit among the campfires,\n' + + 'in order to hear signaling for flocks?\n' + + 'As for the divisions of Reuben\n' + + 'there were great resolutions of heart.'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with new line jdg 5:16', () => { + // given + const expectedPos = [ 87, 97, 100 ]; + const targetText = 'divisions of Reuben'; + const verseText = 'Why did you sit among the campfires,\n' + + 'in order to hear signaling for flocks?\n' + + 'As for the divisions of Reuben\n' + + 'there were great resolutions of heart.'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); + + it('test discontiguous with ‘ mrk 12:29', () => { + // given + const expectedPos = [ 32, 38, 40 ]; + const targetText = 'Hear O Israel'; + const verseText = 'Jesus answered, “The first is, ‘Hear, O Israel, the Lord our God, the Lord is one.'; + + // when + const { targetPos, targetParts } = findBestMatchesForTargetText(targetText, verseText); + + // then + expect(targetPos).toEqual(expectedPos); + expect(targetParts.join(' ')).toEqual(targetText); + }); +}); + +const TCORE_RESOURCES_FOLDER = path.join('/Users/blm/translationCore', 'resources'); + +describe.skip('test indexTwords with callback', () => { + it('test Door43', async () => { + // given + const resource = { + languageId: 'en', + resourceId: 'ult', + owner: 'Door43-Catalog', + bookId: 'tit', + }; + + // when + // eslint-disable-next-line require-await + await indexTwords(TCORE_RESOURCES_FOLDER, resource, async (percent) => { + console.log( `${100 - percent}% left`); + }); + }, 1000000); + + it('test unfoldingWord without callback', async () => { + // given + const resource = { + languageId: 'en', + resourceId: 'ult', + owner: 'unfoldingWord', + bookId: 'tit', + }; + + // when + await indexTwords(TCORE_RESOURCES_FOLDER, resource); + }, 1000000); +}); diff --git a/src/js/helpers/alignmentSearchHelpers.js b/src/js/helpers/alignmentSearchHelpers.js new file mode 100644 index 0000000000..0a59ffbee1 --- /dev/null +++ b/src/js/helpers/alignmentSearchHelpers.js @@ -0,0 +1,2245 @@ +import path from 'path-extra'; +import fs from 'fs-extra'; +import xre from 'xregexp'; +import React from 'react'; +import env from 'tc-electron-env'; +import * as SPT from 'string-punctuation-tokenizer'; +import SourceContentUpdater, { resourcesHelpers, apiHelpers } from 'tc-source-content-updater'; +import wordaligner from 'word-aligner'; +import { getVerses } from 'bible-reference-range'; +import { + BIBLE_BOOKS, + NT_ORIG_LANG, + NT_ORIG_LANG_BIBLE, + OT_ORIG_LANG, + OT_ORIG_LANG_BIBLE, +} from '../common/BooksOfTheBible'; +import { + DEFAULT_ORIG_LANG_OWNER, + DEFAULT_OWNER, + USER_RESOURCES_PATH, +} from '../common/constants'; +import { + getUsfmForVerseContent, + trimNewLine, +} from './FileConversionHelpers/UsfmFileConversionHelpers'; +import * as BibleHelpers from './bibleHelpers'; +import { getMostRecentVersionInFolder } from './originalLanguageResourcesHelpers'; +import { getOriginalLangOwner } from './ResourcesHelpers'; +const normalizer = SPT.normalizer; + +// eslint-disable-next-line no-useless-escape +const START_WORD_REGEX = '(?<=[\\s,.:;“"\'‘({]|^)'; +const START_WORD_REGEX_WJ = '(?<=[\\s,.:;“"\'‘({\\p{Cc}]|^)'; // same as START_WORD_REGEX with word-joiner +// eslint-disable-next-line no-useless-escape +const END_WORD_REGEX = '(?=[\\s,.:;“"\'‘!?)}]|$)'; +const END_WORD_REGEX_WJ = '(?=[\\s,.:;“"\'‘!?)}\\p{Cc}]|$)'; // same as END_WORD_REGEX with word-joiner +// eslint-disable-next-line no-unused-vars +const WORD_JOINER = '\u2060'; // U+2060 +export const ALIGNMENTS_KEY = 'alignmentsIndex5'; // increment the number each time the code changes and breaks compatibility with old index +export const TWORDS_KEY = 'tWordsIndex2'; // increment the number each time the code changes and breaks compatibility with old index +export const OT_BOOKS = Object.keys(BIBLE_BOOKS.oldTestament); +export const NT_BOOKS = Object.keys(BIBLE_BOOKS.newTestament); +const TCORE_FOLDER = path.join(env.home(), 'translationCore'); +export const ALIGNMENT_DATA_DIR = path.join(TCORE_FOLDER, 'alignmentData'); +export const MASTER_DOWNLOADS = path.join(ALIGNMENT_DATA_DIR, 'downloads'); +const MISSING_DATA_SYMBOL = '-'; + +/** + * get keys for alignments and do sort by locale + * @param {object} alignments + * @param {string} langID - language to use for sorting + * @returns {string[]} + */ +export function getSortedKeys(alignments, langID) { + let keys = Object.keys(alignments); + + keys = keys.sort(function (a, b) { + return a.localeCompare(b, langID, { sensitivity: 'base' }); + }); + + return keys; +} + +/** + * count total alignments in alignments object + * @param {object} alignments + * @returns {number} - total + */ +export function getCount(alignments) { + const keys = Object.keys(alignments); + let count = 0; + + for (const key of keys) { + const alignments_ = alignments[key]; + count += alignments_.length; + } + return count; +} + +/** + * generate multiple indices for alignments: + * by lemma + * by target + * by source + * by strong's number + * @param {object} alignments + * @returns {{strongAlignments: {}, lemmaAlignments: {}, targetAlignments: {}, sourceAlignments: {}}} + */ +export function indexAlignments(alignments) { + const lemmaAlignments = {}; + const targetAlignments = {}; + const sourceAlignments = {}; + const strongAlignments = {}; + const sourceKeys = Object.keys(alignments); + + for (const sourceKey of sourceKeys) { + if (!sourceAlignments[sourceKey]) { + sourceAlignments[sourceKey] = []; + } + + const targetAlignments_ = alignments[sourceKey]; + const targetKeys = Object.keys(targetAlignments_); + + for (const targetKey of targetKeys) { + const targetAlignment = targetAlignments_[targetKey]; + const sourceLemma = targetAlignment.sourceLemma; + + if (!lemmaAlignments[sourceLemma]) { + lemmaAlignments[sourceLemma] = []; + } + lemmaAlignments[sourceLemma].push(targetAlignment); + const strong = targetAlignment.strong; + + if (!strongAlignments[strong]) { + strongAlignments[strong] = []; + } + strongAlignments[strong].push(targetAlignment); + const targetText = targetAlignment.targetText; + + if (!targetAlignments[targetText]) { + targetAlignments[targetText] = []; + } + + targetAlignments[targetText].push(targetAlignment); + sourceAlignments[sourceKey].push(targetAlignment); + } + } + return { + lemmaAlignments, + targetAlignments, + sourceAlignments, + strongAlignments, + }; +} + +/** + * do regex search of keys for search string + * @param {string[]} keys + * @param {string} searchStr - string to match + * @param {string} flags - regex flags for searching + * @returns {string[]} + */ +export function regexSearch(keys, searchStr, flags) { + const found = []; + const regex = xre(searchStr, flags); + + for (const key of keys) { + const results = regex.test(key); + + if (results) { + found.push(key); + } + } + + return found; +} + +/** + * do regex to see if text is match for searchStr/flags + * @param {string} text + * @param {string} searchStr - string to match + * @param {string} flags - regex flags for searching + * @returns {Object[]} + */ +export function isMatch(text, searchStr, flags) { + const regex = xre(searchStr, flags); + const results = regex.test(text); + return results; +} + +/** + * build regex search string based on flags + * @param {string} search - string to search + * @param {boolean} fullWord - if true do full word matching + * @param {boolean} caseInsensitive -if true do cse insensitive matching + * @param {boolean} wordJoiner - if true then split words at word-joiner character + * @returns {boolean} {{search: string, flags: string}} + */ +export function buildSearchRegex(search, fullWord, caseInsensitive, wordJoiner = false) { + let flags = 'u'; // enable unicode support + search = xre.escape(normalizer((search || '').trim())); // escape any special character we are trying to match + + if (search.includes('\\?') || search.includes('\\*')) { // check for wildcard characters + search = search.replaceAll('\\?', '\\S{1}'); + search = search.replaceAll('\\*', '\\S*'); + } + + if (fullWord) { + if (wordJoiner) { + search = `${START_WORD_REGEX_WJ}${search}${END_WORD_REGEX_WJ}`; + } else { + search = `${START_WORD_REGEX}${search}${END_WORD_REGEX}`; + } + } + + if (caseInsensitive) { + flags += 'i'; + } + + return { search, flags }; +} + +/** + * load alignments from alignments json + * @param {string} alignmentsPath + * @returns {null|{targetLang: string, strong: ({}|{alignments: {}}|*), alignments, lemma: ({}|{alignments: {}}|*), origLang: string, descriptor: string, source: ({}|{alignments: {}}|*), target: ({}|{alignments: {}}|*)}} + */ +export function loadAlignments(alignmentsPath) { + try { + const alignments = fs.readJsonSync(alignmentsPath); + const baseName = path.parse(alignmentsPath).name; + const [targetLang, descriptor, origLang] = baseName.split('_'); + + return { + alignments: alignments.alignments, + bibleIndex: alignments.bibleIndex, + targetLang, + descriptor, + origLang, + target: alignments.targetAlignments, + lemma: alignments.lemmaAlignments, + source: alignments.sourceAlignments, + strong: alignments.strongAlignments, + }; + } catch (e) { + console.warn(`loadAlignments() - could not read ${alignmentsPath}`); + } + return null; +} + +/** + * search object keys for matches with search string, when match is found get matching alignment from alignments + * @param {string} searchStr - string to match + * @param {string} flags - regex flags + * @param {string[]} objectKeys - keys for alignments object + * @param {object} alignments - index alignments + * @returns {object[]} + */ +export function searchAlignmentsSub(searchStr, flags, objectKeys, alignments) { + const foundKeys = regexSearch(objectKeys, searchStr, flags); + const foundAlignments = []; + + for (const key of foundKeys) { + const alignments_ = alignments[key]; + Array.prototype.push.apply(foundAlignments, alignments_); + } + return foundAlignments; +} + +/** + * first convert alignment refs to refsStr and then search object keys for matches with search string, when match is found get matching alignment from alignments + * @param {string} searchStr - string to match + * @param {string} flags - regex flags + * @param {string[]} objectKeys - keys for alignments object + * @param {object} alignments - index alignments + * @param {object[]} alignmentsArray - list of alignment data that has been indexed + * @returns {object[]} + */ +export function searchRefs(searchStr, flags, objectKeys, alignments, alignmentsArray) { + const refsAlignments = {}; + + // create refs alignments object + for (const key of objectKeys) { + const alignments_ = alignments[key]; + + for (const index of alignments_) { + const alignment = alignmentsArray[index]; + const refs = alignment?.refs || []; + const refsStr = refs.join(' '); + + if (!refsAlignments[refsStr]) { + refsAlignments[refsStr] = []; + } + refsAlignments[refsStr].push(index); + } + } + + const foundAlignments = searchAlignmentsSub(searchStr, flags, Object.keys(refsAlignments), refsAlignments); + return foundAlignments; +} + +/** + * search object keys for matches with search string, when match is found get matching alignment from alignments + * @param {string} searchStr - string to match + * @param {boolean} fullWord - if true do full word matching + * @param {boolean} caseInsensitive -if true do cse insensitive matching + * @param {string[]} objectKeys - keys for alignments object + * @param {object} alignments - index alignments + * @returns {object[]} + */ +export function searchAlignments(searchStr, fullWord, caseInsensitive, objectKeys, alignments) { + const { search, flags } = buildSearchRegex(searchStr, fullWord, caseInsensitive); + const foundAlignments = searchAlignmentsSub(search, flags, objectKeys, alignments); + return foundAlignments; +} + +/** + * search searchData for matches with search string, merge found alignments into found + * @param {string} searchStr - string to match + * @param {string} flags - regex flags + * @param {object} searchData - data to search + * @param {object[]} found - array to accumulate found alignments into if not duplicated + */ +export function searchAlignmentsAndAppend(searchStr, flags, searchData, found) { + const found_ = searchAlignmentsSub(searchStr, flags, searchData.keys, searchData.alignments); + + if (found_.length) { + for (const item of found_) { + pushUnique(found, item); + } + } +} + +/** + * push item if it is not already in array + * @param array + * @param item + */ +function pushUnique(array, item) { + const duplicate = array.includes(item); // ignore duplicates + + if (!duplicate) { + array.push(item); + } +} + +/** + * search references in search data and merge found alignments into found + * @param {string} searchStr - string to match + * @param {string} flags - regex flags + * @param {object} searchData - data to search + * @param {object[]} found - array to accumulate found alignments into if not duplicated + * @param {object[]} alignmentsArray - list of alignment data that has been indexed + */ +export function searchRefsAndAppend(searchStr, flags, searchData, found, alignmentsArray) { + const found_ = searchRefs(searchStr, flags, searchData.keys, searchData.alignments, alignmentsArray); + + if (found_.length) { + for (const item of found_) { + pushUnique(found, item); + } + } +} + +/** + * look in source index for match to source text + * @param {string} sourceText + * @param {object} sourceIndex + * @returns {object[]} + */ +function getSourceIndices(sourceText, sourceIndex) { + let indices; + let sourceAlignments = sourceIndex[sourceText]; + + // try to find matches in alignment data to get lemma and morph + if (sourceAlignments && sourceAlignments.length) { + indices = [sourceAlignments[0]]; + } else { + const sourceWords = sourceText.split(' '); + indices = []; + + for (const sourceWord of sourceWords) { + sourceAlignments = sourceIndex[sourceWord]; + + if (sourceAlignments && sourceAlignments.length) { + indices.push(sourceAlignments[0]); + } else { + indices.push(sourceWord); + } + } + } + return indices; +} + +/** + * find the morph (and lemma) for sourceText + * @param {object} sourceIndex - alignment indices mapped by source text + * @param {string} sourceText + * @param {array} alignments + * @param {array} sourceKeys + * @returns {{sourceLemma: string, morph: string}} + */ +function getMorphData(sourceIndex, sourceText, alignments, sourceKeys) { + let indices = getSourceIndices(sourceText, sourceIndex); + let sourceLemma = []; + let morph = []; + + for (const index of indices) { + if (index >= 0) { + const alignment_ = alignments[index]; + morph.push(alignment_.morph); + sourceLemma.push(alignment_.sourceLemma); + } else { + let matchFound = false; + let wordJoiner = false; + + while (!matchFound) { + let searchStr = index; + + if (wordJoiner) { + const parts = searchStr.split(WORD_JOINER); + searchStr = parts[0]; + let pos = 0; + let longestLength = searchStr.length; + + for (let i = 1; i longestLength) { + pos = i; + longestLength = len; + } + } + + if (pos > 0) { + searchStr = parts[pos]; + } + } + + const { search, flags } = buildSearchRegex(searchStr, true, false, wordJoiner); + const found = searchAlignmentsSub(search, flags, sourceKeys, sourceIndex); + + if (found && found.length) { + const index_ = found[0]; + const alignment_ = alignments[index_]; + const sourceWords = alignment_.sourceText.split(' '); + + for (let i = 0; i < sourceWords.length; i++) { + const sourceWord = sourceWords[i]; + + if (sourceWord === index) { + matchFound = true; + const morph_ = alignment_.morph.split(' ')[i]; + const lemma_ = alignment_.sourceLemma.split(' ')[i]; + morph.push(morph_); + sourceLemma.push(lemma_); + break; + } + } + } + + if (wordJoiner) { + break; + } else { + wordJoiner = true; + } + } + + if (!matchFound) { + morph.push(MISSING_DATA_SYMBOL); + sourceLemma.push(MISSING_DATA_SYMBOL); + } + } + } + + sourceLemma = sourceLemma.join(' '); + morph = morph.join(' '); + return { sourceLemma, morph }; +} + +/** + * search in field + * @param {string} field + * @param {object} tWordsIndex - contains index for tWords search + * @param {string} search - regex for search + * @param {string} flags - regex flags + * @param {array} found - add entries found to this array + */ +function searchAlignmentsForField(field, tWordsIndex, search, flags, found) { + const keys = Object.keys(tWordsIndex[field]); + const alignments = tWordsIndex[field]; + const searchData = { keys, alignments }; + searchAlignmentsAndAppend(search, flags, searchData, found); +} + +/** + * make sure that words in the current match follow the words in the previous match + * @param {string} fTPos - for current match - all positions of target text separated by spaces + * @param {string} fmtPos - for previous match - all positions of target text separated by spaces + * @returns {{positions: *, firstMPos: *, adjacentAlignments: boolean}} + */ +function arePositionsAdjacent(fTPos, fmtPos) { + const positions = fmtPos && fmtPos.split(' '); + const lastPos = positions && positions[positions.length - 1]; + const mPositions = fTPos && fTPos.split(' '); + const firstMPos = mPositions?.[0]; + + // noinspection EqualityComparisonWithCoercionJS + const adjacentAlignments = parseInt(lastPos) + 1 == firstMPos; + return { + positions, + firstMPos, + adjacentAlignments, + }; +} + +/** + * when comparing word found in previous alignment with current alignment, make sure the previous + * alignment ends with the previous word searched for and the current alignment begins with the + * current word searched for + * @param {object} found_t - alignment we are testing for match with merged word + * @param {string} lastSearch - (xre string) + * @param {string} flags + * @param {string} currentSearch (xre string) + * @param {object} merged - previously found alignment + * @returns {boolean} + */ +function areWordsAdjacent(found_t, lastSearch, flags, currentSearch, merged) { + let foundMatch = false; + let mTargetText = merged.targetText && merged.targetText.split(' ; '); + const lastText = mTargetText && mTargetText[mTargetText.length - 1]; + mTargetText = lastText && lastText.split(' '); + mTargetText = mTargetText && mTargetText[mTargetText.length - 1]; + + if (mTargetText && isMatch(mTargetText, lastSearch, flags)) { + let targetText = found_t.targetText && found_t.targetText.split(' ; '); + const firstText = targetText && targetText[0]; + targetText = firstText && firstText.split(' ')?.[0]; + foundMatch = (targetText && isMatch(targetText, currentSearch, flags)); + } + return foundMatch; +} + +/** + * make sure all the words being search for are in consecutive order + * @param {string[]} positions - of the words in the verse + * @param {string} firstMPos - beginning position of merged word + * @param {object} found_t - alignment we are testing for match with merged word + * @param {string[]} allSearches - all the searches to match in order (xre string) + * @param {string} flags - xre flags for search + * @returns {boolean} + */ +function areWordsAdjacentInTarget(positions, firstMPos, found_t, allSearches, flags) { + let foundMatch = false; + const firstPos = positions[0]; + + // noinspection EqualityComparisonWithCoercionJS + if (firstMPos === firstPos) { // if both words in same alignment, check if adjacent + let targetWords = found_t.targetText && found_t.targetText.split(' ; '); + const lastTargetWords = targetWords && targetWords[targetWords.length - 1]; + targetWords = lastTargetWords && lastTargetWords.split(' '); + const searchesLength = allSearches.length; + const l = targetWords?.length; + + for (let i = l - 1; i >= 0; i--) { + foundMatch = false; + + for (let k = 0; k < searchesLength; k++) { + const currentSearch = allSearches[searchesLength - 1 - k]; // starting at end + const testIndex = i - k; + + if (testIndex < 0) { // we hit the end + foundMatch = k >= 2; // as long as we match at least two words starting at beginning + break; + } + + const targetWord = targetWords[testIndex]; + + if (!isMatch(targetWord, currentSearch, flags)) { + foundMatch = false; + break; + } + foundMatch = true; + } + + if (foundMatch) { + break; + } + } + } + return foundMatch; +} + +/** + * combine multiple searches + * @param {object[]} found + * @param {object[]} foundMerged + * @param {boolean} inOrder + * @param {string[]} previousSearches + * @param {string} currentSearch + * @param {string} flags - regex flag + * @returns {object[]} + */ +function mergeAlignmentMatches(found, foundMerged, inOrder, previousSearches, currentSearch, flags ) { + function mergeKeys(mergedAlignment, merged, found_t, keys) { + for (const key of keys) { + mergedAlignment[key] = `${merged[key]} ; ${found_t[key]}`; + } + } + + const lastSearch = previousSearches && previousSearches.length && previousSearches[previousSearches.length - 1]; + + if (!lastSearch) { + return found; + } + + const matches = []; + const allSearches = previousSearches.concat(currentSearch); + + for (const found_t of found) { + for (const merged of foundMerged) { + for (let i = 0, l = found_t.refs.length; i < l; i++) { + const ref = found_t.refs[i]; + + for (let j = 0, l2 = merged.refs.length; j < l2; j++) { + const mRef = merged.refs[j]; + + if (ref === mRef) { + if (inOrder) { + let foundMatch = false; // not certain yet if they are adjacent + const fTPos = found_t.targetsPos[i]; + const fmtPos = merged.targetsPos[j]; + const { + positions, + firstMPos, + adjacentAlignments, + } = arePositionsAdjacent(fTPos, fmtPos); + + if (adjacentAlignments) { + foundMatch = areWordsAdjacent(found_t, lastSearch, flags, currentSearch, merged); + } else { // if not in order then may not be right match, try checking if both words are in same alignmnet string + foundMatch = areWordsAdjacentInTarget(positions, firstMPos, found_t, allSearches, flags); + } + + if (!foundMatch) { // skip if not a match + continue; + } + } + + const targetsPosForMatch = found_t.targetsPos[i]; + const mergedAlignment = { + ...merged, + refs: [ref], + targetsPos: [targetsPosForMatch], + }; + const mergeKeys_ = ['morph', 'sourceLemma', 'sourceText', 'strong', 'targetText']; + mergeKeys(mergedAlignment, merged, found_t, mergeKeys_); + matches.push(mergedAlignment); + } + } + } + } + } + return matches; +} + +/** + * search one or more fields for searchStr and merge the match alignments together + * @param {object} _alignmentData - object that contains raw alignments and indices for search + * @param {object} tWordsIndex - contains index for tWords search + * @param {string} searchStr - string to match + * @param {object} config - search configuration including search types and fields to search + * @param {object} alignmentData2 - secondary object for searching that contains raw alignments and indices + * @param {object} tWordsIndex2 - contains secondary index for tWords search + * @returns {object[]} - array of found alignments + */ +export function multiSearchAlignments(_alignmentData, tWordsIndex, searchStr, config, alignmentData2, tWordsIndex2) { + const searchStrParts = searchStr.split(' '); + const alignmentDataArray = [ _alignmentData ]; + const tWordsIndexArray = [ tWordsIndex ]; + const foundMatches = []; + + if (alignmentData2) { + alignmentDataArray.push(alignmentData2); + } + + if (tWordsIndex2) { + tWordsIndexArray.push(tWordsIndex2); + } + + for (let j = 0; j < 2; j++) { + const alignmentData = alignmentDataArray[j]; + const tWordsIndex = tWordsIndexArray[j]; + let foundMerged = []; + let previousSearches = []; + + for (let i = 0, l = searchStrParts.length; i < l; i++) { + const searchStr = searchStrParts[i]; + + if (!searchStr) { + continue; + } + + const { search, flags } = buildSearchRegex(searchStr, config.fullWord, config.caseInsensitive); + let found = []; + + if (config.searchTwords && tWordsIndex) { + if (config.searchSource) { + const field = 'quoteIndex'; + searchAlignmentsForField(field, tWordsIndex, search, flags, found); + } + + if (config.searchTarget) { + const field = 'groupIndex'; + searchAlignmentsForField(field, tWordsIndex, search, flags, found); + } + + if (config.searchStrong) { + const field = 'strongsIndex'; + searchAlignmentsForField(field, tWordsIndex, search, flags, found); + } + + if (config.searchLemma) { + const field = 'lemmaIndex'; + searchAlignmentsForField(field, tWordsIndex, search, flags, found); + } + + const source = alignmentData.source.alignments; + const alignments = alignmentData.alignments; + const sourceKeys = Object.keys(source); + + found = found?.map(index => { + const check = tWordsIndex.checks[index]; + const contextId = check?.contextId; + const targetText = contextId?.groupId || ''; + const sourceText = check?.quoteString || ''; + const strong = check.strong; + const { sourceLemma, morph } = getMorphData(source, sourceText, alignments, sourceKeys); + + const newCheck = { + ...check, + targetText, + sourceText, + strong, + sourceLemma, + morph, + }; + + return newCheck; + }); + } else if (alignmentData) { + if (config.searchTarget) { + searchAlignmentsAndAppend(search, flags, alignmentData.target, found); + } + + if (config.searchStrong) { + searchAlignmentsAndAppend(search, flags, alignmentData.strong, found); + } + + if (config.searchLemma) { + searchAlignmentsAndAppend(search, flags, alignmentData.lemma, found); + } + + if (config.searchSource) { + searchAlignmentsAndAppend(search, flags, alignmentData.source, found); + } + + if (config.searchRefs) { + searchRefsAndAppend(search, flags, alignmentData.source, found, alignmentData.alignments); + } + + found = found?.map(index => alignmentData.alignments[index]); + } + + if (i) { // if not first pass + const matches = mergeAlignmentMatches(found, foundMerged, config.inOrder, previousSearches, search, flags); + foundMerged = matches; + } else { + foundMerged = found; + } + + previousSearches.push(search); + } + + Array.prototype.push.apply(foundMatches, foundMerged); + } + + return foundMatches; +} + +/** + * generate a key to identify bible + * @param {object} bible + * @param {string} type + * @returns {string} + */ +export function getKeyForBible(bible, type = null) { + const bibleId = bible.resourceId || bible.bibleId; + const key = `${bible.languageId}_${bibleId}_${(encodeParam(bible.owner))}_${bible.origLang}_${type}_${encodeParam(bible.version)}`; + return key; +} + +/** + * filter downloaded aligned bibles and remove those that did not actually have alignments in them (by checking alignment count in index) + * @param {object[]} downloadedAlignedBibles - aligned bibles found in user resources + * @param {object[]} indexedResources - indexed aligned bibles found in alignmentData folder + * @returns {object[]} + */ +export function filterAvailableAlignedBibles(downloadedAlignedBibles, indexedResources) { + const filtered = []; + + for (const downloadedBible of downloadedAlignedBibles) { + for (let testament = 0; testament <= 1; testament++) { + let origLang = testament ? NT_ORIG_LANG : OT_ORIG_LANG; + + if ((downloadedBible.languageId === NT_ORIG_LANG) || (downloadedBible.languageId === OT_ORIG_LANG)) { + if (origLang !== downloadedBible.languageId) { + continue; // skip over incompatible testaments + } + } + + const found = indexedResources.find(item => ( + item.languageId === downloadedBible.languageId && + item.resourceId === downloadedBible.bibleId && + item.owner === downloadedBible.owner && + item.origLang === origLang && + item.version === downloadedBible.version + )); + + if (found) { + if (found.alignmentCount) { + filtered.push(found); + } + } else { + const newResource = { + ...downloadedBible, + origLang, + resourceId: downloadedBible.bibleId, + }; + + filtered.push(newResource); + } + } + } + return filtered; +} + +/** + * parse the resource key into resource object + * @param {string} name + * @returns {{owner: string, alignmentCount: string, resourceId: string, languageId: string, origLang: string, type: string, version: string}} + */ +export function parseResourceKey(name) { + const parts = (name || '').split('_'); + let [ + languageId, + resourceId, + owner, + origLang, + type, + version, + alignmentCount, + ] = parts; + owner = decodeURIComponent(owner); + version = decodeURIComponent(version); + + return { + languageId, + resourceId, + owner, + origLang, + type, + version, + alignmentCount, + }; +} + +/** + * parse the master file name for resource details + * @param {string} fileName + * @returns {null|any} + */ +export function parseMasterFileName(fileName) { + const [owner, languageId, resourceId] = fileName?.split('_') || []; + return { + owner, + languageId, + resourceId, + }; +} + +/** + * get path to master resource in downloads folder + * @param {object} resource + * @returns {string} + */ +function getMasterResourcePath(resource) { + const resourcePath = path.join(MASTER_DOWNLOADS, `${resource.owner}_${resource.languageId}_${resource.resourceId}`); + return resourcePath; +} + +/** + * check if master resource has been downloaded + * @param {object} resource + * @returns {null|any} + */ +export function isMasterResourceDownloaded(resource) { + const resourcePath = getMasterResourcePath(resource); + const exists = fs.existsSync(resourcePath); + return exists; +} + +/** + * return list of indexed aligned bibles found in alignmentData folder + * @param {string} alignmentDataDir - folder to search + * @returns {object[]} + */ +export function getAlignmentIndices(alignmentDataDir) { + const resources = []; + const resourcesIndexed = readDirectory(alignmentDataDir, false, true, '.json'); + + for (const fileName of resourcesIndexed) { + // const fileFolder = path.join(resourcesIndexed, fileName); + // ~/translationCore/alignmentData/en_ult_unfoldingWord_hbo_testament_v0_275433.json + const name = path.parse(fileName).name; + let { + languageId, + resourceId, + owner, + origLang, + type, + version, + alignmentCount, + } = parseResourceKey(name); + + if (type !== ALIGNMENTS_KEY) { + continue; + } + + alignmentCount = parseInt(alignmentCount, 10); + resources.push({ + languageId, + resourceId, + owner, + origLang, + version, + alignmentCount, + }); + } + return resources; +} + +/** + * test if dirPath is actually a folder + * @param {string} dirPath + * @returns {boolean|*} true if folder + */ +function isDirectory(dirPath) { + try { + if (fs.existsSync(dirPath)) { + return fs.statSync(dirPath).isDirectory(); + } + // eslint-disable-next-line no-empty + } catch (e) { } + return false; +} + +/** + * read the directory and filter by folders or file extensions + * @param {string} dirPath - path to folder to read + * @param {boolean} foldersOnly - if true then only return folders + * @param {boolean} sort - if true then sort the results + * @param {string} extension - optional file extension to match + * @returns {string[]|*} + */ +export function readDirectory(dirPath, foldersOnly = true, sort = true, extension = null) { + if (isDirectory(dirPath)) { + let content = fs.readdirSync(dirPath).filter(item => { + if (item === '.DS_Store') { + return false; + } + + if (foldersOnly) { + return isDirectory(path.join(dirPath, item)); + } + + if (extension) { + const ext = path.parse(item).ext; + return ext === extension; + } + return true; + }); + + if (sort) { + content = content.sort(); + } + return content; + } + return []; +} + +export async function downloadBible(resource, alignmentsFolder) { + const sourceContentUpdater = new SourceContentUpdater(); + + if (!resource.version) { + const owner = resource.owner; + const retries = 5; + const stage = resource.stage !== 'prod' ? 'preprod' : undefined; + const resourceName = `${resource.languageId}_${resource.resourceId}`; + const latest = await apiHelpers.getLatestRelease(owner, resourceName, retries, stage); + const release = latest && latest.release; + let version = release && release.tag_name; + + if (version) { + resource.version = version; + } + } + + const destinationPath = path.join(MASTER_DOWNLOADS, `${resource.owner}_${resource.languageId}_${resource.resourceId}`); + let error = false; + + try { + console.log('downloadBibles() - downloading resource', resource); + const resource_ = await sourceContentUpdater.downloadAndProcessResource(resource, destinationPath); + console.log('downloadBibles() - download done', resource_); + } catch (e) { + console.warn('downloadBibles() - download failed', e); + error = e; + } + + // /Users/blm/translationCore/alignmentData/unfoldingWord_en_ult/en/bibles/ult/v40_unfoldingWord + const parentPath = path.join(destinationPath, `${resource.languageId}/bibles/${resource.resourceId}`); + const files = readDirectory(parentPath); + const bibleName = files[0]; + const biblePath = path.join(parentPath, bibleName || 'unknown'); + const destinationBiblePath = path.join(destinationPath, 'bible'); + + if (!error) { + if (fs.existsSync(destinationBiblePath)) { + fs.removeSync(destinationBiblePath); + } + + if (fs.existsSync(biblePath)) { + fs.moveSync(biblePath, destinationBiblePath); + } else { + error = `Missing ${biblePath}`; + } + } + return error; +} + +/** + * URI encode param and replace _ or . with URI codes to prevent problems parsing as key or filename + * @param {string} param + * @returns {string} + */ +export function encodeParam(param) { + let encoded = encodeURIComponent(param); + encoded = encoded.replaceAll('_', '%5F'); + encoded = encoded.replaceAll('.', '%2E'); + return encoded; +} + +async function doCallback(callback, percent) { + if (callback) { + await callback(Math.round(percent)); + } +} + +/** + * read and parse the book USFM + * @param {string} latestVersionPath + * @param {string} bookId + * @return {{readBooks: boolean, chapters: {}}} + */ +function readBibleBook(latestVersionPath, bookId) { + let readBooks = false; + const bookPath = path.join(latestVersionPath, bookId); + const chapters = {}; + + if (fs.existsSync(bookPath)) { + const chapterFiles = fs.readdirSync(bookPath) + .filter(file => path.extname(file) === '.json'); + + for (const chapterFile of chapterFiles) { + const chapterPath = path.join(bookPath, chapterFile); + const chapterJson = fs.readJsonSync(chapterPath); + const c = path.parse(chapterPath).name; + chapters[c] = chapterJson; + readBooks = true; + } + } + return { readBooks, chapters }; +} + +/** + * open Bible json data and extract alignment data for specific testament + * @param {string} resourceFolder + * @param {object} resource + * @param {function} callback - async callback function(percentProress:number) + * @returns {{strongAlignments: {alignments: {}}, alignments: object[], lemmaAlignments: {alignments: {}}, targetAlignments: {alignments: {}}, sourceAlignments: {alignments: {}}}} + */ +export async function getAlignmentsFromResource(resourceFolder, resource, callback = null) { + const bibleIndex = {}; + + try { + let bibleVersionsPath, latestVersionPath; + // /Users/blm/translationCore/resources/en/bibles/ult/v40_Door43-Catalog + const alignmentsFolder = path.join(resourceFolder, '../alignmentData'); + + if (resource.version === 'master') { + bibleVersionsPath = MASTER_DOWNLOADS; + latestVersionPath = path.join(getMasterResourcePath(resource), 'bible'); + } else { + bibleVersionsPath = path.join(resourceFolder, `${resource.languageId}/bibles/${resource.resourceId}`); + latestVersionPath = resourcesHelpers.getLatestVersionInPath(bibleVersionsPath, resource.owner); + const latestVersion = resourcesHelpers.splitVersionAndOwner(path.parse(latestVersionPath).base || '').version; + resource.version = latestVersion; + } + + if (!latestVersionPath) { + console.warn(`getAlignmentsFromResource() - no bibles found for ${resource.owner} in ${bibleVersionsPath}`); + } else { + let alignments = []; + console.log(`getAlignmentsFromResource() - get alignments for ${resource.origLang}`); + const books = resource.origLang === NT_ORIG_LANG ? Object.keys(BIBLE_BOOKS.newTestament) : Object.keys(BIBLE_BOOKS.oldTestament); + + const total = books.length; + let count = -1; + + for (const bookId of books) { + const percent = ++count * 25 / total; + // eslint-disable-next-line no-await-in-loop + await doCallback(callback, percent); + const { readBooks, chapters } = readBibleBook(latestVersionPath, bookId); + const parsedUsfm = { + chapters, + headers: [], + }; + + if (readBooks) { + const manifest = {}; + // eslint-disable-next-line no-await-in-loop + const bookAlignments = getALignmentsFromJson(parsedUsfm, manifest, bookId); + Array.prototype.push.apply(alignments, bookAlignments); + } + } + + // merge alignments + const alignments_ = {}; + const uniqueAlignments = []; + let l = alignments.length; + let stepSize = Math.round(l / 5); + + for (let i = 0; i < l; i++) { + if (i % stepSize === 0) { + const percent = 25 + 25 * i / l; + // eslint-disable-next-line no-await-in-loop + await doCallback(callback, percent); + } + + const alignment = alignments[i]; + const { + sourceText, + targetText, + ref, + reference: r_, + } = alignment; + + if (!alignments_[sourceText]) { + alignments_[sourceText] = {}; + } + + const sourceAlignment = alignments_[sourceText]; + let index = sourceAlignment[targetText]; + + if (!index) { + alignment.refs = [ref]; + alignment.targetsPos = [alignment.targetsPos]; + delete alignment.ref; + index = uniqueAlignments.length; + uniqueAlignments.push(alignment); + sourceAlignment[targetText] = [index]; + } else { + const matchedAlignment = uniqueAlignments[index]; + matchedAlignment.refs.push(ref); + matchedAlignment.targetsPos.push(alignment.targetsPos); + } + + let bookIndex = bibleIndex[r_.bookId]; + + if (!bookIndex) { + bookIndex = {}; + bibleIndex[r_.bookId] = bookIndex; + } + + let chapterIndex = bookIndex[r_.chapter]; + + if (!chapterIndex) { + chapterIndex = {}; + bookIndex[r_.chapter] = chapterIndex; + } + + let verseIndex = chapterIndex[r_.verse]; + + if (!verseIndex) { + verseIndex = []; + chapterIndex[r_.verse] = verseIndex; + } + + verseIndex.push(index); + } + + alignments = uniqueAlignments; + + console.log(`getAlignmentsFromResource() for ${resource.origLang}, ${alignments.length} alignments, indexing`); + const outputFile = path.join(alignmentsFolder, `${resource.languageId}_${resource.resourceId}_${(encodeParam(resource.owner))}_${resource.origLang}_${ALIGNMENTS_KEY}_${encodeParam(resource.version)}_${alignments.length}.json`); + const lemmaAlignments = { alignments: {} }; + const targetAlignments = { alignments: {} }; + const sourceAlignments = { alignments: {} }; + const strongAlignments = { alignments: {} }; + l = alignments.length; + stepSize = Math.round(l / 5); + + for (let i = 0; i < l; i++) { + if (i % stepSize === 0) { + const percent = 50 + 25 * i / l; + // eslint-disable-next-line no-await-in-loop + await doCallback(callback, percent); + } + + const alignment = alignments[i]; + const { + sourceText, + sourceLemma, + strong, + targetText, + } = alignment; + appendToAlignmentIndex(sourceAlignments.alignments, sourceText, i); + appendToAlignmentIndex(strongAlignments.alignments, strong, i); + appendToAlignmentIndex(lemmaAlignments.alignments, sourceLemma, i); + appendToAlignmentIndex(targetAlignments.alignments, targetText, i); + } + + console.log(`getAlignmentsFromResource() for ${resource.origLang}, getting keys`); + await doCallback(callback, 80); + strongAlignments.keys = getSortedKeys(strongAlignments.alignments, 'en'); + await doCallback(callback, 82); + lemmaAlignments.keys = getSortedKeys(lemmaAlignments.alignments, resource.origLang); + await doCallback(callback, 84); + sourceAlignments.keys = getSortedKeys(sourceAlignments.alignments, resource.origLang); + await doCallback(callback, 90); + targetAlignments.keys = getSortedKeys(targetAlignments.alignments, resource.languageId); + await doCallback(callback, 95); + const alignmentData = { + alignments, + lemmaAlignments, + targetAlignments, + sourceAlignments, + strongAlignments, + bibleIndex, + }; + fs.outputJsonSync(outputFile, alignmentData); + return alignmentData; + } + } catch (e) { + console.warn('getAlignmentsFromResource() - parsing alignments failed', e); + } +} + +/** + * open Bible json data and extract alignment data for both testaments + * @param {string} resourceFolder + * @param {object} resource_ + */ +export function getAlignmentsFromDownloadedBible(resourceFolder, resource_) { + for (let testament = 0; testament <= 1; testament++) { + const origLang = testament ? NT_ORIG_LANG : OT_ORIG_LANG; + const resource = { + ...resource_, + origLang, + }; + getAlignmentsFromResource(resourceFolder, resource); + } + + console.log('done'); +} + +/** + * append an alignment to alignments + * @param alignments + * @param text + * @param alignment + */ +function appendToAlignmentIndex(alignments, text, alignment) { + if (!alignments[text]) { + alignments[text] = []; + } + alignments[text].push(alignment); +} + +/** + * add position in verse to the word objects + * @param {array} foundWords - array to fill with found word + * @param {object[]} verseObjects + * @param {number} pos + * @returns {*} + */ +function addPosition(foundWords, verseObjects, pos) { + for (const vo of verseObjects) { + if (vo.type === 'word') { + vo.pos = pos++; + foundWords.push(vo); + } + + if (vo.children) { + pos = addPosition(foundWords, vo.children, pos); + } + } + return pos; +} + +function addUnalignedWords(words, bookAlignments, verseRef, reference) { + for (const word of words) { + bookAlignments.push({ + sourceText: '', + sourceLemma: '', + strong: '', + morph: '', + targetText: word.word || word.text, + ref: verseRef, + reference, + targetsPos: `${word.pos}`, + }); + } +} + +/** + * generate the target language bible from parsed USFM and manifest data + * @param {Object} parsedUsfm - The object containing usfm parsed by chapters + * @param {Object} manifest + * @param {String} selectedProjectFilename + * @return {Promise} + */ +const getALignmentsFromJson = (parsedUsfm, manifest, selectedProjectFilename) => { + try { + const chaptersObject = parsedUsfm.chapters; + const bookId = manifest?.project?.id || selectedProjectFilename; + const bookAlignments = []; + const chapters = Object.keys(chaptersObject); + + for (const chapter of chapters) { + const bibleChapter = {}; + const verses = Object.keys(chaptersObject[chapter]); + const chapterRef = `${bookId} ${chapter}:`; + + for (const verse of verses) { + const verseRef = `${chapterRef}${verse}`; + const verseParts = chaptersObject[chapter][verse]; + const foundWords = []; + addPosition(foundWords, verseParts.verseObjects, 0); + let verseText = getUsfmForVerseContent(verseParts); + bibleChapter[verse] = trimNewLine(verseText); + const object = wordaligner.unmerge(verseParts); + // eslint-disable-next-line object-curly-newline + const reference = { bookId, chapter, verse }; + + for (const alignment of object.alignment) { + const strongs = []; + const lemmas = []; + const targets = []; + const targetsPos = []; + const sources = []; + const morphs = []; + + for (const originalWord of alignment.topWords) { + let { + strong, + lemma, + word, + morph, + } = originalWord; + strongs.push(strong); + + if (!lemma) { + if (!strong || !word) { // TRICKY - if no lemma, but we have a strong's, this is OK for Hebrew + console.warn(`Invalid original word`, { originalWord, reference }); + } + } + + lemmas.push(lemma); + sources.push(word); + morphs.push(morph); + } + + for (const targetWord of alignment.bottomWords) { + targets.push(targetWord.word); + targetsPos.push(`${targetWord.pos}`); + } + bookAlignments.push({ + sourceText: normalizer(sources.join(' ')), + sourceLemma: normalizer(lemmas.join(' ')), + strong: strongs.join(' '), + morph: morphs.join(' '), + targetText: normalizer(targets.join(' ')), + targetsPos: targetsPos.join(' '), + ref: verseRef, + reference, + }); + } + + if (object.wordBank?.length) { + const words = object.wordBank; + addUnalignedWords(words, bookAlignments, verseRef, reference); + } + } + } + + console.log(`getALignmentsFromJson() for book ${bookId}, ${bookAlignments.length} alignments`); + return bookAlignments; + } catch (error) { + console.log('getALignmentsFromJson() error:', error); + throw (error); + } +}; + +/** + * deletes the alignment search indexes as well as master branch data + */ +export function deleteCachedAlignmentData() { + if (fs.existsSync(ALIGNMENT_DATA_DIR)) { + fs.removeSync(ALIGNMENT_DATA_DIR); + } +} + +/** + * get list of searchable bibles loaded in resources + * @param {string} translationCoreFolder + * @param {boolean} useMaster + * @returns {object[]} + */ +export function getSearchableAlignments(translationCoreFolder, useMaster) { + try { + console.log('getSearchableAlignments() - getting aligned bibles'); + const resourceDir = path.join(translationCoreFolder, 'resources'); + const downloadedAlignedBibles = getAlignedBibles(resourceDir); + + if (useMaster) { + const alignedMasterBibles = readDirectory(MASTER_DOWNLOADS); + + for (const fileName of alignedMasterBibles) { + const version = 'master'; + const { + owner, + languageId, + resourceId, + } = parseMasterFileName(fileName); + + downloadedAlignedBibles.push({ + languageId, + bibleId: resourceId, + owner, + version, + biblePath: path.join(MASTER_DOWNLOADS, fileName), + }); + } + } + + console.log('getSearchableAlignments() - getting alignment indexes for bibles'); + const indexedResources = getAlignmentIndices(ALIGNMENT_DATA_DIR); + + // filter selections + console.log('getSearchableAlignments() - filtering aligned bibles'); + const filtered = filterAvailableAlignedBibles(downloadedAlignedBibles, indexedResources); + return filtered; + } catch (e) { + console.error('getSearchableAlignments() - could not load available bibles'); + } +} + +/** + * aligned bibles found in user resources + * @param {string} resourceDir - path to user resources + * @param {boolean} alignedBiblesOnly - if true then filter for alignment + * @returns {object[]} + */ +export function getAvailableBibles(resourceDir, alignedBiblesOnly = true) { + const alignedBibles = []; + + try { + const languages = readDirectory(resourceDir, true, true, null); + + for (const languageId of languages) { + const biblesFolder = path.join(resourceDir, languageId, 'bibles'); + const bibles = readDirectory(biblesFolder, true, true, null); + + for (const bibleId of bibles) { + const biblePath = path.join(biblesFolder, bibleId); + const owners = resourcesHelpers.getLatestVersionsAndOwners(biblePath) || {}; + + for (const owner of Object.keys(owners)) { + try { + const biblePath = owners[owner]; + let manifest = null; + const manifestPath = path.join(biblePath, 'manifest.json'); + + if (fs.pathExistsSync(manifestPath)) { + manifest = fs.readJsonSync(manifestPath); + } + + let useBible = false; + const subject = manifest?.subject; + + if (alignedBiblesOnly) { + let isAligned = (subject === 'Aligned Bible'); + + if (!isAligned) { // check for original bibles + if ((languageId === NT_ORIG_LANG) || (languageId === OT_ORIG_LANG)) { + if ((bibleId === NT_ORIG_LANG_BIBLE) || (bibleId === OT_ORIG_LANG_BIBLE)) { + isAligned = true; + } + } + } + useBible = isAligned; + } else { + useBible = !!subject; + } + + const version = resourcesHelpers.splitVersionAndOwner(path.basename(biblePath))?.version; + + if (useBible) { + alignedBibles.push({ + languageId, + bibleId, + owner, + version, + biblePath, + }); + } + } catch (e) { + console.error(`getAlignedBibles() - could not load ${biblePath} for ${owner}`, e); + } + } + } + } + } catch (e) { + console.error(`getAlignedBibles() - error getting bibles`, e); + } + return alignedBibles; +} + +/** + * aligned bibles found in user resources + * @param {string} resourceDir - path to user resources + * @returns {object[]} + */ +export function getAlignedBibles(resourceDir) { + return getAvailableBibles(resourceDir, true); +} + +/** + * check if translationWordsLink path + * @param bible + * @returns {boolean} + */ +export function checkForHelpsForBible(bible) { + const tHelpsPath = path.join(USER_RESOURCES_PATH, bible.languageId, 'translationHelps/translationWordsLinks'); + const latestVersionPath = resourcesHelpers.getLatestVersionInPath(tHelpsPath, bible.owner); + + if (latestVersionPath && fs.existsSync(path.join(latestVersionPath, 'manifest.json'))) { + return true; + } + return false; +} + +/** + * looks up verses for resource key and caches them + * @param {string} bibleKey + * @param {string} ref + * @param {object} bibles + * @param {boolean} rawFormat - if false, convert to usfm + */ +export function getVerseForKey(bibleKey, ref, bibles, rawFormat = false) { + try { + const resource = parseResourceKey(bibleKey); + const { + languageId, + resourceId, + owner, + version, + } = resource; + let biblePath, bibleVersion; + + if (version === 'master') { + bibleVersion = version; + biblePath = path.join(getMasterResourcePath(resource), 'bible'); + } else { + bibleVersion = resourcesHelpers.addOwnerToKey(version, owner); + biblePath = path.join(USER_RESOURCES_PATH, languageId, 'bibles', resourceId, bibleVersion); + } + + const bibleId = `${resourceId}_${bibleVersion}`; + + if (fs.existsSync(biblePath)) { + return getVerse(biblePath, ref, bibles, bibleId, rawFormat); + } + console.warn(`getVerseForKey() - could not fetch verse for ${bibleVersion} - ${ref} in path ${biblePath} because file does not exist`); + } catch (e) { + console.warn(`getVerseForKey() - could not fetch verse for ${bibleKey} - ${ref}`, e); + } + return ''; +} + +/** + * convert verse data in verse chunks array to USFM + * @param {array} verseChunks + */ +function convertVerseChunksToUSFM(verseChunks) { + for (const chunk of verseChunks) { + if (typeof chunk.verseData !== 'string') { + chunk.verseData = getUsfmForVerseContent(chunk.verseData); + } + } +} + +/** + * looks up verses and caches them + * @param {string} biblePath + * @param {string} ref + * @param {object} bibles + * @param {string} bibleKey + * @param {boolean} rawFormat - if false, convert to usfm + */ +export function getVerse(biblePath, ref, bibles, bibleKey, rawFormat = false) { + const [bookId, ref_] = (ref || '').trim().split(' '); + + if (!bibles[bibleKey]) { + bibles[bibleKey] = {}; + } + + const bible = bibles[bibleKey]; + + if ( bookId && ref_ ) { + const [chapter, verse] = ref_.split(':'); + + if (chapter && verse) { + if (bible?.[bookId]?.[chapter]) { + let verses = getVerses(bible?.[bookId], ref_); + + if (!rawFormat) { + convertVerseChunksToUSFM(verses); + } + + return verses; + } + + if (!bible?.[bookId]) { + bible[bookId] = {}; + } + + const chapterPath = path.join(biblePath, bookId, chapter + '.json'); + + if (fs.existsSync(chapterPath)) { + try { + const chapterData = fs.readJsonSync(chapterPath); + + if (chapterData) { + for (const verseRef of Object.keys(chapterData)) { + let verseData = chapterData[verseRef]; + + if (!bible?.[bookId]?.[chapter]) { + bible[bookId][chapter] = {}; + } + + bible[bookId][chapter][verseRef] = verseData; + } + } + + let verses = getVerses(bible?.[bookId], ref_); + + if (!rawFormat) { + convertVerseChunksToUSFM(verses); + } + + return verses; + } catch (e) { + console.log(`getVerse() - could not read ${chapterPath}`, e); + } + } + } + } + return ''; +} + +/** + * try to find closest matches for target text in verse text + * @param targetText + * @param verseText + * @returns {{targetPos: number[], targetParts: string[]}} + */ +export function findBestMatchesForTargetText(targetText, verseText) { + let targetParts = targetText.split(' '); + let targetPos = []; + let targetSearchRegEx = []; + let pos_ = 0; + let aborted = false; + + // find first position of words in verse + for (let i = 0; i < targetParts.length; i++) { + const searchWord = targetParts[i]; + + if (!searchWord) { + break; + } + + const { search, flags } = buildSearchRegex(searchWord, true, false); + const regex = xre(search, flags); + targetSearchRegEx.push(regex); + const results = xre.exec(verseText, regex, pos_); + let newPos = results?.index; + + if (newPos >= 0) { + targetPos.push(newPos); + newPos += searchWord.length; + pos_ = newPos; + } else { + aborted = true; + break; + } + } + + if (!aborted && targetParts.length > 1) { + // nudge matched words closer to following word + for (let i = targetParts.length - 2; i >= 0; i--) { + const searchWord = targetParts[i]; + let bestPos = targetPos[i]; + const endPos = targetPos[i + 1] - searchWord.length; + const regex = targetSearchRegEx[i]; + + // eslint-disable-next-line no-constant-condition + while (true) { + const results = xre.exec(verseText, regex, bestPos + searchWord.length); + let newPos = results?.index; + + if ((newPos >= 0) && (newPos <= endPos)) { + bestPos = newPos; + targetPos[i] = bestPos; + } else { + break; + } + } + } + } + return { targetParts, targetPos }; +} + +/** + * add highlighting to verse + * @param {string} verseText + * @param {string} targetText + * @returns {object|string[]} + */ +export function highlightSelectedTextInVerse(verseText, targetText) { + const verseParts = []; + + if (targetText) { + // first try easy case + const pos = verseText.indexOf(targetText); + + if (pos >= 0) { + verseParts.push(verseText.substring(0, pos)); + verseParts.push( {targetText} ); + verseParts.push(verseText.substring(pos + targetText.length)); + } else { + const { targetParts, targetPos } = findBestMatchesForTargetText(targetText, verseText); + let lastPos = 0; + + // break into parts with spans for target text + for (let i = 0; i < targetPos.length; i++) { + const searchWord = targetParts[i]; + const pos = targetPos[i]; + + if (pos >= 0) { + verseParts.push(verseText.substring(lastPos, pos)); + verseParts.push( {searchWord} ); + lastPos = pos + searchWord.length; + } else { + break; + } + } + + if (lastPos < verseText.length) { + verseParts.push(verseText.substring(lastPos)); + } + } + } else { + verseParts.push(verseText); + } + + const output = []; + + for (let versePart of verseParts) { + if (typeof versePart === 'string') { + let pos = -1; + + while ((pos = versePart.indexOf('\n')) >= 0) { + output.push(versePart.substring(0, pos)); + output.push(
); + const remainder = versePart.substring(pos + 1); + versePart = remainder; + } + output.push(versePart); + } else { + output.push(versePart); + } + } + + const verseContent = output.filter(item => item); + return verseContent; +} + +export function addTwordsInfoToResource(resource, resourcesFolder) { + let tWordsLangID = resource.languageId; + let subFolder = 'translationWordsLinks'; + let origLang = resource.origLang; + let filterBooks = null; + + if (resource.owner === DEFAULT_ORIG_LANG_OWNER) { + if (!origLang) { + const olForBook = resource.origLang || BibleHelpers.getOrigLangforBook(resource.bookId); + origLang = olForBook.languageId; + } + subFolder = 'translationWords'; + tWordsLangID = origLang; + filterBooks = (origLang === OT_ORIG_LANG) ? OT_BOOKS : NT_BOOKS; + } + + const tWordsPath = path.join(resourcesFolder, `${tWordsLangID}/translationHelps/${subFolder}`); + + if (!fs.existsSync(tWordsPath)) { + return null; + } + + const latestTWordsVersion = getMostRecentVersionInFolder(tWordsPath, resource.owner); + const latestTwordsPath = path.join(tWordsPath, latestTWordsVersion); + + const res = { + ...resource, + origLang, + latestTWordsVersion, + tWordsLangID, + filterBooks, + latestTwordsPath, + }; + return res; +} + +/** + * remove Index file for resource + * @param resource + */ +export function removeIndices(resource) { + for (const origLang of [OT_ORIG_LANG, NT_ORIG_LANG]) { + const resource_ = { + ...resource, + origLang, + }; + + const key = getKeyForBible(resource_, ALIGNMENTS_KEY); + const indexFiles = readDirectory(ALIGNMENT_DATA_DIR, false, true, '.json'); + const found = indexFiles.find(fileName_ => fileName_.includes(key)); + + if (found) { + const alignmentPath = path.join(ALIGNMENT_DATA_DIR, found); + + if (fs.existsSync(alignmentPath)) { + console.log('removeIndices() - removing index: ' + alignmentPath); + fs.removeSync(alignmentPath); + } + } + } +} + +export function getTwordsKey(resource) { + const key = `${resource.tWordsLangID}_${resource.origLang}_${resource.latestTWordsVersion}_${TWORDS_KEY}`; + return key; +} + +export function getTwordsIndexFileName(key) { + return path.join(ALIGNMENT_DATA_DIR, `${key}.json`); +} + +export function saveTwordsIndex(key, index) { + const filePath = getTwordsIndexFileName(key); + fs.outputJsonSync(filePath, index); +} + +/** + * get saved search index for tWords + * @param {string} key + * @returns {null|*} + */ +export function getTwordsIndex(key) { + let filePath; + + try { + filePath = getTwordsIndexFileName(key); + + if (fs.existsSync(filePath)) { + return fs.readJsonSync(filePath); + } + } catch (e) { + console.warn(`getTwordsIndex - cannot read ${filePath}`, e); + } + return null; +} + +/** + * find item in object, if not found then add newItem + * @param {object} object + * @param {*} item + * @param {boolean} newItemIsArray - if true then new item is an empty array, otherwise make it an empty object + * @returns {*} + */ +function findItem(object, item, newItemIsArray = false) { + let verseList = object[item]; + + if (!verseList) { + verseList = newItemIsArray ? [] : {}; + object[item] = verseList; + } + return verseList; +} + +/** + * search through verse objects to find alignment for quote and occurrence + * @param verseObjects + * @param {string} quote + * @param {number} occurrence + * @returns {string|null|*} + */ +function findWord(verseObjects, quote, occurrence, count = 1) { + let found = null; + + for (const vo of verseObjects) { + if ((vo.type === 'word') && (normalizer(vo.text) === quote)) { + if (count >= occurrence) { + return { found: vo, count }; + } else { + count++; + } + } + + if (vo.children) { + const { found, count: _count } = findWord(vo.children, quote, occurrence, count); + count = _count; + + if (found) { + return { found, count }; + } + } + } + return { found, count }; +} + +/** + * find quote in original language bible and add strong and lemma info + * @param bible + * @param contextId + * @param reference + * @param biblePath + */ +function addOriginalLanguageInfo(bible, contextId, reference, biblePath) { + const { + bookId, + chapter, + verse, + } = reference; + + let book = bible[bookId]; + + if (!book) { + const { readBooks, chapters } = readBibleBook(biblePath, bookId); + + if (readBooks) { + book = chapters; + bible[bookId] = book; + } + } + + if (book) { + const quote = contextId?.quote; + const quotes = Array.isArray(quote) ? quote : [{ + word: quote, + occurrence: contextId?.occurrence || 1, + }]; + + let verseObjects = book[chapter]?.[verse]?.verseObjects; + + if (verseObjects) { + const lemma = []; + const strong = []; + + for (const quote of quotes) { + const { found } = findWord(verseObjects, normalizer(quote.word), quote.occurrence); + + if (found) { + lemma.push(found.lemma); + strong.push(found.strong); + } + } + + contextId.lemma = lemma; + contextId.strong = strong; + } + } +} + +/** + * index twords for resource + * @param {string} resourcesFolder + * @param {object} resource + * @param {function} callback - async callback function(percentProress:number) + * @returns {object} + */ +export async function indexTwords(resourcesFolder, resource, callback = null) { + // for D43-Catalog: + // ~/translationCore/resources/el-x-koine/translationHelps/translationWords/v0.29_Door43-Catalog/kt/groups/1co + // for other owners: + // ~/translationCore/resources/en/translationHelps/translationWordsLinks/v17_unfoldingWord/kt/groups/1ch + try { + const bible = {}; + let checks = []; + const bibleIndex = {}; + const groupIndex = {}; + const quoteIndex = {}; + const strongsIndex = {}; + const lemmaIndex = {}; + const alignmentIndex = {}; + const res = addTwordsInfoToResource(resource, resourcesFolder); + let filterBooks = res.filterBooks; + const latestTWordsVersion = res.latestTWordsVersion; + const latestTwordsPath = res.latestTwordsPath; + + const usingDoor43 = (res.owner === DEFAULT_OWNER); + let biblePath = null; + + if (!usingDoor43) { + const resourceId = (res.origLang === 'hbo') ? 'uhb' : 'ugnt'; + const bibleVersionsPath = path.join(resourcesFolder, `${res.origLang}/bibles/${resourceId}`); + const origLangOwner = getOriginalLangOwner(resource.owner); + biblePath = resourcesHelpers.getLatestVersionInPath(bibleVersionsPath, origLangOwner); + filterBooks = (res.origLang === OT_ORIG_LANG) ? OT_BOOKS : NT_BOOKS; + } + + if (latestTWordsVersion) { + await doCallback(callback, 0); + + if (fs.existsSync(latestTwordsPath)) { + console.log(`indexTwords - Found ${latestTWordsVersion}`); + const categories = readDirectory(latestTwordsPath); + const categoryStepSize = 100 / (categories.length || 1); + + for (let i = 0; i < categories.length; i++) { + const category = categories[i]; + const progressCategory = i * categoryStepSize; + const booksPath = path.join(latestTwordsPath, category, 'groups'); + let books = readDirectory(booksPath); + + if (filterBooks) { + const filteredBooks = books.filter(bookId => filterBooks.includes(bookId)); + books = filteredBooks; + } + + const bookStepSize = categoryStepSize / (books.length || 1); + + for (let j = 0; j < books.length; j++) { + const bookId = books[j]; + const bookProgress = j * bookStepSize + progressCategory; + // eslint-disable-next-line no-await-in-loop + await doCallback(callback, bookProgress); + const bookPath = path.join(booksPath, bookId); + const groupFiles = readDirectory(bookPath, false, true, '.json'); + + for (const groupFile of groupFiles) { + const groupFilePath = path.join(bookPath, groupFile); + + try { + const data = fs.readJsonSync(groupFilePath); + const groupId = groupFile.split('.json')[0]; + const groupList = findItem(groupIndex, groupId, true); + + for (const item of data) { + const contextId = item?.contextId; + const reference = contextId?.reference; + + if (!usingDoor43) { + addOriginalLanguageInfo(bible, contextId, reference, biblePath); + } + + let quote = contextId?.quote; + + if (Array.isArray(quote)) { + const quote_ = quote.map(item => normalizer(item.word || '')); + quote = quote_.join(' '); + } else { + quote = normalizer(quote || ''); + } + + item.quoteString = quote; + let location = checks.length; + const alignmentKey = `${groupId}_${quote}`; + let previousCheck = alignmentIndex[alignmentKey]; + + if (!previousCheck) { // if this is a new check + item.refs = [reference]; + checks.push(item); + } else { // if this check type already saved, add this reference + location = previousCheck; + const check = checks[previousCheck]; + check.refs.push(reference); + } + + const chapter = reference?.chapter; + + let strongs = contextId?.strong || []; + strongs = Array.isArray(strongs) ? strongs.join(' ') : strongs || ''; + item.strong = strongs; + const strongsList = findItem(strongsIndex, strongs, true); + pushUnique(strongsList, location); + + item.category = category; + + let lemma = normalizeItem(contextId?.lemma || ''); + lemma = Array.isArray(lemma) ? lemma.join(' ') : lemma || ''; + item.lemma = lemma; + const lemmaList = findItem(lemmaIndex, lemma, true); + pushUnique(lemmaList, location); + + const quoteList = findItem(quoteIndex, quote, true); + pushUnique(quoteList, location); + + const bookIndex = findItem(bibleIndex, bookId, false); + const chapterIndex = findItem(bookIndex, chapter, false); + const verse = reference?.verse; + const verseList = findItem(chapterIndex, verse, true); + + pushUnique(verseList, location); + pushUnique(groupList, location); + } + // console.log(data); + } catch (e) { + console.warn(`indexTwords - could not read ${groupFilePath}`, e); + } + } + } + } + + const newChecks = checks.map(item => { + const refs = item.refs.map(r => `${r.bookId} ${r.chapter}:${r.verse}`); + item.refs = refs; + return item; + }); + + checks = newChecks; + + return { + bibleIndex, + groupIndex, + lemmaIndex, + quoteIndex, + strongsIndex, + checks, + resource: res, + }; + } + } + } catch (e) { + console.warn(`indexTwords - error`, e); + } + return null; +} + +/** + * look up the aligned text for the twords found + * @param {array} found + * @param {string} bibleKey + * @param {object} bibles + * @param {string} saveAlignmentsKey - key to save alignements in + */ +export function getTwordALignments(found, bibleKey, bibles, saveAlignmentsKey) { + /** + * get array of words in verseObjects + * @param {object[]} verseObjects + * @returns {object[]} + */ + function findWords(verseObjects) { + let words = []; + + if (verseObjects?.length) { + for (const vo of verseObjects) { + if (vo.type === 'word') { + words.push(vo.text); + } else if (vo.children) { + const words_ = findWords(vo.children); + words = words.concat(words_); + } + } + } + return words; + } + + /** + * search through verse objects to find alignment for quote and occurrence + * @param verseObjects + * @param {string} quote + * @param {number} occurrence + * @returns {string|null|*} + */ + function findMatch(verseObjects, quote, occurrence) { + let words = []; + + for (const vo of verseObjects) { + if ((vo.tag === 'zaln') && (vo.content === quote) && (vo.occurrence === occurrence)) { + const words_ = findWords(vo.children); + words = words.concat(words_); + } else if (vo.children) { + const words_ = findMatch(vo.children, quote, occurrence); + + if (words_.length) { + words = words.concat(words_); + } + } + } + return words; + } + + for (const item of found) { + const contextId = item?.contextId; + const reference = contextId?.reference; + const ref = `${reference?.bookId} ${reference?.chapter}:${reference?.verse}`; + const verses = getVerseForKey(bibleKey, ref, bibles, true); + + if (verses?.length) { + const verseObjects = verses[0]?.verseData?.verseObjects || []; + + const alignedText = findMatch(verseObjects, contextId?.quote, contextId?.occurrence); + item[saveAlignmentsKey] = alignedText.join(' '); + } + } +} + +function normalizeItem(item) { + let normalized; + + if (Array.isArray(item)) { + normalized = item.map(item => normalizer(item || '')); + } else { + normalized = normalizer(item || ''); + } + + return normalized; +} + diff --git a/src/js/reducers/popoverReducer.js b/src/js/reducers/popoverReducer.js index adbf60b7c7..e085d631a6 100644 --- a/src/js/reducers/popoverReducer.js +++ b/src/js/reducers/popoverReducer.js @@ -16,6 +16,9 @@ const popoverReducer = (state = initialState, action) => { title: action.title, bodyText: action.bodyText, positionCoord: action.positionCoord, + style: action.style, + titleStyle: action.titleStyle, + bodyStyle: action.bodyStyle, }; case consts.CLOSE_POPOVER: return {