From 753ba1316dec5e310d3e8a69402d601a19f14017 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 24 Oct 2024 17:21:19 +0300 Subject: [PATCH 01/29] feat(ebp-upload): add etrs89GridCode cell in birds forms --- server/forms/_fields/etrs89GridCode.js | 16 ++++++++++++++ server/forms/birds.js | 2 ++ server/forms/cbm.js | 2 ++ server/forms/ciconia.js | 2 ++ ...24134104-add-birds-forms-etrs89GridCode.js | 22 +++++++++++++++++++ 5 files changed, 44 insertions(+) create mode 100644 server/forms/_fields/etrs89GridCode.js create mode 100644 server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js diff --git a/server/forms/_fields/etrs89GridCode.js b/server/forms/_fields/etrs89GridCode.js new file mode 100644 index 000000000..c3420179f --- /dev/null +++ b/server/forms/_fields/etrs89GridCode.js @@ -0,0 +1,16 @@ +module.exports = { + fields: { + etrs89GridCode: { + type: 'text', + length: 8, + required: false, + public: false, + uniqueHash: false, + afterApiUpdate (model) { + if (model.changed('latitude') || model.changed('longitude')) { + model.etrs89GridCode = null + } + } + } + } +} diff --git a/server/forms/birds.js b/server/forms/birds.js index 25befc902..251f1eb01 100644 --- a/server/forms/birds.js +++ b/server/forms/birds.js @@ -2,6 +2,7 @@ const _ = require('lodash') const { assign } = Object const bgatlas2008 = require('./_fields/bgatlas2008') const newSpeciesModeratorReview = require('./_fields/newSpeciesModeratorReview') +const etrs89GridCode = require('./_fields/etrs89GridCode') exports = module.exports = _.cloneDeep(require('./_common')) @@ -13,6 +14,7 @@ exports.hasBgAtlas2008 = true exports.fields = assign(exports.fields, { ...bgatlas2008.fields, ...newSpeciesModeratorReview.fields, + ...etrs89GridCode.fields, species: { type: 'choice', required: true, diff --git a/server/forms/cbm.js b/server/forms/cbm.js index 367c23266..38ee46555 100644 --- a/server/forms/cbm.js +++ b/server/forms/cbm.js @@ -6,6 +6,7 @@ const longitude = require('./_fields/longitude') const observationDateTime = require('./_fields/observationDateTime') const bgatlas2008 = require('./_fields/bgatlas2008') const newSpeciesModeratorReview = require('./_fields/newSpeciesModeratorReview') +const etrs89GridCode = require('./_fields/etrs89GridCode') exports = module.exports = _.cloneDeep(require('./_common')) @@ -18,6 +19,7 @@ exports.hasBgAtlas2008 = true exports.fields = { ...bgatlas2008.fields, ...newSpeciesModeratorReview.fields, + ...etrs89GridCode.fields, confidential: 'boolean', plot: { type: 'choice', diff --git a/server/forms/ciconia.js b/server/forms/ciconia.js index d14c3939d..63a201e68 100644 --- a/server/forms/ciconia.js +++ b/server/forms/ciconia.js @@ -2,6 +2,7 @@ const _ = require('lodash') const { assign } = Object const bgatlas2008 = require('./_fields/bgatlas2008') const newSpeciesModeratorReview = require('./_fields/newSpeciesModeratorReview') +const etrs89GridCode = require('./_fields/etrs89GridCode') exports = module.exports = _.cloneDeep(require('./_common')) @@ -13,6 +14,7 @@ exports.hasBgAtlas2008 = true exports.fields = assign(exports.fields, { ...bgatlas2008.fields, ...newSpeciesModeratorReview.fields, + ...etrs89GridCode.fields, primarySubstrateType: { type: 'choice', uniqueHash: true, diff --git a/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js b/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js new file mode 100644 index 000000000..7885eff5e --- /dev/null +++ b/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js @@ -0,0 +1,22 @@ +'use strict' + +const tables = [ + 'FormBirds', + 'FormCBM', + 'FormCiconia' +] + +module.exports = { + up: async function (queryInterface, DataTypes) { + await Promise.all(tables.map(async (table) => { + await queryInterface.addColumn(table, 'etrs89GridCode', DataTypes.STRING(8)) + await queryInterface.addIndex(table, { fields: ['etrs89GridCode'] }) + })) + }, + + down: async function (queryInterface) { + await Promise.all(tables.map(async (table) => { + await queryInterface.removeColumn(table, 'etrs89GridCode') + })) + } +} From 300ad087fa1f520d70406427b5dc3375305e8a1b Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 24 Oct 2024 17:23:42 +0300 Subject: [PATCH 02/29] feat(ebp-upload): add empty upload to EBP task with some api params definitions --- server/tasks/uploadToEBP.js | 83 +++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 server/tasks/uploadToEBP.js diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js new file mode 100644 index 000000000..d06800cbb --- /dev/null +++ b/server/tasks/uploadToEBP.js @@ -0,0 +1,83 @@ +const { Task } = require('actionhero') + +// eslint-disable-next-line no-unused-vars +const API_TOKEN = process.env.EBP_API_TOKEN +// eslint-disable-next-line no-unused-vars +const apiParams = { + provisionMode: { + bulk: { // all data is replaced + code: 'B' + }, + standard: { // new data is added, existing data is updated + code: 'S' + }, + test: { // The data will not be persisted to the database. It is used for testing purposes. + code: 'T' + } + }, + updateMode: { + modify: { // Only updated records will be provided + code: 'M' + }, + all: { // All records will be provided + code: 'A' + } + }, + dataType: { + casual: { // Casual records + code: 'C' + }, + completeList: { // Complete list records + code: 'L' + }, + fixedList: { // Fixed list of records + code: 'F' + } + }, + locationMode: { + exactLocation: { + code: 'E' + }, + coarseLocation: { // location lowered to 10x10km level ETRS89-LAEA grid + code: 'D' + }, + aggregatedLocation: { // data aggregated at 10x10km level ETRS89-LAEA grid + code: 'A' + } + }, + eventState: { + removed: { // Provided event is removed + code: 0 + }, + modified: { // Provided event is new or has been modified + code: 1 + }, + unchanged: { // Provided event is unchanged + code: 2 + } + }, + recordState: { + removed: { // Provided record is removed + code: 0 + }, + modified: { // Provided record is new or has been modified + code: 1 + } + } +} + +module.exports = class UploadToEBP extends Task { + constructor () { + super() + this.name = 'upload-to-ebp' + this.description = 'Upload records to EBP' + // use cronjob to schedule the task + // npm run enqueue upload-to-ebp + this.frequency = 0 + } + + async run () { + // TODO: implement the task + console.log('Not implemented yet') + } +} From d903d4e7ee3c2212028037200dfc6535f5c941f1 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 25 Oct 2024 10:54:16 +0300 Subject: [PATCH 03/29] feat(ebp-upload): create etrs89_cell model and migration --- .../20241025074805-create-etrs89-cells.js | 59 ++++++++++++++++++ server/models/etrs89_cell.js | 60 +++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 server/migrations/20241025074805-create-etrs89-cells.js create mode 100644 server/models/etrs89_cell.js diff --git a/server/migrations/20241025074805-create-etrs89-cells.js b/server/migrations/20241025074805-create-etrs89-cells.js new file mode 100644 index 000000000..5bc06ac67 --- /dev/null +++ b/server/migrations/20241025074805-create-etrs89-cells.js @@ -0,0 +1,59 @@ +'use strict' + +const tableName = 'etrs89_cells' + +module.exports = { + up: async function (queryInterface, Sequelize) { + await queryInterface.createTable(tableName, { + code: { + type: Sequelize.STRING(8), + primaryKey: true + }, + lat1: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lon1: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lat2: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lon2: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lat3: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lon3: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lat4: { + type: Sequelize.DOUBLE, + allowNull: false + }, + lon4: { + type: Sequelize.DOUBLE, + allowNull: false + } + }) + + await queryInterface.addIndex(tableName, { name: `${tableName}_lat1`, fields: ['lat1'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lat2`, fields: ['lat2'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lat3`, fields: ['lat3'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lat4`, fields: ['lat4'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lon1`, fields: ['lon1'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lon2`, fields: ['lon2'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lon3`, fields: ['lon3'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_lon4`, fields: ['lon4'] }) + }, + + down: async function (queryInterface, Sequelize) { + await queryInterface.dropTable(tableName) + } +} diff --git a/server/models/etrs89_cell.js b/server/models/etrs89_cell.js new file mode 100644 index 000000000..3e57ffa32 --- /dev/null +++ b/server/models/etrs89_cell.js @@ -0,0 +1,60 @@ +module.exports = function (sequelize, DataTypes) { + return sequelize.define('etrs89_cell', { + code: { + type: DataTypes.TEXT, + primaryKey: true + }, + lat1: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lon1: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lat2: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lon2: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lat3: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lon3: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lat4: { + type: DataTypes.DOUBLE, + allowNull: false + }, + lon4: { + type: DataTypes.DOUBLE, + allowNull: false + } + }, { + tableName: 'etrs89_cells', + timestamps: false, + underscored: true, + instanceMethods: { + apiData: function () { + return { + code: this.code, + coordinates: this.coordinates() + } + }, + coordinates: function () { + return [ + { latitude: this.lat1, longitude: this.lon1 }, + { latitude: this.lat2, longitude: this.lon2 }, + { latitude: this.lat3, longitude: this.lon3 }, + { latitude: this.lat4, longitude: this.lon4 } + ] + } + } + }) +} From 69875d688e7010aab8394f735894efb84c66f81a Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 25 Oct 2024 11:57:48 +0300 Subject: [PATCH 04/29] feat(ebp-upload): create task for filling missing etrs89 grid codes --- server/config/app.js | 9 ++++ server/forms/birds.js | 1 + server/forms/cbm.js | 1 + server/forms/ciconia.js | 1 + server/tasks/fillEtrs89Codes.js | 95 +++++++++++++++++++++++++++++++++ 5 files changed, 107 insertions(+) create mode 100644 server/tasks/fillEtrs89Codes.js diff --git a/server/config/app.js b/server/config/app.js index b1d28a8fa..bce2bcef4 100644 --- a/server/config/app.js +++ b/server/config/app.js @@ -27,6 +27,15 @@ exports.default = { translate: { // max records per task maxRecords: parseInt(process.env.AUTO_TRANSLATE_MAX_RECORDS, 10) || 100 + }, + // ETRS89 configuration + etrs89: { + // size of the grid cell in meters + gridSize: 10000, + // max records per task + maxRecords: parseInt(process.env.ETRS89_TASK_MAX_RECORDS, 10) || 100, + // consider records newer than this timestamp + startTimestamp: parseInt(process.env.ETRS89_TASK_START_TIME, 10) || new Date('2024-07-01').getTime() } } } diff --git a/server/forms/birds.js b/server/forms/birds.js index 251f1eb01..2eb8ddbc9 100644 --- a/server/forms/birds.js +++ b/server/forms/birds.js @@ -10,6 +10,7 @@ exports.tableName = 'FormBirds' exports.hasSpecies = true exports.hasThreats = true exports.hasBgAtlas2008 = true +exports.hasEtrs89GridCode = true exports.fields = assign(exports.fields, { ...bgatlas2008.fields, diff --git a/server/forms/cbm.js b/server/forms/cbm.js index 38ee46555..bc613ee26 100644 --- a/server/forms/cbm.js +++ b/server/forms/cbm.js @@ -15,6 +15,7 @@ exports.tableName = 'FormCBM' exports.hasSpecies = true exports.hasThreats = true exports.hasBgAtlas2008 = true +exports.hasEtrs89GridCode = true exports.fields = { ...bgatlas2008.fields, diff --git a/server/forms/ciconia.js b/server/forms/ciconia.js index 63a201e68..be14502e5 100644 --- a/server/forms/ciconia.js +++ b/server/forms/ciconia.js @@ -10,6 +10,7 @@ exports.tableName = 'FormCiconia' exports.hasSpecies = false exports.hasThreats = true exports.hasBgAtlas2008 = true +exports.hasEtrs89GridCode = true exports.fields = assign(exports.fields, { ...bgatlas2008.fields, diff --git a/server/tasks/fillEtrs89Codes.js b/server/tasks/fillEtrs89Codes.js new file mode 100644 index 000000000..b115f6798 --- /dev/null +++ b/server/tasks/fillEtrs89Codes.js @@ -0,0 +1,95 @@ +const { api } = require('actionhero') +const sequelize = require('sequelize') +const { getBoundsOfDistance, isPointInLine, isPointInPolygon } = require('geolib') +const FormsTask = require('../classes/FormsTask') + +const { Op } = sequelize + +async function getEtrs89GridCode ({ latitude, longitude }) { + if (latitude == null || longitude == null) { + return '' + } + + const point = { latitude, longitude } + + // add 75% over for rounding errors + const bounds = getBoundsOfDistance(point, api.config.app.etrs89.gridSize * 1.75) + const where = {} + for (let i = 1; i <= 4; i++) { + where[`lat${i}`] = { [Op.between]: [bounds[0].latitude, bounds[1].latitude] } + where[`lon${i}`] = bounds[0].longitude <= bounds[1].longitude + ? { [Op.between]: [bounds[0].longitude, bounds[1].longitude] } + : { + [Op.or]: [ + { [Op.gte]: bounds[0].longitude }, + { [Op.lte]: bounds[1].longitude } + ] + } + } + + const cells = await api.models.etrs89_cell.findAll({ + where, + // doesn't matter the order, but makes sense to have always the same + // as overlapping cells (e.g. vertices, edges) otherwise will be + // assigned randomly + order: ['code'] + }) + + const cell = cells.reduce((found, cell) => { + if (found) return found + + const coords = cell.coordinates() + + // check for point inside + if (isPointInPolygon(point, coords)) { + return cell + } + // check for point on the vertices + for (let i = 0; i < coords.length; i++) { + if (isPointInLine(point, coords[i], coords[(i + 1) % coords.length])) { + return cell + } + } + + return found + }, null) + + if (!cell) { + return '' + } + + return cell.code +} + +module.exports = class FillEtrs89Codes extends FormsTask { + constructor () { + super() + this.name = 'fill-etrs89-codes' + this.description = 'Populate ETRS89-LAEA code based on coordinates' + // use cronjob to schedule the task + // npm run enqueue fill-etrs89-codes + this.frequency = 0 + this.defaultLimit = api.config.app.etrs89.maxRecords + } + + getForms () { + return super.getForms().filter((form) => form.hasEtrs89GridCode) + } + + filterRecords ({ force }) { + return { + ...(force ? {} : { etrs89GridCode: null }), + observationDateTime: { [Op.gte]: api.config.app.etrs89.startTimestamp } + } + } + + async processRecord (record, form) { + record.etrs89GridCode = await getEtrs89GridCode(record) + + if (!await api.forms.trySave(record, api.forms[form]) && record.etrs89GridCode !== '') { + // mark as empty string so we don't repeat it + record.etrs89GridCode = '' + await api.forms.trySave(record, api.forms[form]) + } + } +} From 99ce780bf01d3b871d560f9f961121176eb8892b Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 25 Oct 2024 12:13:10 +0300 Subject: [PATCH 05/29] feat(ebp-upload): create action for triggering etrs89 task --- server/actions/tasks.js | 16 ++++++++++++++++ server/config/routes.js | 1 + 2 files changed, 17 insertions(+) diff --git a/server/actions/tasks.js b/server/actions/tasks.js index 2383fda45..c572363cc 100644 --- a/server/actions/tasks.js +++ b/server/actions/tasks.js @@ -110,3 +110,19 @@ module.exports.autoTranslateNomenclatures = class EnqueueAutoTranslateNomenclatu return await api.tasks.enqueue('autoTranslateNomenclatures', { form, id, limit, force }) } } + +module.exports.etrs89Codes = class EnqueueFillEtrs89Codes extends BaseAction { + constructor () { + super() + this.name = 'tasks:enqueue:etrs89Codes' + this.description = 'Trigger filling ETRS89-LAEA codes' + } + + availableForms () { + return super.availableForms().filter(k => api.forms[k].hasEtrs89GridCode) + } + + async enqueue ({ form, id, limit, force }) { + return await api.tasks.enqueue('fill-etrs89-codes', { form, id, limit, force }) + } +} diff --git a/server/config/routes.js b/server/config/routes.js index d9cc3cb70..59a0debe8 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -133,6 +133,7 @@ exports.default = { { path: '/zone/:id/owner', action: 'zone:requestOwnership' }, { path: '/zone/:id/owner/response', action: 'zone:respondOwnershipRequest' }, { path: '/tasks/auto-translate-nomenclatures', action: 'tasks:enqueue:autoTranslateNomenclatures' }, + { path: '/tasks/etrs89', action: 'tasks:enqueue:etrs89Codes' }, // forms { path: '/bats', action: 'formBats:create' }, From 9d234c4b348ee753fafef70d424541d622287ff7 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 22 Nov 2024 18:17:07 +0200 Subject: [PATCH 06/29] feat(ebp-upload): fix etrs89_cell model --- .../migrations/20241024134104-add-birds-forms-etrs89GridCode.js | 2 +- server/migrations/20241025074805-create-etrs89-cells.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js b/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js index 7885eff5e..aff0674d8 100644 --- a/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js +++ b/server/migrations/20241024134104-add-birds-forms-etrs89GridCode.js @@ -9,7 +9,7 @@ const tables = [ module.exports = { up: async function (queryInterface, DataTypes) { await Promise.all(tables.map(async (table) => { - await queryInterface.addColumn(table, 'etrs89GridCode', DataTypes.STRING(8)) + await queryInterface.addColumn(table, 'etrs89GridCode', DataTypes.STRING(20)) await queryInterface.addIndex(table, { fields: ['etrs89GridCode'] }) })) }, diff --git a/server/migrations/20241025074805-create-etrs89-cells.js b/server/migrations/20241025074805-create-etrs89-cells.js index 5bc06ac67..240e618da 100644 --- a/server/migrations/20241025074805-create-etrs89-cells.js +++ b/server/migrations/20241025074805-create-etrs89-cells.js @@ -6,7 +6,7 @@ module.exports = { up: async function (queryInterface, Sequelize) { await queryInterface.createTable(tableName, { code: { - type: Sequelize.STRING(8), + type: Sequelize.STRING(20), primaryKey: true }, lat1: { From f7c4a81ec7bdc2f11144f882db6f26f63ad960d8 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Tue, 26 Nov 2024 18:01:59 +0200 Subject: [PATCH 07/29] feat(ebp-upload): add birds_ebp model --- .../20241126151813-create-birds-ebp.js | 34 ++++++++++++++++ server/models/birds_ebp.js | 39 +++++++++++++++++++ 2 files changed, 73 insertions(+) create mode 100644 server/migrations/20241126151813-create-birds-ebp.js create mode 100644 server/models/birds_ebp.js diff --git a/server/migrations/20241126151813-create-birds-ebp.js b/server/migrations/20241126151813-create-birds-ebp.js new file mode 100644 index 000000000..6c1c07015 --- /dev/null +++ b/server/migrations/20241126151813-create-birds-ebp.js @@ -0,0 +1,34 @@ +'use strict' + +const tableName = 'birds_ebp' + +module.exports = { + up: async function (queryInterface, Sequelize) { + await queryInterface.createTable(tableName, { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true + }, + sbNameLa: { + type: Sequelize.TEXT, + allowNull: false + }, + ebpNameLa: { + type: Sequelize.TEXT + }, + ebpId: { + type: Sequelize.INTEGER, + allowNull: false + } + }) + + await queryInterface.addIndex(tableName, { name: `${tableName}_sbNameLa`, fields: ['sbNameLa'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_ebpNameLa`, fields: ['ebpNameLa'] }) + await queryInterface.addIndex(tableName, { name: `${tableName}_ebpId`, fields: ['ebpId'] }) + }, + + down: async function (queryInterface, Sequelize) { + await queryInterface.dropTable(tableName) + } +} diff --git a/server/models/birds_ebp.js b/server/models/birds_ebp.js new file mode 100644 index 000000000..0fa3eb8b8 --- /dev/null +++ b/server/models/birds_ebp.js @@ -0,0 +1,39 @@ +'use strict' +module.exports = function (sequelize, DataTypes) { + return sequelize.define('birds_ebp', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + sbNameLa: { + type: DataTypes.STRING, + allowNull: false + }, + ebpNameLa: { + type: DataTypes.STRING, + allowNull: true + }, + ebpId: { + type: DataTypes.STRING, + allowNull: false + } + }, { + tableName: 'birds_ebp', + instanceMethods: { + apiData: function () { + return { + id: this.id, + sbNameLa: this.sbNameLa, + ebpNameLa: this.ebpNameLa, + ebpId: this.ebpId + } + }, + apiUpdate: function (data) { + this.sbNameLa = data.sbNameLa + this.ebpNameLa = data.ebpNameLa + this.ebpId = data.ebpId + } + } + }) +} From 6e179f9cf99b0aead0f84fd4c507e8943d7f2ea8 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Wed, 27 Nov 2024 11:43:29 +0200 Subject: [PATCH 08/29] fixup! feat(ebp-upload): add birds_ebp model --- ....js => 20241126151813-create-ebp-birds.js} | 11 ++++----- server/models/{birds_ebp.js => ebp_birds.js} | 24 +++++++++---------- 2 files changed, 16 insertions(+), 19 deletions(-) rename server/migrations/{20241126151813-create-birds-ebp.js => 20241126151813-create-ebp-birds.js} (90%) rename server/models/{birds_ebp.js => ebp_birds.js} (68%) diff --git a/server/migrations/20241126151813-create-birds-ebp.js b/server/migrations/20241126151813-create-ebp-birds.js similarity index 90% rename from server/migrations/20241126151813-create-birds-ebp.js rename to server/migrations/20241126151813-create-ebp-birds.js index 6c1c07015..63aab3c5b 100644 --- a/server/migrations/20241126151813-create-birds-ebp.js +++ b/server/migrations/20241126151813-create-ebp-birds.js @@ -1,6 +1,6 @@ 'use strict' -const tableName = 'birds_ebp' +const tableName = 'ebp_birds' module.exports = { up: async function (queryInterface, Sequelize) { @@ -10,16 +10,15 @@ module.exports = { primaryKey: true, autoIncrement: true }, - sbNameLa: { - type: Sequelize.TEXT, + ebpId: { + type: Sequelize.INTEGER, allowNull: false }, ebpNameLa: { type: Sequelize.TEXT }, - ebpId: { - type: Sequelize.INTEGER, - allowNull: false + sbNameLa: { + type: Sequelize.TEXT } }) diff --git a/server/models/birds_ebp.js b/server/models/ebp_birds.js similarity index 68% rename from server/models/birds_ebp.js rename to server/models/ebp_birds.js index 0fa3eb8b8..4403fba00 100644 --- a/server/models/birds_ebp.js +++ b/server/models/ebp_birds.js @@ -1,38 +1,36 @@ 'use strict' module.exports = function (sequelize, DataTypes) { - return sequelize.define('birds_ebp', { + return sequelize.define('ebp_birds', { id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true }, - sbNameLa: { - type: DataTypes.STRING, + ebpId: { + type: DataTypes.INTEGER, allowNull: false }, ebpNameLa: { - type: DataTypes.STRING, - allowNull: true + type: DataTypes.TEXT }, - ebpId: { - type: DataTypes.STRING, - allowNull: false + sbNameLa: { + type: DataTypes.TEXT } }, { - tableName: 'birds_ebp', + tableName: 'ebp_birds', instanceMethods: { apiData: function () { return { id: this.id, - sbNameLa: this.sbNameLa, + ebpId: this.ebpId, ebpNameLa: this.ebpNameLa, - ebpId: this.ebpId + sbNameLa: this.sbNameLa } }, apiUpdate: function (data) { - this.sbNameLa = data.sbNameLa - this.ebpNameLa = data.ebpNameLa this.ebpId = data.ebpId + this.ebpNameLa = data.ebpNameLa + this.sbNameLa = data.sbNameLa } } }) From ee1798609741d70a490f46d989bee5b92db8e4aa Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Wed, 27 Nov 2024 12:07:09 +0200 Subject: [PATCH 09/29] fixup! fixup! feat(ebp-upload): add birds_ebp model --- server/migrations/20241126151813-create-ebp-birds.js | 5 ++--- server/models/ebp_birds.js | 4 ++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/server/migrations/20241126151813-create-ebp-birds.js b/server/migrations/20241126151813-create-ebp-birds.js index 63aab3c5b..d44ba3c53 100644 --- a/server/migrations/20241126151813-create-ebp-birds.js +++ b/server/migrations/20241126151813-create-ebp-birds.js @@ -22,9 +22,8 @@ module.exports = { } }) - await queryInterface.addIndex(tableName, { name: `${tableName}_sbNameLa`, fields: ['sbNameLa'] }) - await queryInterface.addIndex(tableName, { name: `${tableName}_ebpNameLa`, fields: ['ebpNameLa'] }) - await queryInterface.addIndex(tableName, { name: `${tableName}_ebpId`, fields: ['ebpId'] }) + await queryInterface.addIndex(tableName, { unique: true, fields: ['ebpId'] }) + await queryInterface.addIndex(tableName, { unique: true, fields: ['sbNameLa'] }) }, down: async function (queryInterface, Sequelize) { diff --git a/server/models/ebp_birds.js b/server/models/ebp_birds.js index 4403fba00..a5a645646 100644 --- a/server/models/ebp_birds.js +++ b/server/models/ebp_birds.js @@ -18,6 +18,10 @@ module.exports = function (sequelize, DataTypes) { } }, { tableName: 'ebp_birds', + indexes: [ + { unique: true, fields: ['ebpId'] }, + { unique: true, fields: ['sbNameLa'] } + ], instanceMethods: { apiData: function () { return { From 90e55152a9c4e13d557ba6ded3e6af07a10972c9 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Wed, 27 Nov 2024 14:16:37 +0200 Subject: [PATCH 10/29] feat(ebp-upload): add ebp_birds_status model --- .../20241127094732-create-ebp-birds-status.js | 27 ++++++++++++++ server/models/ebp_birds_status.js | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 server/migrations/20241127094732-create-ebp-birds-status.js create mode 100644 server/models/ebp_birds_status.js diff --git a/server/migrations/20241127094732-create-ebp-birds-status.js b/server/migrations/20241127094732-create-ebp-birds-status.js new file mode 100644 index 000000000..4c23bfca7 --- /dev/null +++ b/server/migrations/20241127094732-create-ebp-birds-status.js @@ -0,0 +1,27 @@ +'use strict' + +const tableName = 'ebp_birds_status' + +module.exports = { + up: async function (queryInterface, Sequelize) { + await queryInterface.createTable(tableName, { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true + }, + ebpId: { + type: Sequelize.INTEGER, + allowNull: false + }, + sbNameEn: { + type: Sequelize.TEXT + } + }) + await queryInterface.addIndex(tableName, { unique: true, fields: ['sbNameEn'] }) + }, + + down: async function (queryInterface, Sequelize) { + await queryInterface.dropTable(tableName) + } +} diff --git a/server/models/ebp_birds_status.js b/server/models/ebp_birds_status.js new file mode 100644 index 000000000..f405bdba5 --- /dev/null +++ b/server/models/ebp_birds_status.js @@ -0,0 +1,35 @@ +'use strict' +module.exports = function (sequelize, DataTypes) { + return sequelize.define('ebp_birds_status', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + ebpId: { + type: DataTypes.INTEGER, + allowNull: false + }, + sbNameEn: { + type: DataTypes.TEXT + } + }, { + tableName: 'ebp_birds_status', + indexes: [ + { unique: true, fields: ['sbNameEn'] } + ], + instanceMethods: { + apiData: function () { + return { + id: this.id, + ebpId: this.ebpId, + sbNameEn: this.sbNameEn + } + }, + apiUpdate: function (data) { + this.ebpId = data.ebpId + this.sbNameEn = data.sbNameEn + } + } + }) +} From 8ea94e2de1dc1d2d4fdecd01630a34ae1eee0e73 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 28 Nov 2024 14:44:12 +0200 Subject: [PATCH 11/29] feat(ebp-upload): add ebp api token env to server container --- .env.sample | 1 + compose.yml | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/.env.sample b/.env.sample index ea2c227dd..8aec51422 100644 --- a/.env.sample +++ b/.env.sample @@ -1,2 +1,3 @@ BACKUP_ZIP_PASSWORD= SB_DOMAIN=sb.localhost +EBP_API_TOKEN= diff --git a/compose.yml b/compose.yml index 46637fc87..4e52c8fc5 100644 --- a/compose.yml +++ b/compose.yml @@ -41,6 +41,11 @@ services: build: . entrypoint: ./entrypoint.sh command: start + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" # env_file: # - sb-server.env environment: @@ -51,6 +56,7 @@ services: REDIS_HOST: redis REDIS_PORT: 6379 REDIS_DB: 0 + EBP_API_TOKEN: ${EBP_API_TOKEN} depends_on: redis: condition: service_healthy From d80057fe8185181a237c0c797b9965c1207a1927 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 28 Nov 2024 16:09:40 +0200 Subject: [PATCH 12/29] feat(ebp-upload): wip prepare data for upload --- server/tasks/uploadToEBP.js | 60 +++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js index d06800cbb..c421111b1 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/uploadToEBP.js @@ -1,4 +1,9 @@ -const { Task } = require('actionhero') +const { Task, api } = require('actionhero') +const sequelize = require('sequelize') +const { Op } = sequelize + +// const availableForms = [api.forms.formCBM, api.forms.formBirds, api.forms.formCiconia] +const availableForms = [api.forms.formBirds] // eslint-disable-next-line no-unused-vars const API_TOKEN = process.env.EBP_API_TOKEN @@ -66,6 +71,55 @@ const apiParams = { } } +const allowedOrganizations = () => { + return ['bspb', 'independent'] +} + +const excludedSources = () => { + return ['Project NMNH-BAS & MOEW', 'Research of breeding birds, BSPB-NMNH'] +} + +const getEbpSpecies = async () => { + return api.models.ebp_birds.findAll({ + where: { + sbNameLa: { [Op.not]: null } + } + }) +} + +const loadRecords = async (forms, date) => { + const records = await Promise.allSettled(forms.map(async form => { + return form.model.findAll({ + where: { + etrs89GridCode: { [Op.not]: null }, + observationDateTime: { + [Op.and]: [ + { [Op.gte]: '2024-01-13 00:00:00' }, + { [Op.lte]: '2024-01-13 23:59:59' } + ] + }, + organization: { [Op.in]: allowedOrganizations() }, + sourceEn: { [Op.notIn]: excludedSources() } + } + }) + })) + + for (const record of records) { + if (record.status === 'rejected') { + throw record.reason + } + } + + return records?.map(record => record.value).flat() +} + +const prepareEbpData = async (date = new Date()) => { + const ebpSpecies = await getEbpSpecies() + console.log('+++++ EBP SPECIES: ', ebpSpecies?.length) + const records = await loadRecords(availableForms, date) + return records +} + module.exports = class UploadToEBP extends Task { constructor () { super() @@ -77,7 +131,7 @@ module.exports = class UploadToEBP extends Task { } async run () { - // TODO: implement the task - console.log('Not implemented yet') + const ebpData = await prepareEbpData(new Date()) + console.log('+++++ EBP DATA: ', ebpData?.length) } } From 1d831246f358062af21f7f3a7261a2b5ee0d745d Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 29 Nov 2024 17:34:29 +0200 Subject: [PATCH 13/29] fixup! feat(ebp-upload): wip prepare data for upload --- server/models/ebp_birds.js | 1 + server/models/ebp_birds_status.js | 1 + server/tasks/uploadToEBP.js | 87 +++++++++++++++++++++++++++++-- 3 files changed, 84 insertions(+), 5 deletions(-) diff --git a/server/models/ebp_birds.js b/server/models/ebp_birds.js index a5a645646..f508e3a45 100644 --- a/server/models/ebp_birds.js +++ b/server/models/ebp_birds.js @@ -18,6 +18,7 @@ module.exports = function (sequelize, DataTypes) { } }, { tableName: 'ebp_birds', + timestamps: false, indexes: [ { unique: true, fields: ['ebpId'] }, { unique: true, fields: ['sbNameLa'] } diff --git a/server/models/ebp_birds_status.js b/server/models/ebp_birds_status.js index f405bdba5..c967c1a9e 100644 --- a/server/models/ebp_birds_status.js +++ b/server/models/ebp_birds_status.js @@ -15,6 +15,7 @@ module.exports = function (sequelize, DataTypes) { } }, { tableName: 'ebp_birds_status', + timestamps: false, indexes: [ { unique: true, fields: ['sbNameEn'] } ], diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js index c421111b1..d97251939 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/uploadToEBP.js @@ -1,6 +1,9 @@ const { Task, api } = require('actionhero') const sequelize = require('sequelize') const { Op } = sequelize +const startOfDay = require('date-fns/startOfDay') +const endOfDay = require('date-fns/endOfDay') +const format = require('date-fns/format') // const availableForms = [api.forms.formCBM, api.forms.formBirds, api.forms.formCiconia] const availableForms = [api.forms.formBirds] @@ -79,6 +82,15 @@ const excludedSources = () => { return ['Project NMNH-BAS & MOEW', 'Research of breeding birds, BSPB-NMNH'] } +const getSensitiveSpecies = async () => { + return api.models.species.findAll({ + where: { + sensitive: true, + type: 'birds' + } + } || []) +} + const getEbpSpecies = async () => { return api.models.ebp_birds.findAll({ where: { @@ -87,6 +99,14 @@ const getEbpSpecies = async () => { }) } +const getEbpSpeciesStatus = async () => { + return api.models.ebp_birds_status.findAll({ + where: { + sbNameEn: { [Op.not]: null } + } + }) +} + const loadRecords = async (forms, date) => { const records = await Promise.allSettled(forms.map(async form => { return form.model.findAll({ @@ -94,8 +114,8 @@ const loadRecords = async (forms, date) => { etrs89GridCode: { [Op.not]: null }, observationDateTime: { [Op.and]: [ - { [Op.gte]: '2024-01-13 00:00:00' }, - { [Op.lte]: '2024-01-13 23:59:59' } + { [Op.gte]: startOfDay(date) }, + { [Op.lte]: endOfDay(date) } ] }, organization: { [Op.in]: allowedOrganizations() }, @@ -115,9 +135,33 @@ const loadRecords = async (forms, date) => { const prepareEbpData = async (date = new Date()) => { const ebpSpecies = await getEbpSpecies() + const sensitiveSpecies = await getSensitiveSpecies() + const ebpSpeciesStatus = await getEbpSpeciesStatus() + console.log('+++++ EBP SPECIES: ', ebpSpecies?.length) + console.log('+++++ SENSITIVE SPECIES: ', sensitiveSpecies?.length) + + // load all records for the given date const records = await loadRecords(availableForms, date) - return records + + console.log('+++++ RECORDS: ', records?.length) + // filter records by EBP species + let filtered = records.filter(record => ebpSpecies.find(species => { + return species.sbNameLa === record.species + })) + console.log('+++++ FILTERED BY EBP SPECIES: ', filtered?.length) + + // filter records by sensitive species + filtered = filtered.filter(record => !sensitiveSpecies.find(species => { + return species.labelLa === record.species + })) + console.log('+++++ FILTERED BY SENSITIVE SPECIES: ', filtered?.length) + + // filter records by EBP species status + filtered = filtered.filter(record => record.speciesStatusEn === null || ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)) + console.log('+++++ FILTERED BY EBP SPECIES STATUS: ', filtered?.length) + + return filtered } module.exports = class UploadToEBP extends Task { @@ -131,7 +175,40 @@ module.exports = class UploadToEBP extends Task { } async run () { - const ebpData = await prepareEbpData(new Date()) - console.log('+++++ EBP DATA: ', ebpData?.length) + const recordsDate = new Date('2024-01-13') + const startTimestamp = new Date().getTime() + + const ebpEvent = { + data_type: apiParams.dataType.casual.code, + locationMode: apiParams.locationMode.aggregatedLocation.code, + date: format(recordsDate, 'yyyy-MM-dd'), + state: apiParams.eventState.modified.code + } + + const ebpRecords = [] + + const birdsRecords = await prepareEbpData(recordsDate) + + const ebpEvents = birdsRecords.reduce((acc, record) => { + if (!acc[record.etrs89GridCode]) { + acc[record.etrs89GridCode] = [] + } + acc[record.etrs89GridCode].push(record) + + return acc + }, {}) + + const etrs89GridCodeCounts = Object.entries(ebpEvents).reduce((acc, [key, value]) => { + acc[key] = value.length + return acc + }, {}) + + const operationTime = new Date().getTime() - startTimestamp + + console.log('+++++ EBP DATA: ', birdsRecords?.length) + console.log('+++++ EBP RECORDS: ', ebpRecords?.length) + console.log('+++++ EBP EVENT: ', ebpEvent) + console.log('+++++ ETRS codes counts: ', etrs89GridCodeCounts) + console.log('+++++ OPERATION TIME: ', operationTime) } } From 18474500c6d0ea5b0eeaa7f47ea6163ad357e51e Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Mon, 2 Dec 2024 16:33:12 +0200 Subject: [PATCH 14/29] fixup! feat(ebp-upload): wip prepare data for upload --- server/tasks/uploadToEBP.js | 158 +++++++++++++++++++++++++----------- 1 file changed, 111 insertions(+), 47 deletions(-) diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js index d97251939..d0020e26c 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/uploadToEBP.js @@ -133,35 +133,125 @@ const loadRecords = async (forms, date) => { return records?.map(record => record.value).flat() } +const filterRecords = async (records, ebpSpecies, ebpSpeciesStatus) => { + const sensitiveSpecies = await getSensitiveSpecies() + + // filter records by species, sensitive species and species status + return records + .filter(record => ebpSpecies.find(species => { + return species.sbNameLa === record.species + })) + .filter(record => !sensitiveSpecies.find(species => { + return species.labelLa === record.species + })) + .filter(record => record.speciesStatusEn === null || ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)) +} + +const generateEventId = (etrsCode, date) => { + return format(date, 'yyyyMMdd') + '_' + etrsCode +} + const prepareEbpData = async (date = new Date()) => { const ebpSpecies = await getEbpSpecies() - const sensitiveSpecies = await getSensitiveSpecies() const ebpSpeciesStatus = await getEbpSpeciesStatus() - console.log('+++++ EBP SPECIES: ', ebpSpecies?.length) - console.log('+++++ SENSITIVE SPECIES: ', sensitiveSpecies?.length) - // load all records for the given date const records = await loadRecords(availableForms, date) - console.log('+++++ RECORDS: ', records?.length) - // filter records by EBP species - let filtered = records.filter(record => ebpSpecies.find(species => { - return species.sbNameLa === record.species - })) - console.log('+++++ FILTERED BY EBP SPECIES: ', filtered?.length) - // filter records by sensitive species - filtered = filtered.filter(record => !sensitiveSpecies.find(species => { - return species.labelLa === record.species - })) - console.log('+++++ FILTERED BY SENSITIVE SPECIES: ', filtered?.length) + // additional filtering + const filtered = await filterRecords(records, ebpSpecies, ebpSpeciesStatus) - // filter records by EBP species status - filtered = filtered.filter(record => record.speciesStatusEn === null || ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)) - console.log('+++++ FILTERED BY EBP SPECIES STATUS: ', filtered?.length) + // common event data + const ebpEvent = { + data_type: apiParams.dataType.casual.code, + locationMode: apiParams.locationMode.aggregatedLocation.code, + date: format(date, 'yyyy-MM-dd'), + state: apiParams.eventState.modified.code + } - return filtered + // group records by ETRS89 grid code + const etrsRecords = filtered.reduce((acc, record) => { + if (!acc[record.etrs89GridCode]) { + acc[record.etrs89GridCode] = [] + } + acc[record.etrs89GridCode].push(record) + + return acc + }, {}) + + // prepare EBP events + const ebpEvents = Object.entries(etrsRecords).reduce((acc, [etrsCode, records]) => { + const eventData = { + event: { + event_id: generateEventId(etrsCode, date), + ...ebpEvent + }, + records: [] + } + + const observers = [] + const speciesUsersRecords = [] + + // group records by species + const speciesRecords = records.reduce((acc, record) => { + const key = `${record.species}_${record.userId}` + if (!speciesUsersRecords.includes(key)) { + speciesUsersRecords.push(key) + } + + if (!observers.includes(record.userId)) { + observers.push(record.userId) + } + + if (!acc[record.species]) { + acc[record.species] = { + species: record.species, + species_code: ebpSpecies.find(species => species.sbNameLa === record.species)?.ebpId, + breeding_code: null, + users: [], + records: [] + } + } + + if (!acc[record.species].users.includes(record.userId)) { + acc[record.species].users.push(record.userId) + } + + if (record.speciesStatusEn) { + const breedingCode = ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)?.ebpId + if (!acc[record.species].breeding_code || acc[record.species].breeding_code < breedingCode) { + acc[record.species].breeding_code = breedingCode + } + } + + acc[record.species].records.push(record) + + return acc + }, {}) + + eventData.records = Object.values(speciesRecords).map(speciesRecord => { + return { + event_id: eventData.event.event_id, + record_id: eventData.event.event_id + '_' + speciesRecord.species_code, + species_code: speciesRecord.species_code, + count: speciesRecord.records.reduce((acc, record) => acc + record.count, 0), + records_of_species: speciesRecord.users.length, + breeding_code: speciesRecord.breeding_code, + state: apiParams.recordState.modified.code + } + }) + + eventData.event.records = speciesUsersRecords.length + eventData.event.observers = observers.length + + acc.push(eventData) + return acc + }, []) + + console.log('+++++ EBP EVENTS: ', JSON.stringify(ebpEvents)) + + return ebpEvents } module.exports = class UploadToEBP extends Task { @@ -178,37 +268,11 @@ module.exports = class UploadToEBP extends Task { const recordsDate = new Date('2024-01-13') const startTimestamp = new Date().getTime() - const ebpEvent = { - data_type: apiParams.dataType.casual.code, - locationMode: apiParams.locationMode.aggregatedLocation.code, - date: format(recordsDate, 'yyyy-MM-dd'), - state: apiParams.eventState.modified.code - } - - const ebpRecords = [] - - const birdsRecords = await prepareEbpData(recordsDate) - - const ebpEvents = birdsRecords.reduce((acc, record) => { - if (!acc[record.etrs89GridCode]) { - acc[record.etrs89GridCode] = [] - } - acc[record.etrs89GridCode].push(record) - - return acc - }, {}) - - const etrs89GridCodeCounts = Object.entries(ebpEvents).reduce((acc, [key, value]) => { - acc[key] = value.length - return acc - }, {}) + const ebpData = await prepareEbpData(recordsDate) const operationTime = new Date().getTime() - startTimestamp - console.log('+++++ EBP DATA: ', birdsRecords?.length) - console.log('+++++ EBP RECORDS: ', ebpRecords?.length) - console.log('+++++ EBP EVENT: ', ebpEvent) - console.log('+++++ ETRS codes counts: ', etrs89GridCodeCounts) + console.log('+++++ EBP DATA: ', ebpData) console.log('+++++ OPERATION TIME: ', operationTime) } } From c553f244dd429bd50d4a86aba397cf3c82c312e5 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Mon, 9 Dec 2024 11:07:29 +0200 Subject: [PATCH 15/29] feat(ebp-upload): send data to EBP --- server/config/app.js | 2 +- server/tasks/uploadToEBP.js | 47 +++++++++++++++++++++++++++++-------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/server/config/app.js b/server/config/app.js index bce2bcef4..bc72d559d 100644 --- a/server/config/app.js +++ b/server/config/app.js @@ -35,7 +35,7 @@ exports.default = { // max records per task maxRecords: parseInt(process.env.ETRS89_TASK_MAX_RECORDS, 10) || 100, // consider records newer than this timestamp - startTimestamp: parseInt(process.env.ETRS89_TASK_START_TIME, 10) || new Date('2024-07-01').getTime() + startTimestamp: parseInt(process.env.ETRS89_TASK_START_TIME, 10) || new Date('2024-01-01').getTime() } } } diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js index d0020e26c..afe3a3617 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/uploadToEBP.js @@ -4,6 +4,7 @@ const { Op } = sequelize const startOfDay = require('date-fns/startOfDay') const endOfDay = require('date-fns/endOfDay') const format = require('date-fns/format') +const fetch = require('node-fetch') // const availableForms = [api.forms.formCBM, api.forms.formBirds, api.forms.formCiconia] const availableForms = [api.forms.formBirds] @@ -12,6 +13,7 @@ const availableForms = [api.forms.formBirds] const API_TOKEN = process.env.EBP_API_TOKEN // eslint-disable-next-line no-unused-vars const apiParams = { + partnerSource: 'BUL_SBI', provisionMode: { bulk: { // all data is replaced code: 'B' @@ -165,7 +167,7 @@ const prepareEbpData = async (date = new Date()) => { // common event data const ebpEvent = { data_type: apiParams.dataType.casual.code, - locationMode: apiParams.locationMode.aggregatedLocation.code, + location_mode: apiParams.locationMode.aggregatedLocation.code, date: format(date, 'yyyy-MM-dd'), state: apiParams.eventState.modified.code } @@ -185,6 +187,7 @@ const prepareEbpData = async (date = new Date()) => { const eventData = { event: { event_id: generateEventId(etrsCode, date), + location: etrsCode, ...ebpEvent }, records: [] @@ -195,11 +198,13 @@ const prepareEbpData = async (date = new Date()) => { // group records by species const speciesRecords = records.reduce((acc, record) => { + // count unique species-users records const key = `${record.species}_${record.userId}` if (!speciesUsersRecords.includes(key)) { speciesUsersRecords.push(key) } + // count unique observers if (!observers.includes(record.userId)) { observers.push(record.userId) } @@ -235,7 +240,7 @@ const prepareEbpData = async (date = new Date()) => { event_id: eventData.event.event_id, record_id: eventData.event.event_id + '_' + speciesRecord.species_code, species_code: speciesRecord.species_code, - count: speciesRecord.records.reduce((acc, record) => acc + record.count, 0), + count: speciesRecord.records.reduce((acc, record) => acc + Math.max(record.count, record.countMin, record.countMax), 0), records_of_species: speciesRecord.users.length, breeding_code: speciesRecord.breeding_code, state: apiParams.recordState.modified.code @@ -243,15 +248,20 @@ const prepareEbpData = async (date = new Date()) => { }) eventData.event.records = speciesUsersRecords.length - eventData.event.observers = observers.length + eventData.event.observer = observers.length?.toString() acc.push(eventData) return acc }, []) - console.log('+++++ EBP EVENTS: ', JSON.stringify(ebpEvents)) - - return ebpEvents + return { + mode: apiParams.provisionMode.test.code, + partner_source: apiParams.partnerSource, + start_date: format(date, 'yyyy-MM-dd'), + end_date: format(date, 'yyyy-MM-dd'), + events: ebpEvents.map(event => event.event), + records: ebpEvents.map(event => event.records).flat() + } } module.exports = class UploadToEBP extends Task { @@ -264,15 +274,32 @@ module.exports = class UploadToEBP extends Task { this.frequency = 0 } - async run () { - const recordsDate = new Date('2024-01-13') + async run ({ date } = {}) { + console.log('+++++ UPLOAD TO EBP TASK: ', date) + const recordsDate = new Date(date || '2024-01-15') const startTimestamp = new Date().getTime() - const ebpData = await prepareEbpData(recordsDate) + const eventsData = await prepareEbpData(recordsDate) const operationTime = new Date().getTime() - startTimestamp - console.log('+++++ EBP DATA: ', ebpData) + try { + const response = await fetch('https://api-v2.eurobirdportal.org/data/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${API_TOKEN}` + }, + body: JSON.stringify(eventsData) + }) + + console.log('+++++ EBP RESPONSE: ', response.status) + console.log('+++++ RESPONSE: ', await response.json()) + } catch (error) { + console.log('+++++ ERROR: ', error) + } + + console.log('+++++ EBP DATA: ', JSON.stringify(eventsData)) console.log('+++++ OPERATION TIME: ', operationTime) } } From 70f7424b8cb76b35d666c33a7e3ee897102fbc1b Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Mon, 9 Dec 2024 17:31:00 +0200 Subject: [PATCH 16/29] fixup! feat(ebp-upload): send data to EBP --- server/tasks/uploadToEBP.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/server/tasks/uploadToEBP.js b/server/tasks/uploadToEBP.js index afe3a3617..b45b48096 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/uploadToEBP.js @@ -11,6 +11,7 @@ const availableForms = [api.forms.formBirds] // eslint-disable-next-line no-unused-vars const API_TOKEN = process.env.EBP_API_TOKEN +const API_URL = 'https://api-v2.eurobirdportal.org' // eslint-disable-next-line no-unused-vars const apiParams = { partnerSource: 'BUL_SBI', @@ -122,7 +123,8 @@ const loadRecords = async (forms, date) => { }, organization: { [Op.in]: allowedOrganizations() }, sourceEn: { [Op.notIn]: excludedSources() } - } + }, + limit: 5 }) })) @@ -159,7 +161,6 @@ const prepareEbpData = async (date = new Date()) => { // load all records for the given date const records = await loadRecords(availableForms, date) - console.log('+++++ RECORDS: ', records?.length) // additional filtering const filtered = await filterRecords(records, ebpSpecies, ebpSpeciesStatus) @@ -275,16 +276,15 @@ module.exports = class UploadToEBP extends Task { } async run ({ date } = {}) { - console.log('+++++ UPLOAD TO EBP TASK: ', date) const recordsDate = new Date(date || '2024-01-15') - const startTimestamp = new Date().getTime() + // const startTimestamp = new Date().getTime() const eventsData = await prepareEbpData(recordsDate) - const operationTime = new Date().getTime() - startTimestamp + // const operationTime = new Date().getTime() - startTimestamp try { - const response = await fetch('https://api-v2.eurobirdportal.org/data/', { + const response = await fetch(`${API_URL}/data/`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -293,13 +293,13 @@ module.exports = class UploadToEBP extends Task { body: JSON.stringify(eventsData) }) - console.log('+++++ EBP RESPONSE: ', response.status) - console.log('+++++ RESPONSE: ', await response.json()) + api.log(`Successfully uploaded ${eventsData.events.length} events and ${eventsData.records.length} records to EBP`, 'info') + api.log(`EBP response: ${response.status} ${response.statusText}`, 'info') } catch (error) { - console.log('+++++ ERROR: ', error) + console.log('Failed to upload data to EBP', error) } - console.log('+++++ EBP DATA: ', JSON.stringify(eventsData)) - console.log('+++++ OPERATION TIME: ', operationTime) + // console.log('+++++ EBP DATA: ', JSON.stringify(eventsData)) + // console.log('+++++ OPERATION TIME: ', operationTime) } } From c0a22ed484689bffc81ca8bd85f3a96f0b0c9568 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Wed, 11 Dec 2024 17:05:23 +0200 Subject: [PATCH 17/29] feat(ebp-upload): add task action --- server/actions/tasks.js | 30 +++ server/config/routes.js | 1 + server/tasks/{uploadToEBP.js => ebpUpload.js} | 190 ++++++++++-------- 3 files changed, 135 insertions(+), 86 deletions(-) rename server/tasks/{uploadToEBP.js => ebpUpload.js} (53%) diff --git a/server/actions/tasks.js b/server/actions/tasks.js index c572363cc..1f83b7fc3 100644 --- a/server/actions/tasks.js +++ b/server/actions/tasks.js @@ -1,4 +1,5 @@ const { Action, api } = require('actionhero') +const differenceInDays = require('date-fns/differenceInDays') class BaseAction extends Action { constructor () { @@ -126,3 +127,32 @@ module.exports.etrs89Codes = class EnqueueFillEtrs89Codes extends BaseAction { return await api.tasks.enqueue('fill-etrs89-codes', { form, id, limit, force }) } } + +module.exports.ebpUpload = class EnqueueEbpUpload extends Action { + constructor () { + super() + this.name = 'tasks:enqueue:ebpUpload' + this.description = 'Trigger EBP upload' + this.middleware = ['auth', 'admin'] + this.inputs = { + startDate: {}, + endDate: {}, + mode: {} + } + } + + async run ({ params: { startDate, endDate, mode }, response }) { + if (startDate && endDate) { + // check if startDate is before endDate + if (new Date(startDate) > new Date(endDate)) { + throw new Error('startDate must be before endDate') + } + + if (differenceInDays(new Date(endDate), new Date(startDate)) > 31) { + throw new Error('Date range must be less than 31 days') + } + } + + response.result = await api.tasks.enqueue('ebpUpload', { startDate, endDate, mode: mode || 'insert' }) + } +} diff --git a/server/config/routes.js b/server/config/routes.js index 59a0debe8..a3b6f3166 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -134,6 +134,7 @@ exports.default = { { path: '/zone/:id/owner/response', action: 'zone:respondOwnershipRequest' }, { path: '/tasks/auto-translate-nomenclatures', action: 'tasks:enqueue:autoTranslateNomenclatures' }, { path: '/tasks/etrs89', action: 'tasks:enqueue:etrs89Codes' }, + { path: '/tasks/ebp-upload', action: 'tasks:enqueue:ebpUpload' }, // forms { path: '/bats', action: 'formBats:create' }, diff --git a/server/tasks/uploadToEBP.js b/server/tasks/ebpUpload.js similarity index 53% rename from server/tasks/uploadToEBP.js rename to server/tasks/ebpUpload.js index b45b48096..cc14f5043 100644 --- a/server/tasks/uploadToEBP.js +++ b/server/tasks/ebpUpload.js @@ -1,8 +1,6 @@ const { Task, api } = require('actionhero') const sequelize = require('sequelize') const { Op } = sequelize -const startOfDay = require('date-fns/startOfDay') -const endOfDay = require('date-fns/endOfDay') const format = require('date-fns/format') const fetch = require('node-fetch') @@ -110,21 +108,21 @@ const getEbpSpeciesStatus = async () => { }) } -const loadRecords = async (forms, date) => { +const loadRecords = async (forms, startDate, endDate) => { const records = await Promise.allSettled(forms.map(async form => { return form.model.findAll({ where: { etrs89GridCode: { [Op.not]: null }, observationDateTime: { [Op.and]: [ - { [Op.gte]: startOfDay(date) }, - { [Op.lte]: endOfDay(date) } + { [Op.gte]: startDate }, + { [Op.lte]: endDate } ] }, organization: { [Op.in]: allowedOrganizations() }, sourceEn: { [Op.notIn]: excludedSources() } - }, - limit: 5 + } + }) })) @@ -155,133 +153,153 @@ const generateEventId = (etrsCode, date) => { return format(date, 'yyyyMMdd') + '_' + etrsCode } -const prepareEbpData = async (date = new Date()) => { +const prepareEbpData = async (startDate, endDate, mode) => { const ebpSpecies = await getEbpSpecies() const ebpSpeciesStatus = await getEbpSpeciesStatus() // load all records for the given date - const records = await loadRecords(availableForms, date) + const records = await loadRecords(availableForms, startDate, endDate) // additional filtering const filtered = await filterRecords(records, ebpSpecies, ebpSpeciesStatus) + // set event and records state based on the mode + const eventState = mode === 'delete' ? apiParams.eventState.removed.code : apiParams.eventState.modified.code + const recordsUpdateMode = mode === 'replace' ? apiParams.updateMode.all.code : apiParams.updateMode.modify.code + const recordState = mode === 'delete' ? apiParams.recordState.removed.code : apiParams.recordState.modified.code + // common event data const ebpEvent = { data_type: apiParams.dataType.casual.code, location_mode: apiParams.locationMode.aggregatedLocation.code, - date: format(date, 'yyyy-MM-dd'), - state: apiParams.eventState.modified.code + state: eventState, + record_updates_mode: recordsUpdateMode } - // group records by ETRS89 grid code - const etrsRecords = filtered.reduce((acc, record) => { - if (!acc[record.etrs89GridCode]) { - acc[record.etrs89GridCode] = [] + // group records by date + const recordsByDate = filtered.reduce((acc, record) => { + const formattedDate = format(record.observationDateTime, 'yyyy-MM-dd') + if (!acc[formattedDate]) { + acc[formattedDate] = [] } - acc[record.etrs89GridCode].push(record) + acc[formattedDate].push(record) return acc }, {}) - // prepare EBP events - const ebpEvents = Object.entries(etrsRecords).reduce((acc, [etrsCode, records]) => { - const eventData = { - event: { - event_id: generateEventId(etrsCode, date), - location: etrsCode, - ...ebpEvent - }, - records: [] - } + const ebpEvents = [] - const observers = [] - const speciesUsersRecords = [] - - // group records by species - const speciesRecords = records.reduce((acc, record) => { - // count unique species-users records - const key = `${record.species}_${record.userId}` - if (!speciesUsersRecords.includes(key)) { - speciesUsersRecords.push(key) + for (const [formattedDate, records] of Object.entries(recordsByDate)) { + // group records by ETRS89 grid code + const etrsRecords = records.reduce((acc, record) => { + if (!acc[record.etrs89GridCode]) { + acc[record.etrs89GridCode] = [] } + acc[record.etrs89GridCode].push(record) + + return acc + }, {}) - // count unique observers - if (!observers.includes(record.userId)) { - observers.push(record.userId) + // prepare EBP events + ebpEvents.push(Object.entries(etrsRecords).reduce((acc, [etrsCode, records]) => { + const eventData = { + event: { + event_id: generateEventId(etrsCode, new Date(formattedDate)), + location: etrsCode, + date: formattedDate, + ...ebpEvent + }, + records: [] } - if (!acc[record.species]) { - acc[record.species] = { - species: record.species, - species_code: ebpSpecies.find(species => species.sbNameLa === record.species)?.ebpId, - breeding_code: null, - users: [], - records: [] + const observers = [] + const speciesUsersRecords = [] + + // group records by species + const speciesRecords = records.reduce((acc, record) => { + // count unique species-users records + const key = `${record.species}_${record.userId}` + if (!speciesUsersRecords.includes(key)) { + speciesUsersRecords.push(key) } - } - if (!acc[record.species].users.includes(record.userId)) { - acc[record.species].users.push(record.userId) - } + // count unique observers + if (!observers.includes(record.userId)) { + observers.push(record.userId) + } - if (record.speciesStatusEn) { - const breedingCode = ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)?.ebpId - if (!acc[record.species].breeding_code || acc[record.species].breeding_code < breedingCode) { - acc[record.species].breeding_code = breedingCode + if (!acc[record.species]) { + acc[record.species] = { + species: record.species, + species_code: ebpSpecies.find(species => species.sbNameLa === record.species)?.ebpId, + breeding_code: null, + users: [], + records: [] + } } - } - acc[record.species].records.push(record) + if (!acc[record.species].users.includes(record.userId)) { + acc[record.species].users.push(record.userId) + } - return acc - }, {}) + if (record.speciesStatusEn) { + const breedingCode = ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)?.ebpId + if (!acc[record.species].breeding_code || acc[record.species].breeding_code < breedingCode) { + acc[record.species].breeding_code = breedingCode + } + } - eventData.records = Object.values(speciesRecords).map(speciesRecord => { - return { - event_id: eventData.event.event_id, - record_id: eventData.event.event_id + '_' + speciesRecord.species_code, - species_code: speciesRecord.species_code, - count: speciesRecord.records.reduce((acc, record) => acc + Math.max(record.count, record.countMin, record.countMax), 0), - records_of_species: speciesRecord.users.length, - breeding_code: speciesRecord.breeding_code, - state: apiParams.recordState.modified.code - } - }) + acc[record.species].records.push(record) - eventData.event.records = speciesUsersRecords.length - eventData.event.observer = observers.length?.toString() + return acc + }, {}) - acc.push(eventData) - return acc - }, []) + eventData.records = Object.values(speciesRecords).map(speciesRecord => { + return { + event_id: eventData.event.event_id, + record_id: eventData.event.event_id + '_' + speciesRecord.species_code, + species_code: speciesRecord.species_code, + count: speciesRecord.records.reduce((acc, record) => acc + Math.max(record.count, record.countMin, record.countMax), 0), + records_of_species: speciesRecord.users.length, + breeding_code: speciesRecord.breeding_code, + state: recordState + } + }) + + eventData.event.records = speciesUsersRecords.length + eventData.event.observer = observers.length?.toString() + + acc.push(eventData) + return acc + }, [])) + } return { - mode: apiParams.provisionMode.test.code, + mode: apiParams.provisionMode.standard.code, partner_source: apiParams.partnerSource, - start_date: format(date, 'yyyy-MM-dd'), - end_date: format(date, 'yyyy-MM-dd'), - events: ebpEvents.map(event => event.event), - records: ebpEvents.map(event => event.records).flat() + start_date: format(startDate, 'yyyy-MM-dd'), + end_date: format(endDate, 'yyyy-MM-dd'), + events: ebpEvents.flat().map(event => event.event), + records: ebpEvents.flat().map(event => event.records).flat() } } module.exports = class UploadToEBP extends Task { constructor () { super() - this.name = 'upload-to-ebp' + this.name = 'ebpUpload' this.description = 'Upload records to EBP' // use cronjob to schedule the task // npm run enqueue upload-to-ebp this.frequency = 0 } - async run ({ date } = {}) { - const recordsDate = new Date(date || '2024-01-15') - // const startTimestamp = new Date().getTime() + async run ({ startDate, endDate, mode } = {}) { + const startTimestamp = new Date().getTime() - const eventsData = await prepareEbpData(recordsDate) + const eventsData = await prepareEbpData(startDate ? new Date(startDate) : new Date(), endDate ? new Date(endDate) : new Date(), mode) - // const operationTime = new Date().getTime() - startTimestamp + const operationTime = new Date().getTime() - startTimestamp try { const response = await fetch(`${API_URL}/data/`, { @@ -299,7 +317,7 @@ module.exports = class UploadToEBP extends Task { console.log('Failed to upload data to EBP', error) } - // console.log('+++++ EBP DATA: ', JSON.stringify(eventsData)) - // console.log('+++++ OPERATION TIME: ', operationTime) + console.log('+++++ EBP DATA: ', JSON.stringify(eventsData)) + console.log('+++++ OPERATION TIME: ', operationTime) } } From db1541d7b4179d504f707fbdec21d3c2089a1cd7 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 12 Dec 2024 14:58:17 +0200 Subject: [PATCH 18/29] fixup! fixup! feat(ebp-upload): send data to EBP --- server/tasks/ebpUpload.js | 96 ++++++++++++++++++++------------------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/server/tasks/ebpUpload.js b/server/tasks/ebpUpload.js index cc14f5043..26d7971f0 100644 --- a/server/tasks/ebpUpload.js +++ b/server/tasks/ebpUpload.js @@ -215,56 +215,58 @@ const prepareEbpData = async (startDate, endDate, mode) => { const observers = [] const speciesUsersRecords = [] - // group records by species - const speciesRecords = records.reduce((acc, record) => { - // count unique species-users records - const key = `${record.species}_${record.userId}` - if (!speciesUsersRecords.includes(key)) { - speciesUsersRecords.push(key) - } - - // count unique observers - if (!observers.includes(record.userId)) { - observers.push(record.userId) - } - - if (!acc[record.species]) { - acc[record.species] = { - species: record.species, - species_code: ebpSpecies.find(species => species.sbNameLa === record.species)?.ebpId, - breeding_code: null, - users: [], - records: [] + if (mode !== 'delete') { + // group records by species + const speciesRecords = records.reduce((acc, record) => { + // count unique species-users records + const key = `${record.species}_${record.userId}` + if (!speciesUsersRecords.includes(key)) { + speciesUsersRecords.push(key) } - } - if (!acc[record.species].users.includes(record.userId)) { - acc[record.species].users.push(record.userId) - } + // count unique observers + if (!observers.includes(record.userId)) { + observers.push(record.userId) + } - if (record.speciesStatusEn) { - const breedingCode = ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)?.ebpId - if (!acc[record.species].breeding_code || acc[record.species].breeding_code < breedingCode) { - acc[record.species].breeding_code = breedingCode + if (!acc[record.species]) { + acc[record.species] = { + species: record.species, + species_code: ebpSpecies.find(species => species.sbNameLa === record.species)?.ebpId, + breeding_code: null, + users: [], + records: [] + } } - } - - acc[record.species].records.push(record) - - return acc - }, {}) - - eventData.records = Object.values(speciesRecords).map(speciesRecord => { - return { - event_id: eventData.event.event_id, - record_id: eventData.event.event_id + '_' + speciesRecord.species_code, - species_code: speciesRecord.species_code, - count: speciesRecord.records.reduce((acc, record) => acc + Math.max(record.count, record.countMin, record.countMax), 0), - records_of_species: speciesRecord.users.length, - breeding_code: speciesRecord.breeding_code, - state: recordState - } - }) + + if (!acc[record.species].users.includes(record.userId)) { + acc[record.species].users.push(record.userId) + } + + if (record.speciesStatusEn) { + const breedingCode = ebpSpeciesStatus.find(status => status.sbNameEn === record.speciesStatusEn)?.ebpId + if (!acc[record.species].breeding_code || acc[record.species].breeding_code < breedingCode) { + acc[record.species].breeding_code = breedingCode + } + } + + acc[record.species].records.push(record) + + return acc + }, {}) + + eventData.records = Object.values(speciesRecords).map(speciesRecord => { + return { + event_id: eventData.event.event_id, + record_id: eventData.event.event_id + '_' + speciesRecord.species_code, + species_code: speciesRecord.species_code, + count: speciesRecord.records.reduce((acc, record) => acc + Math.max(record.count, record.countMin, record.countMax), 0), + records_of_species: speciesRecord.users.length, + breeding_code: speciesRecord.breeding_code, + state: recordState + } + }) + } eventData.event.records = speciesUsersRecords.length eventData.event.observer = observers.length?.toString() @@ -280,7 +282,7 @@ const prepareEbpData = async (startDate, endDate, mode) => { start_date: format(startDate, 'yyyy-MM-dd'), end_date: format(endDate, 'yyyy-MM-dd'), events: ebpEvents.flat().map(event => event.event), - records: ebpEvents.flat().map(event => event.records).flat() + records: mode !== 'delete' ? ebpEvents.flat().map(event => event.records).flat() : [] } } From c456909c8692f06b219796a558b04f09a5547d57 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 12 Dec 2024 15:14:02 +0200 Subject: [PATCH 19/29] fixup! fixup! fixup! feat(ebp-upload): send data to EBP --- server/tasks/ebpUpload.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/tasks/ebpUpload.js b/server/tasks/ebpUpload.js index 26d7971f0..315788613 100644 --- a/server/tasks/ebpUpload.js +++ b/server/tasks/ebpUpload.js @@ -266,10 +266,10 @@ const prepareEbpData = async (startDate, endDate, mode) => { state: recordState } }) - } - eventData.event.records = speciesUsersRecords.length - eventData.event.observer = observers.length?.toString() + eventData.event.records = speciesUsersRecords.length + eventData.event.observer = observers.length?.toString() + } acc.push(eventData) return acc From 5400dbead902a09cdbc593eae4db56542c497488 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 13 Dec 2024 16:54:14 +0200 Subject: [PATCH 20/29] feat(ebp-upload): add ebp-species settings --- server/actions/ebp.js | 34 ++++++++++++++++++++++++++++++++++ server/config/routes.js | 4 +++- 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 server/actions/ebp.js diff --git a/server/actions/ebp.js b/server/actions/ebp.js new file mode 100644 index 000000000..d27ab1ac2 --- /dev/null +++ b/server/actions/ebp.js @@ -0,0 +1,34 @@ +const paging = require('../helpers/paging') +const incremental = require('../helpers/incremental') +const { upgradeAction } = require('../utils/upgrade') + +exports.ebbSpeciesList = upgradeAction('ah17', { + name: 'ebp:speciesList', + description: 'ebp:speciesList', + middleware: ['auth'], + inputs: paging.declareInputs(incremental.declareInputs({ + filter: {} + })), + + run: function (api, data, next) { + try { + let q = {} + q = paging.prepareQuery(q, data.params) + q = incremental.prepareQuery(q, data.params) + return api.models.ebp_birds.findAndCountAll(q).then(function (result) { + data.response.count = result.count + data.response.meta = incremental.generateMeta(data, paging.generateMeta(result.count, data)) + data.response.data = result.rows.map(function (ebpSpecies) { + return ebpSpecies.apiData(api) + }) + return next() + }).catch(function (e) { + console.error('Failure for list EBP species', e) + return next(e) + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index a3b6f3166..ff7b1bdec 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -112,7 +112,9 @@ exports.default = { { path: '/threats', action: 'formThreats:list' }, { path: '/threats.csv', action: 'formThreats:list' }, { path: '/threats.zip', action: 'formThreats:list' }, - { path: '/threats/:id', action: 'formThreats:view' } + { path: '/threats/:id', action: 'formThreats:view' }, + + { path: '/ebp-species', action: 'ebp:speciesList' } ], post: [ From 34105fa3124a84ad0c61ed9349c6b0c7d6716df7 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Tue, 17 Dec 2024 17:22:36 +0200 Subject: [PATCH 21/29] fixup! feat(ebp-upload): add ebp-species settings --- server/actions/ebp.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/actions/ebp.js b/server/actions/ebp.js index d27ab1ac2..1f91d9bb9 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -12,9 +12,10 @@ exports.ebbSpeciesList = upgradeAction('ah17', { run: function (api, data, next) { try { - let q = {} + let q = { + order: [['sbNameLa', 'ASC']] + } q = paging.prepareQuery(q, data.params) - q = incremental.prepareQuery(q, data.params) return api.models.ebp_birds.findAndCountAll(q).then(function (result) { data.response.count = result.count data.response.meta = incremental.generateMeta(data, paging.generateMeta(result.count, data)) From ee144b995e7d58ccc9aac48f123da4cdc8ff0e4c Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Tue, 7 Jan 2025 14:45:51 +0200 Subject: [PATCH 22/29] feat(ebp-upload): support updating ebp species --- server/actions/ebp.js | 58 ++++++++++++++++++++++++++++++++++++++++- server/config/routes.js | 3 ++- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/server/actions/ebp.js b/server/actions/ebp.js index 1f91d9bb9..3ad2a0152 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -1,11 +1,12 @@ const paging = require('../helpers/paging') const incremental = require('../helpers/incremental') const { upgradeAction } = require('../utils/upgrade') +const Promise = require('bluebird') exports.ebbSpeciesList = upgradeAction('ah17', { name: 'ebp:speciesList', description: 'ebp:speciesList', - middleware: ['auth'], + middleware: ['admin'], inputs: paging.declareInputs(incremental.declareInputs({ filter: {} })), @@ -33,3 +34,58 @@ exports.ebbSpeciesList = upgradeAction('ah17', { } } }) + +exports.ebbSpeciesUpdate = upgradeAction('ah17', { + name: 'ebp:speciesUpdate', + description: 'ebp:speciesUpdate', + middleware: ['admin'], + inputs: { + items: { required: true } + }, + + run: function (api, data, next) { + Promise.resolve() + .then(function () { + if (!(data.params.items instanceof Array)) { + data.connection.rawConnection.responseHttpCode = 400 + throw new Error('items is not array') + } + if (data.params.items.length <= 0) { + data.connection.rawConnection.responseHttpCode = 400 + throw new Error('cannot update with empty items') + } + return data.params.items.map(function (item) { + const record = api.models.ebp_birds.build({}) + record.apiUpdate(item) + return record + }) + }) + .then(function (models) { + return api.sequelize.sequelize.transaction(function (t) { + return api.models.ebp_birds + .destroy({ + where: {}, + transaction: t + }) + .then(function (deleted) { + api.log('replacing ebp_birds %d with %d', 'info', deleted, models.length) + }) + .then(function () { + return Promise.map(models, function (item) { + return item.save({ transaction: t }).then((m) => m.apiData()) + }) + }) + }) + }) + .then(function (result) { + data.response.count = result.length + data.response.data = result + }) + .then(function () { + return next() + }, function (e) { + api.log('Failure to update ebp_birds', 'error', e) + return next(e) + }) + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index ff7b1bdec..45c34c493 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -214,7 +214,8 @@ exports.default = { { path: '/pois/:type', action: 'poi:updateType' }, { path: '/session', action: 'session:check' }, { path: '/species/:type', action: 'species:updateType' }, - { path: '/zone/:id/owner', action: 'zone:setOwner' } + { path: '/zone/:id/owner', action: 'zone:setOwner' }, + { path: '/ebp-species', action: 'ebp:speciesUpdate' } ], patch: [ From dfb17ab316a4a0c92e2a0ca33fe4257c9cfdc6d8 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Tue, 7 Jan 2025 16:39:49 +0200 Subject: [PATCH 23/29] feat(ebp-upload): support updating ebp species statuses --- server/actions/ebp.js | 82 +++++++++++++++++++++++++++++++++++++++++ server/config/routes.js | 6 ++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/server/actions/ebp.js b/server/actions/ebp.js index 3ad2a0152..01855cb46 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -89,3 +89,85 @@ exports.ebbSpeciesUpdate = upgradeAction('ah17', { }) } }) + +exports.ebpSpeciesStatusList = upgradeAction('ah17', { + name: 'ebp:speciesStatusList', + description: 'ebp:speciesStatusList', + middleware: ['admin'], + + run: function (api, data, next) { + try { + const q = { + order: [['ebpId', 'ASC']] + } + return api.models.ebp_birds_status.findAndCountAll(q).then(function (result) { + data.response.count = result.count + data.response.data = result.rows.map(function (ebpSpeciesStatus) { + return ebpSpeciesStatus.apiData(api) + }) + return next() + }).catch(function (e) { + console.error('Failure for list EBP species statuses', e) + return next(e) + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) + +exports.ebbSpeciesStatusUpdate = upgradeAction('ah17', { + name: 'ebp:speciesStatusUpdate', + description: 'ebp:speciesStatusUpdate', + middleware: ['admin'], + inputs: { + items: { required: true } + }, + + run: function (api, data, next) { + Promise.resolve() + .then(function () { + if (!(data.params.items instanceof Array)) { + data.connection.rawConnection.responseHttpCode = 400 + throw new Error('items is not array') + } + if (data.params.items.length <= 0) { + data.connection.rawConnection.responseHttpCode = 400 + throw new Error('cannot update with empty items') + } + return data.params.items.map(function (item) { + const record = api.models.ebp_birds_status.build({}) + record.apiUpdate(item) + return record + }) + }) + .then(function (models) { + return api.sequelize.sequelize.transaction(function (t) { + return api.models.ebp_birds_status + .destroy({ + where: {}, + transaction: t + }) + .then(function (deleted) { + api.log('replacing ebp_birds_status %d with %d', 'info', deleted, models.length) + }) + .then(function () { + return Promise.map(models, function (item) { + return item.save({ transaction: t }).then((m) => m.apiData()) + }) + }) + }) + }) + .then(function (result) { + data.response.count = result.length + data.response.data = result + }) + .then(function () { + return next() + }, function (e) { + api.log('Failure to update ebp_birds_status', 'error', e) + return next(e) + }) + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index 45c34c493..2977ec8ca 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -114,7 +114,8 @@ exports.default = { { path: '/threats.zip', action: 'formThreats:list' }, { path: '/threats/:id', action: 'formThreats:view' }, - { path: '/ebp-species', action: 'ebp:speciesList' } + { path: '/ebp-species', action: 'ebp:speciesList' }, + { path: '/ebp-species-status', action: 'ebp:speciesStatusList' } ], post: [ @@ -215,7 +216,8 @@ exports.default = { { path: '/session', action: 'session:check' }, { path: '/species/:type', action: 'species:updateType' }, { path: '/zone/:id/owner', action: 'zone:setOwner' }, - { path: '/ebp-species', action: 'ebp:speciesUpdate' } + { path: '/ebp-species', action: 'ebp:speciesUpdate' }, + { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' } ], patch: [ From f6776eeb6103434443dbdc223a373bdf756d549e Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Wed, 8 Jan 2025 17:51:51 +0200 Subject: [PATCH 24/29] feat(ebp-upload): support updating ebp allowed organizations --- server/actions/ebp.js | 52 +++++++++++++++++++ server/config/routes.js | 6 ++- .../20250108094110-create-settings.js | 31 +++++++++++ server/models/settings.js | 38 ++++++++++++++ 4 files changed, 125 insertions(+), 2 deletions(-) create mode 100644 server/migrations/20250108094110-create-settings.js create mode 100644 server/models/settings.js diff --git a/server/actions/ebp.js b/server/actions/ebp.js index 01855cb46..fed2d9bfd 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -171,3 +171,55 @@ exports.ebbSpeciesStatusUpdate = upgradeAction('ah17', { }) } }) + +exports.ebpOrganizationsList = upgradeAction('ah17', { + name: 'ebp:organizationsList', + description: 'ebp:organizationsList', + middleware: ['admin'], + + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_organizations' + } + }).then(function (ebpOrganizations) { + data.response.data = JSON.parse(ebpOrganizations?.value || '[]') + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) + +exports.ebpOrganizationsUpdate = upgradeAction('ah17', { + name: 'ebp:organizationsUpdate', + description: 'ebp:organizationsUpdate', + middleware: ['admin'], + inputs: { + items: { required: true } + }, + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_organizations' + } + }).then(function (ebpOrganizations) { + if (!ebpOrganizations) { + ebpOrganizations = api.models.settings.build({ + name: 'ebp_organizations' + }) + } + ebpOrganizations.value = JSON.stringify(data.params.items) + ebpOrganizations.save() + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index 2977ec8ca..352be69c3 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -115,7 +115,8 @@ exports.default = { { path: '/threats/:id', action: 'formThreats:view' }, { path: '/ebp-species', action: 'ebp:speciesList' }, - { path: '/ebp-species-status', action: 'ebp:speciesStatusList' } + { path: '/ebp-species-status', action: 'ebp:speciesStatusList' }, + { path: '/ebp/organizations', action: 'ebp:organizationsList' } ], post: [ @@ -217,7 +218,8 @@ exports.default = { { path: '/species/:type', action: 'species:updateType' }, { path: '/zone/:id/owner', action: 'zone:setOwner' }, { path: '/ebp-species', action: 'ebp:speciesUpdate' }, - { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' } + { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' }, + { path: '/ebp/organizations', action: 'ebp:organizationsUpdate' } ], patch: [ diff --git a/server/migrations/20250108094110-create-settings.js b/server/migrations/20250108094110-create-settings.js new file mode 100644 index 000000000..f66959ac0 --- /dev/null +++ b/server/migrations/20250108094110-create-settings.js @@ -0,0 +1,31 @@ +'use strict' + +const { DataTypes } = require('sequelize') +const tableName = 'settings' + +module.exports = { + up: async function (queryInterface, Sequelize) { + await queryInterface.createTable(tableName, { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: Sequelize.TEXT, + allowNull: false + }, + value: { + type: Sequelize.TEXT + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE + }) + + await queryInterface.addIndex(tableName, { unique: true, fields: ['name'] }) + }, + + down: async function (queryInterface, Sequelize) { + await queryInterface.dropTable(tableName) + } +} diff --git a/server/models/settings.js b/server/models/settings.js new file mode 100644 index 000000000..33bc3a674 --- /dev/null +++ b/server/models/settings.js @@ -0,0 +1,38 @@ +'use strict' + +module.exports = function (sequelize, DataTypes) { + return sequelize.define('settings', { + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + name: { + type: DataTypes.TEXT, + allowNull: false + }, + value: { + type: DataTypes.TEXT + }, + createdAt: DataTypes.DATE, + updatedAt: DataTypes.DATE + }, { + tableName: 'settings', + indexes: [ + { unique: true, fields: ['name'] } + ], + instanceMethods: { + apiData: function () { + return { + id: this.id, + name: this.name, + value: this.value + } + }, + apiUpdate: function (data) { + this.name = data.name + this.value = data.value + } + } + }) +} From ba0973fd035c75255db03fdd7ce3b52fd0977f5b Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 9 Jan 2025 10:44:40 +0200 Subject: [PATCH 25/29] feat(ebp-upload): support updating ebp allowed sources --- server/actions/ebp.js | 52 +++++++++++++++++++++++++++++++++++++++++ server/config/routes.js | 6 +++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/server/actions/ebp.js b/server/actions/ebp.js index fed2d9bfd..c9bf700ae 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -223,3 +223,55 @@ exports.ebpOrganizationsUpdate = upgradeAction('ah17', { } } }) + +exports.ebpSourcesList = upgradeAction('ah17', { + name: 'ebp:sourcesList', + description: 'ebp:sourcesList', + middleware: ['admin'], + + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_sources' + } + }).then(function (ebpSources) { + data.response.data = JSON.parse(ebpSources?.value || '[]') + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) + +exports.ebpSourcesUpdate = upgradeAction('ah17', { + name: 'ebp:sourcesUpdate', + description: 'ebp:sourcesUpdate', + middleware: ['admin'], + inputs: { + items: { required: true } + }, + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_sources' + } + }).then(function (ebpSources) { + if (!ebpSources) { + ebpSources = api.models.settings.build({ + name: 'ebp_sources' + }) + } + ebpSources.value = JSON.stringify(data.params.items) + ebpSources.save() + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index 352be69c3..2f94576f7 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -116,7 +116,8 @@ exports.default = { { path: '/ebp-species', action: 'ebp:speciesList' }, { path: '/ebp-species-status', action: 'ebp:speciesStatusList' }, - { path: '/ebp/organizations', action: 'ebp:organizationsList' } + { path: '/ebp/organizations', action: 'ebp:organizationsList' }, + { path: '/ebp/sources', action: 'ebp:sourcesList' } ], post: [ @@ -219,7 +220,8 @@ exports.default = { { path: '/zone/:id/owner', action: 'zone:setOwner' }, { path: '/ebp-species', action: 'ebp:speciesUpdate' }, { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' }, - { path: '/ebp/organizations', action: 'ebp:organizationsUpdate' } + { path: '/ebp/organizations', action: 'ebp:organizationsUpdate' }, + { path: '/ebp/sources', action: 'ebp:sourcesUpdate' } ], patch: [ From a4765a07a92709f7a39f099503831a53c9748d04 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Thu, 9 Jan 2025 17:00:59 +0200 Subject: [PATCH 26/29] feat(ebp-upload): support updating ebp upload protocol --- server/actions/ebp.js | 54 +++++++++++++++++++++++++++++++++++++++++ server/config/routes.js | 6 +++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/server/actions/ebp.js b/server/actions/ebp.js index c9bf700ae..1ceaaf31c 100644 --- a/server/actions/ebp.js +++ b/server/actions/ebp.js @@ -275,3 +275,57 @@ exports.ebpSourcesUpdate = upgradeAction('ah17', { } } }) + +exports.ebpProtocolGet = upgradeAction('ah17', { + name: 'ebp:protocolGet', + description: 'ebp:protocolGet', + middleware: ['admin'], + + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_protocol' + } + }).then(function (setting) { + data.response.data = { + protocol: setting?.value || '' + } + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) + +exports.ebpProtocolUpdate = upgradeAction('ah17', { + name: 'ebp:protocolUpdate', + description: 'ebp:protocolUpdate', + middleware: ['admin'], + inputs: { + protocol: {} + }, + run: function (api, data, next) { + try { + return api.models.settings.findOne({ + where: { + name: 'ebp_protocol' + } + }).then(function (setting) { + if (!setting) { + setting = api.models.settings.build({ + name: 'ebp_protocol' + }) + } + setting.value = data.params.protocol + setting.save() + return next() + }) + } catch (e) { + console.error(e) + return next(e) + } + } +}) diff --git a/server/config/routes.js b/server/config/routes.js index 2f94576f7..9c4a1a7d9 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -117,7 +117,8 @@ exports.default = { { path: '/ebp-species', action: 'ebp:speciesList' }, { path: '/ebp-species-status', action: 'ebp:speciesStatusList' }, { path: '/ebp/organizations', action: 'ebp:organizationsList' }, - { path: '/ebp/sources', action: 'ebp:sourcesList' } + { path: '/ebp/sources', action: 'ebp:sourcesList' }, + { path: '/ebp/protocol', action: 'ebp:protocolGet' } ], post: [ @@ -221,7 +222,8 @@ exports.default = { { path: '/ebp-species', action: 'ebp:speciesUpdate' }, { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' }, { path: '/ebp/organizations', action: 'ebp:organizationsUpdate' }, - { path: '/ebp/sources', action: 'ebp:sourcesUpdate' } + { path: '/ebp/sources', action: 'ebp:sourcesUpdate' }, + { path: '/ebp/protocol', action: 'ebp:protocolUpdate' } ], patch: [ From a8918e939f067c3406c436a1e82c8ea1f77b499d Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 10 Jan 2025 10:05:02 +0200 Subject: [PATCH 27/29] feat(ebp-upload): change some routes --- server/config/routes.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/server/config/routes.js b/server/config/routes.js index 9c4a1a7d9..286b76803 100644 --- a/server/config/routes.js +++ b/server/config/routes.js @@ -114,8 +114,8 @@ exports.default = { { path: '/threats.zip', action: 'formThreats:list' }, { path: '/threats/:id', action: 'formThreats:view' }, - { path: '/ebp-species', action: 'ebp:speciesList' }, - { path: '/ebp-species-status', action: 'ebp:speciesStatusList' }, + { path: '/ebp/species', action: 'ebp:speciesList' }, + { path: '/ebp/species-status', action: 'ebp:speciesStatusList' }, { path: '/ebp/organizations', action: 'ebp:organizationsList' }, { path: '/ebp/sources', action: 'ebp:sourcesList' }, { path: '/ebp/protocol', action: 'ebp:protocolGet' } @@ -219,8 +219,8 @@ exports.default = { { path: '/session', action: 'session:check' }, { path: '/species/:type', action: 'species:updateType' }, { path: '/zone/:id/owner', action: 'zone:setOwner' }, - { path: '/ebp-species', action: 'ebp:speciesUpdate' }, - { path: '/ebp-species-status', action: 'ebp:speciesStatusUpdate' }, + { path: '/ebp/species', action: 'ebp:speciesUpdate' }, + { path: '/ebp/species-status', action: 'ebp:speciesStatusUpdate' }, { path: '/ebp/organizations', action: 'ebp:organizationsUpdate' }, { path: '/ebp/sources', action: 'ebp:sourcesUpdate' }, { path: '/ebp/protocol', action: 'ebp:protocolUpdate' } From a2b2512f9f6e0968b8d318da205c5635b5f66271 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 10 Jan 2025 10:31:41 +0200 Subject: [PATCH 28/29] feat(ebp-upload): read ebp organizations and sources from the settings --- server/tasks/ebpUpload.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/server/tasks/ebpUpload.js b/server/tasks/ebpUpload.js index 315788613..4e1a11ec1 100644 --- a/server/tasks/ebpUpload.js +++ b/server/tasks/ebpUpload.js @@ -75,12 +75,24 @@ const apiParams = { } } -const allowedOrganizations = () => { - return ['bspb', 'independent'] +const getAllowedOrganizations = async () => { + const organizationsSetting = await api.models.settings.findOne({ + where: { + name: 'ebp_organizations' + } + }) + + return JSON.parse(organizationsSetting?.value || '[]') } -const excludedSources = () => { - return ['Project NMNH-BAS & MOEW', 'Research of breeding birds, BSPB-NMNH'] +const getAllowedSources = async () => { + const sourcesSetting = await api.models.settings.findOne({ + where: { + name: 'ebp_sources' + } + }) + + return JSON.parse(sourcesSetting?.value || '[]') } const getSensitiveSpecies = async () => { @@ -109,6 +121,9 @@ const getEbpSpeciesStatus = async () => { } const loadRecords = async (forms, startDate, endDate) => { + const allowedOrganizations = await getAllowedOrganizations() + const allowedSources = await getAllowedSources() + const records = await Promise.allSettled(forms.map(async form => { return form.model.findAll({ where: { @@ -119,8 +134,8 @@ const loadRecords = async (forms, startDate, endDate) => { { [Op.lte]: endDate } ] }, - organization: { [Op.in]: allowedOrganizations() }, - sourceEn: { [Op.notIn]: excludedSources() } + organization: { [Op.in]: allowedOrganizations }, + sourceEn: { [Op.in]: allowedSources } } }) From 0be7aad1b5e2985ac6d8834dbce1e9d1524ff494 Mon Sep 17 00:00:00 2001 From: Daniel Petrov Date: Fri, 10 Jan 2025 13:57:39 +0200 Subject: [PATCH 29/29] feat(ebp-upload): update bg and en translations --- i18n/bg.json | 445 +++++++++++++++++++++++++++++++++++++++++++++------ i18n/en.json | 437 +++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 797 insertions(+), 85 deletions(-) diff --git a/i18n/bg.json b/i18n/bg.json index 206185ded..29ec43a21 100644 --- a/i18n/bg.json +++ b/i18n/bg.json @@ -41,17 +41,17 @@ "PUBLIC_HOME_ABOUT_TITLE": "За проекта", "PUBLIC_HOME_ABOUT_BODY": "SmartBirds.org e електронна база данни разработена от {{bspbLinkStart}}Българско дружество за защита на птиците{{bspbLinkEnd}}. Целта и е да служи като електронен бележник за съхранение на записи от наблюдения на птици, бозайници, земноводни, влечуги, защитени и други безгръбначни животни. Записването на всяко едно наблюдение подпомага природозащитните дейности в България. Затова записвайте наблюденията си директно тук в страницата или чрез мобилното приложение {{mobileLinkStart}}SmartBirds Pro{{mobileLinkEnd}}. Ако обичате природата и желаете да се включите в инициативата за преброяване на птиците около нас, регистрирайте се в SmartBirds {{registerLinkStart}}тук{{registerLinkEnd}} и изберете своето лично място за броене на птици.", "PUBLIC_HOME_FUNDING_TITLE": "Финансиране", - "PUBLIC_HOME_FUNDING_BODY1": "Модулът за Мониторинг на обикновените видове птици (МОВП) към портала SmartBirds.org е финансиран по проект „Птиците за хората и хората за птиците: с добра воля за активно опазване на природата” в рамките на {{linkStart}}Програми BG03 в България{{linkEnd}} по Финансовия механизъм на Европейското икономическо пространство 2009–2014 г.", - "PUBLIC_HOME_FUNDING_BODY2": "Останалите модули на SmartBirds.org са осъществени в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "PUBLIC_HOME_FUNDING_BODY1": "Информационна система с биологична информация SmartBirds.org и мобилните приложения {{linkSmartBirdsProStart}}SmartBirds Pro{{linkEnd}} и {{linkSmartBirdsStart}}SmartBirds{{linkEnd}} са разработени и обновявани от БДЗП с финансовата подкрепа на:", + "PUBLIC_HOME_FUNDING_BODY2": "Част от модулите на SmartBirds.org и SmartBirds Pro са разработени в партньорство с {{linkNMNHSStart}}Национален природонаучен музей при БАН{{linkEnd}}.\nИнформационна система с биологична информация на БДЗП е част от {{linkEuroBirdPortalStart}}EuroBirdPortal{{linkEnd}} – инициатива на {{linkEuroBirdCensusStart}}European Bird Census Council{{linkEnd}}.", "PUBLIC_BIRDS_TITLE": "Форма Птици", "PUBLIC_BIRDS_SUBTITLE": "Наблюдения от стандартна форма птици", - "PUBLIC_BIRDS_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "PUBLIC_BIRDS_TEXT": "Тази част от SmartBirds.org е осъществена с финансовата подкрепа на Програма LIFE на Европейския съюз.", "PUBLIC_CBM_TITLE": "МОВП", "PUBLIC_CBM_SUBTITLE": "Площадки за мониторинг на обикновените видове птици", - "PUBLIC_CBM_TEXT": "Модулът за Мониторинг на обикновените видове птици (МОВП) към портала SmartBirds.org е финансиран по проект „Птиците за хората и хората за птиците: с добра воля за активно опазване на природата” в рамките на {{linkStart}}Програми BG03{{linkEnd}} в България по Финансовия механизъм на Европейското икономическо пространство 2009–2014 г.", + "PUBLIC_CBM_TEXT": "Модулът за Мониторинг на обикновените видове птици (МОВП) към портала SmartBirds.org е финансиран по проект „Птиците за хората и хората за птиците: с добра воля за активно опазване на природата” в рамките на {{linkStart}}Програми BG03{{linkEnd}} в България по Финансовия механизъм на Европейското икономическо пространство 2009–2014 г. и проект „Осъществяване на схемата за мониторинг на обикновените видове птици, като част от Националната система за мониторинг на биологичното разнообразие“, финансиран от ПУДООС.", "PUBLIC_CICONIA_TITLE": "Форма бял щъркел", "PUBLIC_CICONIA_SUBTITLE": "Наблюдения на бял щъркел", - "PUBLIC_CICONIA_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "PUBLIC_CICONIA_TEXT": "Тази част от SmartBirds.org е осъществена с финансовата подкрепа на Програма LIFE на Европейския съюз. Модулът е подпомогнат и в рамките на проект \"SmartStorkBelozem - развитие на гражданска наука за щъркелите в Белозем - Европейско село на щъркелите в България\", финансиран от фондация EuroNatur, Германия.", "PUBLIC_HERP_TITLE": "Форма ЗВБ", "PUBLIC_HERP_SUBTITLE": "Наблюдения на земноводни, влечуги и бозайници", "PUBLIC_HERP_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", @@ -134,7 +134,7 @@ "CBM_LIST_TITLE_SECONDARY": "Mониторинг на обикновените видове птици", "CBM_LIST_BUTTON_DELETE": "Изтриване", "CBM_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "CBM_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "CBM_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "CBM_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "CBM_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "CBM_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -194,7 +194,7 @@ "BIRDS_LIST_TITLE_SECONDARY": "Наблюдения от стандартна форма птици", "BIRDS_LIST_BUTTON_DELETE": "Изтриване", "BIRDS_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "BIRDS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "BIRDS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "BIRDS_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "BIRDS_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "BIRDS_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -220,7 +220,6 @@ "BIRDS_DETAIL_TITLE_NEW": "Ново наблюдение", "BIRDS_DETAIL_TITLE_EDIT": "Редакция на запис", "BIRDS_DETAIL_SPECIES": "Вид научно име", - "BIRDS_DETAIL_CONFIDENTIAL": "Поверително", "BIRDS_DETAIL_LATITUDE": "Географска ширина", "BIRDS_DETAIL_LONGITUDE": "Географска дължина", "BIRDS_DETAIL_COUNT_UNIT": "Единица на броя", @@ -269,7 +268,7 @@ "HERP_LIST_TITLE_SECONDARY": "Наблюдения на земноводни, влечуги и бозайници", "HERP_LIST_BUTTON_DELETE": "Изтриване", "HERP_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "HERP_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "HERP_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "HERP_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "HERP_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "HERP_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -296,8 +295,8 @@ "HERP_DETAIL_TITLE_EDIT": "Редакция на запис", "HERP_DETAIL_LATITUDE": "Географска ширина", "HERP_DETAIL_LONGITUDE": "Географска дължина", - "HERP_DETAIL_SPECIES": "Вид научно име", - "HERP_DETAIL_COUNT": "Брой", + "FIELD_SPECIES": "Вид научно име", + "FIELD_COUNT": "Брой", "HERP_DETAIL_AGE": "Възраст", "HERP_DETAIL_SEX": "Пол", "HERP_DETAIL_HABITAT": "Местообитание", @@ -319,7 +318,7 @@ "CICONIA_LIST_TITLE_SECONDARY": "Наблюдения на бял щъркел", "CICONIA_LIST_BUTTON_DELETE": "Изтриване", "CICONIA_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "CICONIA_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "CICONIA_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "CICONIA_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "CICONIA_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "CICONIA_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -464,12 +463,12 @@ "FORM_MAMMALS_SHORT": "Бозайници", "PUBLIC_HERPTILES_TITLE": "Форма З и В", "PUBLIC_HERPTILES_SUBTITLE": "Наблюдения на земноводни и влечуги", - "PUBLIC_HERPTILES_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "PUBLIC_HERPTILES_TEXT": "Тази част от www.SmartBirds.org е осъществена с финансовата подкрепа на Програма LIFE на Европейския съюз.", "PUBLIC_HERPTILES_MAP_SPECIES": "видове", "PUBLIC_HERPTILES_MAP_INDIVIDUALS": "индивиди", "PUBLIC_MAMMALS_TITLE": "Форма Бозайници", "PUBLIC_MAMMALS_SUBTITLE": "Наблюдения на бозайници", - "PUBLIC_MAMMALS_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "PUBLIC_MAMMALS_TEXT": "Тази част от www.SmartBirds.org е осъществена с финансовата подкрепа на Програма LIFE на Европейския съюз.", "PUBLIC_MAMMALS_MAP_SPECIES": "видове", "PUBLIC_MAMMALS_MAP_INDIVIDUALS": "индивиди", "DASHBOARD_CARD_HERPTILES": "З и В", @@ -478,7 +477,7 @@ "HERPTILES_LIST_TITLE_SECONDARY": "Наблюдения на земноводни и влечуги", "HERPTILES_LIST_BUTTON_DELETE": "Изтриване", "HERPTILES_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "HERPTILES_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "HERPTILES_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "HERPTILES_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "HERPTILES_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "HERPTILES_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -528,7 +527,7 @@ "MAMMALS_LIST_TITLE_SECONDARY": "Наблюдения на бозайници", "MAMMALS_LIST_BUTTON_DELETE": "Изтриване", "MAMMALS_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "MAMMALS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "MAMMALS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "MAMMALS_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "MAMMALS_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "MAMMALS_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -625,20 +624,20 @@ "DOWNLOAD_TITLE": "Свалени данни", "You will be notified by email when your export is ready": "Ще бъдете уведомени с емайл, когато експортът Ви е готов.", "Error during export": "Грешка по време на експорт", - "FORM_INVERTEBRATES_LONG": "Защитени безгръбначни", - "FORM_INVERTEBRATES_SHORT": "Защитени безгръбначни", - "FORM_LABEL_INVERTEBRATES": "Защитени безгръбначни", - "DASHBOARD_CARD_INVERTEBRATES": "Защитени безгръбначни", - "PUBLIC_INVERTEBRATES_TITLE": "Форма Защитени безгръбначни", - "PUBLIC_INVERTEBRATES_SUBTITLE": "Наблюдения на защитени безгръбначни", - "PUBLIC_INVERTEBRATES_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Опазване на ключови горски хабитати на малкия креслив орел в България”{{linkEnd}} (LIFE12 NAT/BG/001218), финансиран от програма LIFE+ на Европейския съюз.", + "FORM_INVERTEBRATES_LONG": "Безгръбначни животни", + "FORM_INVERTEBRATES_SHORT": "Безгръбначни животни", + "FORM_LABEL_INVERTEBRATES": "Безгръбначни животни", + "DASHBOARD_CARD_INVERTEBRATES": "Безгръбначни животни", + "PUBLIC_INVERTEBRATES_TITLE": "Форма Безгръбначни животни", + "PUBLIC_INVERTEBRATES_SUBTITLE": "Наблюдения на безгръбначни животни", + "PUBLIC_INVERTEBRATES_TEXT": "Тази част от www.SmartBirds.org е осъществена в партньорство с Националния природонаучен музей при БАН и с финансовата подкрепа на Програма LIFE на Европейския съюз.", "PUBLIC_INVERTEBRATES_MAP_SPECIES": "видове", "PUBLIC_INVERTEBRATES_MAP_INDIVIDUALS": "индивиди", - "INVERTEBRATES_LIST_TITLE_MAIN": "Форма Защитени безгръбначни", - "INVERTEBRATES_LIST_TITLE_SECONDARY": "Наблюдения на защитени безгръбначни", + "INVERTEBRATES_LIST_TITLE_MAIN": "Форма Безгръбначни животни", + "INVERTEBRATES_LIST_TITLE_SECONDARY": "Наблюдения на безгръбначни животни", "INVERTEBRATES_LIST_BUTTON_DELETE": "Изтриване", "INVERTEBRATES_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", - "INVERTEBRATES_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "INVERTEBRATES_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "INVERTEBRATES_LIST_FOUND_RECORDS_COUNT_TOOLTIP": "Намерени записи отговарящи на избраните филтри", "INVERTEBRATES_LIST_BUTTON_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи", "INVERTEBRATES_LIST_BUTTON_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи, техните снимки и gpx файлове", @@ -686,7 +685,7 @@ "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", - "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Място", + "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", "USER_DETAIL_PRIVACY": "Публични данни и статистика", @@ -698,9 +697,6 @@ "WARNING_EXPORT_LIMIT": "Системата ограничава единичен експорт до 20 000 записа", "SPECIES_COL_INTERESTING": "Интересен", "SPECIES_COL_SENSITIVE": "Чувствителен", - "HERPTILES_DETAIL_CONFIDENTIAL": "Поверително", - "MAMMALS_DETAIL_CONFIDENTIAL": "Поверително", - "INVERTEBRATES_DETAIL_CONFIDENTIAL": "Поверително", "WARNING_EXPORT_DISABLED": "Експортът на данни от други потребители не е разрешен", "NAV_FRIENDS": "Приятели", "FRIENDS_TITLE": "Модул за управление на приятели", @@ -714,7 +710,6 @@ "FRIENDS_UNSAVED_DATA_ALERT_MESSAGE": "Имате незаписани промени. Сигурни ли сте, че искате да продължите?", "FRIENDS_SAVE_SUCCESS": "Приятелите са успешно добавени", "FRIENDS_SAVE_ERROR": "Грешка при добавяне на приятели", - "CBM_DETAIL_CONFIDENTIAL": "Поверително", "PUBLIC_BIRDS_STATISTIC_TABLE_COL_COUNT_SPECIES": "{{count}} вида", "PUBLIC_BIRDS_STATISTIC_TABLE_COL_COUNT_RECORDS": "{{count}} записа", "NAV_PRIVATE_FORMS": "Лични и споделени данни", @@ -727,8 +722,8 @@ "HERPTILES_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на земноводни и влечуги", "MAMMALS_PUBLIC_LIST_TITLE_MAIN": "Форма Бозайници", "MAMMALS_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на бозайници", - "INVERTEBRATES_PUBLIC_LIST_TITLE_MAIN": "Форма Защитени безгръбначни", - "INVERTEBRATES_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на защитени безгръбначни", + "INVERTEBRATES_PUBLIC_LIST_TITLE_MAIN": "Форма Безгръбначни животни", + "INVERTEBRATES_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на безгръбначни животни", "CICONIA_PUBLIC_LIST_TITLE_MAIN": "Форма бял щъркел", "CICONIA_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на бял щъркел", "PUBLIC_LIST_FILTER_USER": "Наблюдател", @@ -749,7 +744,7 @@ "DASHBOARD_CARD_PLANTS": "Защитени растения", "FORM_PLANTS_LONG": "Защитени растения", "FORM_PLANTS_SHORT": "Защитени растения", - "PLANTS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "PLANTS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "PLANTS_LIST_BUTTON_DELETE": "Изтриване", "PLANTS_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", "PLANTS_LIST_TITLE_MAIN": "Форма Защитени растения", @@ -778,7 +773,6 @@ "PLANTS_DETAIL_UNSAVED_DATA_ALERT_TITLE": "Имате незаписани промени!", "PLANTS_DETAIL_UNSAVED_DATA_ALERT_MESSAGE": "Имате незаписани промени. Сигурни ли сте че искате да продължите?", "PLANTS_DETAIL_SPECIES": "Вид научно име", - "PLANTS_DETAIL_CONFIDENTIAL": "Поверително", "PLANTS_DETAIL_ELEVATION": "Надморска височина", "PLANTS_DETAIL_HABITAT": "Местообитание", "PLANTS_DETAIL_REPORTING_UNIT": "Отчетна единица", @@ -789,7 +783,7 @@ "PLANTS_DETAIL_ACCOMPANYING_SPECIES": "Съпътстващи видове", "PUBLIC_PLANTS_TITLE": "Форма защитени растения", "PUBLIC_PLANTS_SUBTITLE": "Наблюдения на защитени растения", - "PUBLIC_PLANTS_TEXT": "Защитените растения са част от Smartbirds.org", + "PUBLIC_PLANTS_TEXT": "Тази част от www.SmartBirds.org е осъществена в партньорство с Националния природонаучен музей при БАН и с финансовата подкрепа на Програма LIFE на Европейския съюз.", "PUBLIC_PLANTS_MAP_SPECIES": "видове", "PUBLIC_PLANTS_MAP_INDIVIDUALS": "индивиди", "LOGO_NMNHS_ALT": "НПМ-БАН", @@ -841,8 +835,8 @@ "USER_LIST_TABLE_GDPR_CONSENT": "GDPR", "USER_DETAIL_GDPR_CONSENT_GRANTED": "Разрешено", "USER_DETAIL_GDPR_CONSENT_NOT_GRANTED": "Неразрешено", - "CAMPAIGN1_HEADER": "Отброяваме заедно до милионния запис!", - "CAMPAIGN1_SUBHEADER": "Спечели таблет! Направи милионния запис!", + "CAMPAIGN1_HEADER": "Отброяваме заедно до двумилионния запис!", + "CAMPAIGN1_SUBHEADER": "Спечели ваучер за магазини Стената! Направи двумилионния запис!", "CAMPAIGN1_BUTTON": "РЕГИСТРИРАЙ СЕ!", "CAMPAIGN1_TEXT": "Записвай данни!", "PUBLIC_HERPTILES_TOP_OBSERVERS_RECORDS_MONTH": "Най-много въведени наблюдения за последните 30 дни", @@ -855,7 +849,7 @@ "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", - "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Място", + "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", "PUBLIC_MAMMALS_TOP_OBSERVERS_RECORDS_MONTH": "Най-много въведени наблюдения за последните 30 дни", @@ -868,7 +862,7 @@ "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", - "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Място", + "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", "PUBLIC_INVERTEBRATES_TOP_OBSERVERS_RECORDS_MONTH": "Най-много въведени наблюдения за последните 30 дни", @@ -881,7 +875,7 @@ "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", - "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Място", + "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", "PUBLIC_PLANTS_TOP_OBSERVERS_RECORDS_MONTH": "Най-много въведени наблюдения за последните 30 дни", @@ -894,7 +888,7 @@ "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", - "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Място", + "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", "PUBLIC_STATISTIC_TABLE_COL_COUNT_SPECIES": "{{count}} вида", @@ -908,7 +902,7 @@ "STATS_MAMMALS_SUBTITLE": "Статистики за наблюденията", "STATS_PLANTS_TITLE": "Защитени растения", "STATS_PLANTS_SUBTITLE": "Статистики за наблюденията", - "STATS_INVERTEBRATES_TITLE": "Защитени безгръбначни", + "STATS_INVERTEBRATES_TITLE": "Безгръбначни животни", "STATS_INVERTEBRATES_SUBTITLE": "Статистики за наблюденията", "STATS_INTERESTING_SPECIES_TITLE_TAB": "Интересни видове", "STATS_TOP_SPECIES_MONTH_TITLE_TAB": "Най-често наблюдавани видове", @@ -942,7 +936,7 @@ "DASHBOARD_CARD_THREATS": "Заплахи", "FORM_THREATS_LONG": "Заплахи", "FORM_THREATS_SHORT": "Заплахи", - "THREATS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} записа", + "THREATS_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", "THREATS_LIST_BUTTON_DELETE": "Изтриване", "THREATS_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", "THREATS_LIST_TITLE_MAIN": "Форма Заплахи", @@ -1004,6 +998,363 @@ "THREATS_LIST_FILTER_CATEGORY": "Категория", "PUBLIC_THREATS_TITLE": "Форма заплахи", "PUBLIC_THREATS_SUBTITLE": "Наблюдения на заплахи", - "PUBLIC_THREATS_TEXT": "Заплахите са част от Smartbirds.org", - "FORM_LABEL_THREATS": "Заплахи" -} \ No newline at end of file + "PUBLIC_THREATS_TEXT": "Тази част от SmartBirds.org е осъществена в рамките на проект {{linkStart}}„Нова Надежда За Египетския Лешояд”{{linkEnd}} (LIFE16 NAT/BG/000874), финансиран от програма LIFE+ на Европейския съюз и ко-финасиран от фондация „Левентис”.", + "FORM_LABEL_THREATS": "Заплахи", + "LOGO_LIFENEOPHRON_ALT": "Програма Life+", + "LOGO_BIRDLIFE_ALT": "Birdlife International", + "LOGO_LEVENTIS_ALT": "Фонадация \"Левентис\"", + "LANGUAGE_ALBANIAN": "Албански", + "LANGUAGE_MACEDONIAN": "Македонски", + "PUBLIC_THREATS_FILTER_BY_THREAT": "Заплаха:", + "PUBLIC_THREATS_FILTER_ALL_THREATS": "- всички заплахи -", + "PUBLIC_THREATS_FILTER_BY_DATE": "Период:", + "PUBLIC_THREATS_FILTER_ALL_TIME": "- за целия период -", + "PUBLIC_THREATS_FILTER_1_MONTH": "последния месец", + "PUBLIC_THREATS_FILTER_3_MONTHS": "последните 3 месеца", + "PUBLIC_THREATS_FILTER_6_MONTHS": "последните 6 месеца", + "PUBLIC_THREATS_FILTER_1_YEAR": "последната година", + "NAV_ORGANIZATIONS": "Организации", + "ORGANIZATIONS_TITLE": "Организации", + "ORGANIZATIONS_BUTTON_SAVE": "Запис", + "CHANGE_ORGANIZATION_WARNING_MESSAGE": "Промяната на организация ще смени ролята на наблюдател", + "LIST_TABLE_ORGANIZATION": "Организация", + "LIST_FILTER_ORGANIZATION": "Организация", + "USER_DETAIL_ORGANIZATION": "Организация", + "LIST_FILTER_SETTLEMENT": "Селище", + "LIST_TABLE_SETTLEMENT": "Селище", + "DASHBOARD_CARD_RETRY": "Повторен опит", + "DASHBOARD_CARD_MODERATOR_REVIEW": "за преглед", + "FIELD_MODERATOR_REVIEW": "За проверка от модератор", + "FIELD_MODERATOR_REVIEW_HELP": "Отбележете ако желаете записа да бъде прегледан от модератор. Необходима е снимка!", + "LIST_FILTER_MODERATOR_REVIEW": "За проверка от модератор", + "FIELD_CONFIDENTIAL": "Поверително", + "FORM_DETAIL_BUTTON_CONFIRM": "Потвърждаване", + "SETTLEMENT_VALUE_UNKNOWN": "Неизвестно", + "SETTLEMENT_VALUE_PENDING": "Обработва се…", + "NAV_ATLAS": "Атлас", + "NAV_ATLAS_DASHBOARD": "Преглед", + "NAV_ATLAS_REQUEST": "Избор на квадрати", + "ATLAS_DASHBOARD_TITLE": "Преглед данните по квадрати", + "ATLAS_REQUEST_TITLE": "Избор на квадрати", + "ATLAS_REQUEST_SUBTITLE": "Изберете квадратите, които искате да проучите. По възможност, предпочетете по-слабо проучени. Може да изберете до 10 квадрата.", + "ATLAS_REQUEST_BTN_SAVE": "Запис", + "ATLAS_REQUEST_ZONES_HEADING": "Избрани квадрати. За всеки избран квадрат е посочено колко процента от видовете установени в рамките на предния атлас (Янков, 2007) са установени след 2016г.", + "ATLAS_REQUEST_ZONES_TEXT": "Избрани квадрати (10 X 10 km) за атласно проучване.", + "ATLAS_LEGEND_LOW": "Слабо проучен", + "ATLAS_LEGEND_MEDIUM": "Средно проучен", + "ATLAS_LEGEND_HIGH": "Силно проучен", + "ATLAS_REQUEST_ZONES_HINT": "Изберете желаните от вас квадрати.", + "ATLAS_DASHBOARD_ZONE_HEADING": "Информация за квадрат", + "ATLAS_DASHBOARD_ZONE_HEADER_SPECIES": "Вид", + "ATLAS_DASHBOARD_ZONE_HEADER_ATLAS": "Атлас 2007", + "ATLAS_DASHBOARD_ZONE_HEADER_OBSERVATION": "Лични наблюдения", + "ATLAS_DASHBOARD_ZONE_TEXT": "Съпоставка на видове установени в квадрата през предишен период според Атласа на гнездящите птици в България (Янков, 2007), видовете установени от потребителя и всички видове установени в квадрата след 01.01.2016 г.", + "ATLAS_DASHBOARD_ZONE_NO_ROWS": "Няма известни видове птици нито в атласа нито в наблюденията на потребителя", + "PUBLIC_ATLAS_TITLE": "Атлас на птиците", + "PUBLIC_ATLAS_SUBTITLE": "Степен на проученост след 01.01.2016 г. (UTM грид 10 x 10 km)", + "PUBLIC_ATLAS_PAGE_TITLE": "Атлас на птиците", + "ATLAS_DASHBOARD_ZONE_HEADER_OTHER": "Всички наблюдения", + "PUBLIC_ATLAS_TEXT": "Този модул е разработен в рамките на проект „Наука на гражданите в полза на местните общности и природата“ с финансова подкрепа на {{linkStart}} фонд Активни граждани {{linkEnd}} по линия на Финансовия механизъм на ЕИП.“", + "PUBLIC_ATLAS_LONG": "Атлас на гнездящите птици в България", + "PUBLIC_ATLAS_SHORT": "Атлас", + "ATLAS_STATS_TOP_OBSERVERS_HEADING": "Изберете квадрат от картата", + "ATLAS_STATS_TOP_OBSERVERS_ROW_VALUE": "{{ species }} вида", + "ATLAS_STATS_TOP_OBSERVERS_TEXT": "Общ брой видове: {{ species }} вида", + "ATLAS_STATS_TOP_OBSERVERS_HINT": "Изберете квадрата от картата за да видите информация за него", + "ATLAS_STATS_TITLE": "Атлас статистики", + "ATLAS_STATS_SUBTITLE": "Наблюдатели въвели данни за най-голям брой УТМ (10х10 km) квадрати", + "ATLAS_STATS_TOP_OBSERVERS": "Най-много наблюдения на квадрат", + "ATLAS_STATS_TOP_OBSERVERS_TITLE_TAB": "Наблюдатели по квадрати", + "ATLAS_STATS_RANKING_LABEL_COUNT": "{{ count }} квадрата", + "ATLAS_STATS_RANKING_TITLE_TAB": "Най-много квадрати", + "ATLAS_STATS_RANKING": "Най-много посетени квадрати от наблюдател", + "ATLAS_MISSING_SPECIES_TITLE": "Неустановени видове по квадрати", + "NAV_ATLAS_MISSING_SPECIES": "Неустановени видове", + "ATLAS_MISSING_SPECIES_ZONE_HEADING": "Информация за неустановени видове", + "ATLAS_MISSING_SPECIES_ZONE_TEXT": "Изберете квадрат от картата за да видите неустановените видове", + "ATLAS_MISSING_SPECIES_ZONE_HEADER_SPECIES": "Вид", + "ATLAS_MISSING_SPECIES_ZONE_NO_ROWS": "Липсват неустановени видове за този квадрат", + "FORM_ATLAS_SHORT": "Атлас Птици", + "LANGUAGE_GREEK": "Гръцки", + "LANGUAGE_TURKISH": "Турски", + "LANGUAGE_ARABIC": "Арабски", + "LANGUAGE_FRENCH": "Френски", + "PUBLIC_HOME_FUNDING_LIST": "
  • {{linkLIFEStart}}Програма LIFE на Европейския съюз{{linkEnd}}\n
  • {{linkUS4BGStart}}Фондация Америка за България{{linkEnd}}\n
  • Програма Мтел еко грант\n
  • {{linkEEAGrantsStart}}Програма BG03 в България по Финансовия механизъм на Европейското икономическо пространство 2009–2014 г.{{linkEnd}}\n
  • {{linkPUDOOSStart}}Предприятие за управление на дейностите по опазване на околната среда{{linkEnd}}\n
  • {{linkLeventisStart}}Фондация A. G. Leventis{{linkEnd}}\n
  • {{linkEurNaturStart}}Фондация EuroNatur, Германия.{{linkEnd}}\n
  • {{linkActiveCitizensFundStart}}Фонд Активни граждани {{linkEnd}}по линия на Финансовия механизъм на ЕИП.", + "LOGO_ACTIVECITIZENSFUND_ALT": "Фонд Активни граждани", + "LOGO_PUDOOS_ALT": "Предприятие за управление на дейностите по опазване на околната среда", + "FIELD_OBSERVATION_METHODOLOGY": "Методика на наблюдение", + "NAV_ATLAS_INTEREST_MAP": "Степен на интерес", + "ATLAS_INTEREST_MAP_TITLE": "Карта степен на интерес", + "ATLAS_INTEREST_MAP_NONE": "Свободен квадрат (0)", + "ATLAS_INTEREST_MAP_LOW": "Заявен (1)", + "ATLAS_INTEREST_MAP_MEDIUM": "Среден интерес (2)", + "ATLAS_INTEREST_MAP_HIGH": "Висок интерес (3+)", + "NAV_ATLAS_MODERATOR_PROGRESS": "Проученост на квадрати (за модератори)", + "ATLAS_MODERATOR_PROGRESS_TITLE": "Проученост на квадрати", + "ATLAS_MODERATOR_PROGRESS_SUBTITLE": "Установени видове спрамо стария атлас", + "ATLAS_LEGEND_VHIGH": "Добре проучен", + "ATLAS_MODERATOR_PROGRESS_ZONE_HEADING": "Избрани квадрати", + "ATLAS_MODERATOR_PROGRESS_ZONE_TEXT": "Статус на кваратите на база \"Наблюдател\" и \"Методика\"", + "ATLAS_MODERATOR_PROGRESS_ZONE_COMPLETE": "Завършен квадрат", + "ATLAS_MODERATOR_PROGRESS_ZONE_UNCOMPLETE": "Отворен квадрат", + "ATLAS_MODERATOR_PROGRESS_ZONE_COMPLETE_CONFIRM": "Моля потвърдете завършването на квадрата. Тази операция ще премахне квадрата от всички потребители. Опреацията може да бъде върната само от Администратора.", + "ATLAS_MODERATOR_PROGRESS_ZONE_UNCOMPLETE_CONFIRM": "Моля потвърдете за повторно отваряне на квадрата", + "ATLAS_LEGEND_COMPLETED": "Приключен", + "ATLAS_NEW_SPECIES_TITLE": "Нови видове по квадрати", + "NAV_ATLAS_NEW_SPECIES": "Нови видове", + "ATLAS_NEW_SPECIES_ZONE_HEADING": "Информация за нови видове", + "ATLAS_NEW_SPECIES_ZONE_TEXT": "Изберете квадрат от картата за да видите новите видове", + "ATLAS_NEW_SPECIES_ZONE_HEADER_SPECIES": "Вид", + "ATLAS_NEW_SPECIES_ZONE_NO_ROWS": "Липсват нови видове за този квадрат", + "FORM_LABEL_PYLONS": "Проучване електропроводи", + "FORM_PYLONS_LONG": "Проучване електропроводи", + "FORM_PYLONS_SHORT": "Проучване електропроводи", + "FORM_LABEL_PYLONS_CASUALTIES": "Жертви електропроводи", + "FORM_PYLONS_CASUALTIES_LONG": "Жертви електропроводи", + "FORM_PYLONS_CASUALTIES_SHORT": "Жертви електропроводи", + "PYLONS_LIST_TITLE_MAIN": "Проучване електропроводи", + "PYLONS_LIST_TITLE_SECONDARY": "Форма за проучване на електропроводи", + "MONITORING_LIST_FILTER_USER": "Наблюдател", + "MONITORING_LIST_FILTER_FROM_DATE": "От", + "MONITORING_LIST_FILTER_TO_DATE": "До", + "MONITORING_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Потвърдете изтриването на {{count}} запис(а)", + "MONITORING_LIST_BUTTON_DELETE": "Изтриване", + "MONITORING_LIST_BUTTON_NEW_ENTRY": "Ново наблюдение", + "MONITORING_LIST_FILTER_SPECIES": "Вид", + "MONITORING_LIST_TAB_LIST": "Списък", + "MONITORING_LIST_TAB_MAP": "Карта", + "MONITORING_LIST_MAP_BUTTON_SHOW_ALL": "Покажи всички", + "MONITORING_LIST_MAP_SHOWN_RECORDS_WARNING": "Показани са само последните {{count}} записа!", + "MONITORING_LIST_TABLE_USER": "Наблюдател", + "MONITORING_LIST_TABLE_DATE": "Дата/час", + "MONITORING_LIST_TABLE_UPDATED": "Променен", + "MONITORING_LIST_TABLE_SPECIES": "Вид", + "MONITORING_LIST_TABLE_COUNT": "Брой", + "PYLONS_CASUALTIES_LIST_TITLE_SECONDARY": "Форма за проучване на жертвите от елелтропреносната мрежа", + "PYLONS_CASUALTIES_LIST_TITLE_MAIN": "Жертви електропроводи", + "MONITORING_DETAIL_UNSAVED_DATA_ALERT_TITLE": "Имате незаписани промени!", + "MONITORING_DETAIL_UNSAVED_DATA_ALERT_MESSAGE": "Имате незаписани промени. Сигурни ли сте че искате да продължите?", + "MONITORING_DETAIL_TITLE_NEW": "Ново наблюдение", + "MONITORING_DETAIL_TITLE_EDIT": "Редакция на запис", + "FIELD_SPECIES_HELP_PROTECTED_SPECIES": "Видовете отбелязани с * са „защитени“", + "PYLONS_CASUALTIES_DETAIL_COUNT": "Брой", + "PYLONS_CASUALTIES_DETAIL_AGE": "Възраст", + "PYLONS_CASUALTIES_DETAIL_SEX": "Пол", + "PYLONS_CASUALTIES_DETAIL_CAUSE_OF_DEATH": "Причина за смъртта", + "PYLONS_CASUALTIES_DETAIL_BODY_CONDITION": "Състояние на жертвата", + "PYLONS_DETAIL_HABITAT_100M_PRIME": "Основно местообитание радиус 100 m", + "PYLONS_DETAIL_HABITAT_100M_SECOND": "Второстепенно местообитание радиус 100 m", + "PYLONS_FIELD_PYLON_TYPE": "Тип на стълба", + "PYLONS_FIELD_SPECIES_NEST": "Наличие на гнездо", + "PYLONS_FIELD_TYPE_NEST": "Тип на гнездо", + "PYLONS_FIELD_INSULATED": "Стълбът е обезопасен", + "PYLONS_LIST_TYPE": "Тип на стълба", + "PYLONS_CASUALTIES_LIST_CAUSE_OF_DEATH": "Причина за смъртта", + "PYLONS_PUBLIC_LIST_TITLE_MAIN": "Проучване електропроводи", + "PYLONS_PUBLIC_LIST_TITLE_SECONDARY": "Форма за проучване на електропроводи", + "PYLONS_CASUALTIES_PUBLIC_LIST_TITLE_MAIN": "Жертви електропроводи", + "PYLONS_CASUALTIES_PUBLIC_LIST_TITLE_SECONDARY": "Форма за проучване на жертвите от елелтропреносната мрежа", + "PYLONS_NEST_SPECIES": "Наличие на гнездо", + "PYLONS_FIELD_DAMAGED_INSULATION": "Увредена изолация", + "NAV_DAILY_REPORT": "Дневен отчет", + "DAILY_REPORT_FILTER_DATE": "Дата", + "DAILY_REPORT_TITLE_MAIN": "Дневен отчет", + "DAILY_REPORT_TITLE_SECONDARY": "Обобщен отчет на записите от избран ден", + "DAILY_REPORT_SUBMIT": "Зареди отчет", + "DAILY_REPORT_LIST_TABLE_FORM": "Форма", + "DAILY_REPORT_LIST_TABLE_LOCATION": "Селище", + "DAILY_REPORT_LIST_TABLE_SPECIES": "Вид", + "DAILY_REPORT_LIST_TABLE_COUNT": "Брой", + "DAILY_REPORT_LIST_TABLE_SPECIES_LATIN": "Латинско име", + "FORM_LABEL_BIRDS_MIGRATIONS": "Форма Миграция птици", + "FORM_BIRDS_MIGRATIONS_LONG": "Наблюдения на мигриращи реещи се птици от точка", + "FORM_BIRDS_MIGRATIONS_SHORT": "Форма Миграция птици", + "BIRDS_MIGRATIONS_LIST_TITLE_MAIN": "Форма Миграция птици", + "BIRDS_MIGRATIONS_LIST_TITLE_SECONDARY": "Наблюдения на мигриращи реещи се птици от точка", + "MONITORING_LIST_BIRDS_MIGRATION_POINT": "Точка на наблюдение", + "LIST_FILTER_BIRDS_MIGRATION_POINT": "Списък с точки на наблюдение", + "BIRDS_MIGRATIONS_DETAIL_MIGRATION_POINT": "Точка на наблюдение", + "BIRDS_MIGRATIONS_DETAIL_LOCATION_FROM_MIGRATION_POINT": "Посока от точката на миграция", + "BIRDS_MIGRATIONS_DETAIL_SEX": "Пол", + "BIRDS_MIGRATIONS_DETAIL_PLUMAGE": "Форма", + "BIRDS_MIGRATIONS_DETAIL_AGE": "Възраст", + "BIRDS_MIGRATIONS_DETAIL_VISOCHINA_POLET": "Височина на полет, m", + "BIRDS_MIGRATIONS_DETAIL_POSOKA_POLET_FROM": "Посока от", + "BIRDS_MIGRATIONS_DETAIL_POSOKA_POLET_TO": "Посока към", + "BIRDS_MIGRATIONS_DETAIL_TYPE_FLIGHT": "Тип на полета", + "DETAIL_DISTANCE_FROM_MIGRATION_POINT": "Отстояние", + "FORM_LABEL_FISHES": "Форма Риби", + "FORM_FISHES_LONG": "Форма Риби", + "FORM_FISHES_SHORT": "Форма Риби", + "FISHES_LIST_TITLE_MAIN": "Форма Риби", + "FISHES_LIST_TITLE_SECONDARY": "Наблюдения на риби", + "FISHES_DETAIL_SEX": "Пол", + "FISHES_DETAIL_NAME_WATER_BODY": "Водоем", + "FISHES_DETAIL_AGE": "Възраст", + "FISHES_DETAIL_SIZE_TL_MM": "Обща дължина на тялото (TL), mm", + "FISHES_DETAIL_SIZE_SL_MM": "Стандартна дължина на тялото (SL), mm", + "FISHES_DETAIL_MASA_GR": "Тегло, gr", + "FISHES_DETAIL_FINDINGS": "Mетод на отчитане", + "FISHES_DETAIL_MONITORING_TYPE": "Tип проучване", + "FISHES_DETAIL_TRANSECT_LENGTH_M": "Дължина на трансекта, m", + "FISHES_DETAIL_TRANSECT_WIDTH_M": "Ширина на трансекта, m", + "FISHES_DETAIL_FISHING_AREA_M": "обща риболовна площ, кв. m", + "FISHES_DETAIL_EXPOSITION": "Експозиция", + "FISHES_DETAIL_MESH_SIZE": "Големина на окото", + "FISHES_DETAIL_COUNT_NET_TRAP": "Брой мрежи/винтери", + "FISHES_DETAIL_WATER_TEMP": "Температура на водата", + "FISHES_DETAIL_CONDUCTIVITY": "Електропроводимост", + "FISHES_DETAIL_PH": "pH", + "FISHES_DETAIL_O2MG_L": "O2 mg/l", + "FISHES_DETAIL_O2PERCENT": "O2 %", + "FISHES_DETAIL_SALINITY": "Соленост", + "FISHES_DETAIL_HABITAT_DESCRIPTION_TYPE": "Тип водоем", + "FISHES_DETAIL_SUBSTRATE_MUD": "Тиня, %", + "FISHES_DETAIL_SUBSTRATE_SILT": "Ситен пясък - до 2 mm, %", + "FISHES_DETAIL_SUBSTRATE_SAND": "Едър пясък - 2-5 mm, %", + "FISHES_DETAIL_SUBSTRATE_GRAVEL": "Чакъл - 0.5 - 4 cm, %", + "FISHES_DETAIL_SUBSTRATE_SMALL_STONES": "Дребни камъни - 4-10 cm, %", + "FISHES_DETAIL_SUBSTRATE_COBBLE": "Едри камъни - 10-25 cm, %", + "FISHES_DETAIL_SUBSTRATE_BOULDER": "Валчести камъни - 25-50 cm, %", + "FISHES_DETAIL_SUBSTRATE_ROCK": "Скално легло, %", + "FISHES_DETAIL_SUBSTRATE_OTHER": "Друго, %", + "FISHES_DETAIL_WATER_LEVEL": "Водно ниво", + "FISHES_DETAIL_RIVER_CURRENT": "Скорост на течение", + "FISHES_DETAIL_TRANSECT_AV_DEPTH": "Средна дълбочина", + "FISHES_DETAIL_TRANSECT_MAX_DEPTH": "Максимална дълбочина", + "FISHES_DETAIL_SLOPE": "Бряг", + "FISHES_DETAIL_BANK_TYPE": "Вид на брега", + "FISHES_DETAIL_SHADING": "Засенченост", + "FISHES_DETAIL_RIPARIAN_VEGETATION": "Крайбрежна растителност", + "FISHES_DETAIL_SHELTERS": "Естествени укрития", + "FISHES_DETAIL_TRANSPARENCY": "Прозрачност на водата", + "FISHES_DETAIL_VEGETATION_TYPE": "Водна растителност", + "FISHES_DETAIL_NATURAL_BARRIERS": "Наличие на естествени препядствия", + "CLASS_FISHES": "Риби", + "PUBLIC_FISHES_TEXT": "Тази част от SmartBirds.org е осъществена с финансовата подкрепа на Програма LIFE на Европейския съюз.", + "PUBLIC_FISHES_TITLE": "Форма Риби", + "PUBLIC_FISHES_SUBTITLE": "Наблюдения на риби", + "PUBLIC_MAP_SPECIES": "видове", + "PUBLIC_MAP_INDIVIDUALS": "индивиди", + "USER_DETAIL_ALLOW_DATA_MOSV": "Съгласен съм събраните от мен данни да бъдат предоставени на МОСВ", + "USER_DETAIL_ALLOW_DATA_SCIENCE_PUBLICATIONS": "Съгласен съм събраните от мен данни да бъдат публикувани в научна статия", + "NAV_BIRDS_MIGRATIONS": "Форма Миграция птици", + "STATS_BIRDS_MIGRATIONS_SEASON_TOTALS_TITLE_TAB": "Общ брой мигриращи птици за всеки сезон", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_TOTALS": "Общ брой мигриращи птици за всеки сезон - есенен (10 Август - 30 Октомври) и пролетен (1 март до 31 май)", + "STATS_BIRDS_MIGRATIONS_PEAK_DAILY_SPECIES_TITLE_TAB": "Пикови дневни числености на мигриращи птици", + "PUBLIC_BIRDS_MIGRATIONS_PEAK_DAILY_SPECIES": "Максимални (пикови) дневни числености на мигриращи птици (с избор на дата)", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", + "PUBLIC_BIRDS_MIGRATIONS_COL_MIGRATION_POINT": "Точка на наблюдение", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_FALL": "есен", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_SPRING": "пролет", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_DATE": "Дата", + "PUBLIC_BIRDS_MIGRATIONS_PEAK_NO_RECORDS": "Липсват записи за избраната дата", + "PUBLIC_BIRDS_MIGRATIONS_TITLE": "Форма Миграция птици", + "PUBLIC_BIRDS_MIGRATIONS_SUBTITLE": "Наблюдения на мигриращи реещи се птици от точка", + "PUBLIC_BIRDS_MIGRATIONS_TEXT": "Тази част от www.SmartBirds.org е осъществена в партньорство с Националния природонаучен музей при БАН и с финансовата подкрепа на Програма LIFE на Европейския съюз.", + "PUBLIC_BIRDS_MIGRATIONS_TOP_SPECIES_MONTH": "Най-често наблюдавани видове за последните 30 дни", + "FISHES_PUBLIC_LIST_TITLE_MAIN": "Форма Риби", + "FISHES_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на риби", + "BIRDS_MIGRATIONS_PUBLIC_LIST_TITLE_MAIN": "Форма Миграция птици", + "BIRDS_MIGRATIONS_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на мигриращи реещи се птици от точка", + "STATS_BIRDS_MIGRATIONS_TITLE": "Миграция на птици", + "STATS_BIRDS_MIGRATIONS_SUBTITLE": "Статистики за наблюденията", + "STATS_FISHES_TITLE": "Форма Риби", + "STATS_FISHES_SUBTITLE": "Статистики за наблюденията", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Вид", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_DATE": "Дата", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Селище", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_COUNT": "Брой", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Наблюдател", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES": "Наблюдавани интересни видове за последните 30 дни", + "PUBLIC_FISHES_TOP_SPECIES_MONTH": "Най-често наблюдавани видове за последните 30 дни", + "PUBLIC_FISHES_TOP_OBSERVERS_RECORDS_YEAR": "Най-много въведени наблюдения за календарна година", + "PUBLIC_FISHES_TOP_OBSERVERS_SPECIES_YEAR": "Най-много наблюдавани видове за календарна година", + "PUBLIC_STATISTIC_TABLE_COL_SUM_COUNT": "{{count}} индивида", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_MIGRATION_POINT": "Точка на наблюдение", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_NO_RECORDS": "Липсват записи за избраната миграционна точка", + "PUBLIC_NO_RECORDS": "Липсват записи", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_MIGRATION_POINT_PLACEHOLDER": "Всички", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_SPECIES": "Вид", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_SPECIES_PLACEHOLDER": "Избери вид", + "FORM_LIST_BUTTON_FULL_CSV_EXPORT_TOOLTIP": "Генериране на csv файл с всички намерени записи и всички полета", + "NAV_MAP_LAYERS": "Слоеве", + "MAP_LAYERS_TITLE": "Слоеве", + "MAP_LAYERS_UNSAVED_DATA_TITLE": "Имате незаписани промени!", + "MAP_LAYERS_UNSAVED_DATA_MESSAGE": "Имате незаписани промени. Сигурни ли сте, че искате да продължите?", + "MAP_LAYERS_COL_LABEL": "Име", + "MAP_LAYERS_COL_SUMMARY": "Описание", + "MAP_LAYERS_COL_URL": "Линк", + "MAP_LAYERS_ROW_WIDTH": "Ширина (px)", + "MAP_LAYERS_COL_WIDTH": "Височина (px)", + "MAP_LAYERS_BUTTON_ADD": "Добавяне", + "MAP_LAYERS_BUTTON_SAVE": "Запис", + "FORM_LIST_BUTTON_FULL_ZIP_EXPORT_TOOLTIP": "Генериране на zip файл с всички намерени записи и всички полета, техните снимки и gpx файлове", + "CLASS_BATS": "Прилепи", + "FORM_LABEL_BATS": "Форма Прилепи", + "FORM_BATS_LONG": "Форма Прилепи", + "FORM_BATS_SHORT": "Форма Прилепи", + "BATS_LIST_TITLE_MAIN": "Форма Прилепи", + "BATS_LIST_TITLE_SECONDARY": "Наблюдения на прилепи", + "BATS_DETAIL_METODOLOGY": "Метод", + "BATS_DETAIL_T_CAVE": "T°C на убежището", + "BATS_DETAIL_H_CAVE": "H - Влажност на убежището (%)", + "BATS_DETAIL_TYPE_LOCATION": "Тип на мястото", + "BATS_DETAIL_SUBLOCALITY": "Описание на мястото", + "BATS_DETAIL_SWARMING": "Суорминг", + "BATS_DETAIL_AGE": "Възраст", + "BATS_DETAIL_SEX": "Пол", + "BATS_DETAIL_HABITAT": "Местообитание", + "BATS_DETAIL_CONDITION": "Състояние", + "BATS_DETAIL_TYPE_CONDITION": "Тип на броя", + "BATS_DETAIL_REPRODUCTIVE_STATUS": "Репродуктивен статус", + "BATS_DETAIL_RING": "Пръстен", + "BATS_DETAIL_RING_N": "Номер на пръстена", + "BATS_DETAIL_BODY_LENGTH": "L - дължина на тялото (mm)", + "BATS_DETAIL_TAIL_LENGTH": "C - дължина на опашката (mm)", + "BATS_DETAIL_EAR_LENGTH": "А - дължина на ухото (mm)", + "BATS_DETAIL_FOREARM_LENGTH": "FA - Дължина на совалката (mm)", + "BATS_DETAIL_LENGTH_THIRD_DIGIT": "D3 - Дължина на трети пръст +китката (mm)", + "BATS_DETAIL_LENGTH_FIFTH_DIGIT": "D5 - Дължина на пети пръст +китката (mm)", + "BATS_DETAIL_LENGTH_WS": "WS - Дължина на горен зъбен ред (mm)", + "BATS_DETAIL_WEIGHT": "Тегло (g)", + "BATS_DETAIL_SWARMING_HELP": " ", + "BATS_DETAIL_AGE_HELP": "Възраст на индивидите", + "BATS_DETAIL_SEX_HELP": "Пол на индивидите", + "BATS_DETAIL_CONDITION_HELP": "Състояние", + "BATS_DETAIL_TRAGUS": "Tr - Трагус", + "BATS_DETAIL_UPPER_MOLAR": "CM3 - Горен зъбен ред", + "BATS_PUBLIC_LIST_TITLE_MAIN": "Форма Прилепи", + "BATS_PUBLIC_LIST_TITLE_SECONDARY": "Наблюдения на прилепи", + "IMPORT_RECORDS_MODAL_TITLE": "Импортитане от файл", + "IMPORT_RECORDS_CHOOSE_FILE": "Изберете файл", + "IMPORT_RECORDS_READING_FILE": "Зареждане на файла...", + "IMPORT_RECORDS_LANGUAGE": "Език", + "BTN_IMPORT": "Импортирай", + "FORM_LIST_BUTTON_CSV_IMPORT_TOOLTIP": "Импорт от CSV файл", + "FISHES_DETAIL_TOTAL_LENGTH": "Обща дължина на тялото (TL) в категории, cm", + "IMPORT_RECORDS_IGNORE_ERRORS": "Игнорирай грешките", + "You will be notified by email when your import is ready": "Ще бъдете уведомени с емайл, когато импортът Ви е готов.", + "EBP_SETTINGS_BUTTON_SAVE": "Запис", + "EBP_SETTINGS_BUTTON_ADD": "Добави", + "EBP_SETTINGS_TITLE": "EBP настройки", + "EBP_SETTINGS_ORGANIZATIONS": "Организации", + "EBP_SETTINGS_SOURCES": "Източници", + "EBP_SETTINGS_PROTOCOL": "Протокол", + "EBP_SETTINGS_SPECIES": "Видове", + "EBP_SETTINGS_SPECIES_STATUS": "Статус на вид", + "EBP_SETTINGS_NO_PERMISSION": "Нямате достъп до тази страница", + "EBP_SETTINGS_ORGANIZATION": "Организация", + "EBP_SETTINGS_ORGANIZATION_ENABLED": "Позволена", + "EBP_SETTINGS_SOURCE": "Източник", + "EBP_SETTINGS_SOURCE_ENABLED": "Позволен", + "EBP_SETTINGS_SPECIES_CODE": "EBP код", + "EBP_SETTINGS_SPECIES_SB_NAME": "Име в smartbirds", + "EBP_SETTINGS_SPECIES_EBP_NAME": "Име в EBP", + "EBP_SETTINGS_SPECIES_STATUS_CODE": "EBP Код", + "EBP_SETTINGS_SPECIES_STATUS_SB_NAME": "Име в smartbirds", + "NAV_EBP": "EBP" +} diff --git a/i18n/en.json b/i18n/en.json index 5ed6653f2..7ebbf3d8a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -41,17 +41,17 @@ "PUBLIC_HOME_ABOUT_TITLE": "About the project", "PUBLIC_HOME_ABOUT_BODY": "SmartBirds.org is an electronic database developed by {{bspbLinkStart}}the Bulgarian Society for the Protection of Birds{{bspbLinkEnd}}. Its purpose is to serve as an electronic notebook for the storage of records from observations of birds, mammals, amphibians, reptiles protected and other invertebrates. The recording of each observation supports the conservation activities in Bulgaria. So write your observations directly here on the page or through the mobile application {{mobileLinkStart}} SmartBirds Pro{{mobileLinkEnd}}. If you love nature and want to join the bird counting initiative around us, register with SmartBirds {{registerLinkStart}} here {{registerLinkEnd}} and choose your personal bird counting place.", "PUBLIC_HOME_FUNDING_TITLE": "Funding", - "PUBLIC_HOME_FUNDING_BODY1": "The Common Birds Monitoring form in the portal SmartBirds.org is financed through the project \"Birds for the people and people for the birds: with a good will to actively conserve nature\" within the {{linkStart}}Programs BG03 in Bulgaria{{linkEnd}} of the Financial Mechanism of the European Economic Area 2009-2014.", - "PUBLIC_HOME_FUNDING_BODY2": "The other modules of SmartBirds.org have been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "PUBLIC_HOME_FUNDING_BODY1": "Biological Information System (SmartBirds.org) and the {{linkSmartBirdsProStart}}SmartBirds Pro{{linkEnd}} and {{linkSmartBirdsStart}}SmartBirds{{linkEnd} mobile applications were developed and updated by BSPB with the financial support of:", + "PUBLIC_HOME_FUNDING_BODY2": "Some of the modules of SmartBirds.org and SmartBirds Pro have been developed in partnership with the {{linkNMNHSStart}}National Museum of Natural History at the Bulgarian Academy of Sciences{{linkEnd}}.\nThe BSPB Biological Information System is part of the {{linkEuroBirdPortalStart}}EuroBirdPortal{{linkEnd}} initiative of the {{linkEuroBirdCensusStart}}European Bird Census Council{{linkStart}}.", "PUBLIC_BIRDS_TITLE": "Form Birds", "PUBLIC_BIRDS_SUBTITLE": "Observations from Standard form Birds", - "PUBLIC_BIRDS_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "PUBLIC_BIRDS_TEXT": "This part of SmartBirds.org is implemented with the financial support of the LIFE Program of the European Union.", "PUBLIC_CBM_TITLE": "CBM", "PUBLIC_CBM_SUBTITLE": "Sites for monitoring of the Common Birds species", - "PUBLIC_CBM_TEXT": "The Common Birds Monitoring form in the portal SmartBirds.org is financed through the project \"Birds for the people and people for the birds: with a good will to actively conserve nature\" within the {{linkStart}}Programs BG03 in Bulgaria{{linkEnd}} of the Financial Mechanism of the European Economic Area 2009-2014.", + "PUBLIC_CBM_TEXT": "The Common Birds Monitoring form in the portal SmartBirds.org is financed through the project \"Birds for the people and people for the birds: with a good will to actively conserve nature\" within the {{linkStart}}Programs BG03 in Bulgaria{{linkEnd}} of the Financial Mechanism of the European Economic Area 2009-2014 and the project \"Implementation of the monitoring scheme for common bird species as part of the National Biodiversity Monitoring System\", funded by the EMEPA. ", "PUBLIC_CICONIA_TITLE": "Form White stork", "PUBLIC_CICONIA_SUBTITLE": "Observations of White stork", - "PUBLIC_CICONIA_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "PUBLIC_CICONIA_TEXT": "This part of www.SmartBirds.org is funded by the European Union's LIFE Program. The module is also supported within the project \"SmartStorkBelozem - development of civil science for storks in Belozem - European village of storks in Bulgaria\", funded by the EuroNatur Foundation, Germany.", "PUBLIC_HERP_TITLE": "Form ARM", "PUBLIC_HERP_SUBTITLE": "Observations of amphibians, reptiles, and mammals", "PUBLIC_HERP_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", @@ -220,7 +220,6 @@ "BIRDS_DETAIL_TITLE_NEW": "New observation", "BIRDS_DETAIL_TITLE_EDIT": "Edit record", "BIRDS_DETAIL_SPECIES": "Scientific name", - "BIRDS_DETAIL_CONFIDENTIAL": "Confidential", "BIRDS_DETAIL_LATITUDE": "Latitude", "BIRDS_DETAIL_LONGITUDE": "Longitude", "BIRDS_DETAIL_COUNT_UNIT": "Count unit", @@ -296,8 +295,8 @@ "HERP_DETAIL_TITLE_EDIT": "Edit record", "HERP_DETAIL_LATITUDE": "Latitude", "HERP_DETAIL_LONGITUDE": "Longitude", - "HERP_DETAIL_SPECIES": "Scientific name", - "HERP_DETAIL_COUNT": "Count", + "FIELD_SPECIES": "Scientific name", + "FIELD_COUNT": "Count", "HERP_DETAIL_AGE": "Age", "HERP_DETAIL_SEX": "Sex", "HERP_DETAIL_HABITAT": "Habitat", @@ -464,12 +463,12 @@ "FORM_MAMMALS_SHORT": "Mammals", "PUBLIC_HERPTILES_TITLE": "Form A&R", "PUBLIC_HERPTILES_SUBTITLE": "Observations of amphibians and reptiles", - "PUBLIC_HERPTILES_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "PUBLIC_HERPTILES_TEXT": "This part of SmartBirds.org is implemented with the financial support of the LIFE Program of the European Union.", "PUBLIC_HERPTILES_MAP_SPECIES": "species", "PUBLIC_HERPTILES_MAP_INDIVIDUALS": "individuals", "PUBLIC_MAMMALS_TITLE": "Form Mammals", "PUBLIC_MAMMALS_SUBTITLE": "Observations of mammals", - "PUBLIC_MAMMALS_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "PUBLIC_MAMMALS_TEXT": "This part of SmartBirds.org is implemented with the financial support of the LIFE Program of the European Union.", "PUBLIC_MAMMALS_MAP_SPECIES": "species", "PUBLIC_MAMMALS_MAP_INDIVIDUALS": "individuals", "DASHBOARD_CARD_HERPTILES": "A&R", @@ -625,17 +624,17 @@ "DOWNLOAD_TITLE": "Download data", "You will be notified by email when your export is ready": "You will be notified by email when your export is ready", "Error during export": "Error during export", - "FORM_INVERTEBRATES_LONG": "Protected invertebrates", - "FORM_INVERTEBRATES_SHORT": "Protected invertebrates", - "FORM_LABEL_INVERTEBRATES": "Protected invertebrates", - "DASHBOARD_CARD_INVERTEBRATES": "Protected invertebrates", - "PUBLIC_INVERTEBRATES_TITLE": "Form Protected invertebrates", - "PUBLIC_INVERTEBRATES_SUBTITLE": "Observations of protected invertebrates", - "PUBLIC_INVERTEBRATES_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"LIFE for Eagles Forests - Preserve Key Forest Habitats of the Lesser Spotted Eagle (Aquila pomarina) in Bulgaria\"{{linkEnd}} (LIFE12 NAT/BG/001218) financed by the LIFE + program The European Union.", + "FORM_INVERTEBRATES_LONG": "Invertebrates", + "FORM_INVERTEBRATES_SHORT": "Invertebrates", + "FORM_LABEL_INVERTEBRATES": "Invertebrates", + "DASHBOARD_CARD_INVERTEBRATES": "Invertebrates", + "PUBLIC_INVERTEBRATES_TITLE": "Form Invertebrates", + "PUBLIC_INVERTEBRATES_SUBTITLE": "Observations of invertebrates", + "PUBLIC_INVERTEBRATES_TEXT": "\nThis part of www.SmartBirds.org is implemented in partnership with the National Museum of Natural History at the Bulgarian Academy of Sciences and with the financial support of the LIFE Program of the European Union.", "PUBLIC_INVERTEBRATES_MAP_SPECIES": "species", "PUBLIC_INVERTEBRATES_MAP_INDIVIDUALS": "individuals", - "INVERTEBRATES_LIST_TITLE_MAIN": "Form Protected invertebrates", - "INVERTEBRATES_LIST_TITLE_SECONDARY": "Observations of protected invertebrates", + "INVERTEBRATES_LIST_TITLE_MAIN": "Form Invertebrates", + "INVERTEBRATES_LIST_TITLE_SECONDARY": "Observations of invertebrates", "INVERTEBRATES_LIST_BUTTON_DELETE": "Delete", "INVERTEBRATES_LIST_BUTTON_NEW_ENTRY": "New observation", "INVERTEBRATES_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Confirm the deletion of {{count}} records", @@ -686,7 +685,7 @@ "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_DATE": "Date", - "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Location", + "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", "PUBLIC_BIRDS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", "USER_DETAIL_PRIVACY": "Public data and statistics", @@ -698,9 +697,6 @@ "WARNING_EXPORT_LIMIT": "The system limits exporting up to 20 000 records at a time", "SPECIES_COL_INTERESTING": "Interesting", "SPECIES_COL_SENSITIVE": "Sensitive", - "HERPTILES_DETAIL_CONFIDENTIAL": "Confidential", - "MAMMALS_DETAIL_CONFIDENTIAL": "Confidential", - "INVERTEBRATES_DETAIL_CONFIDENTIAL": "Confidential", "WARNING_EXPORT_DISABLED": "Exports of other user's data is not allowed", "NAV_FRIENDS": "Friends", "FRIENDS_TITLE": "Friends Management", @@ -714,7 +710,6 @@ "FRIENDS_UNSAVED_DATA_ALERT_MESSAGE": "There are unsaved changes! Are you sure you want to continue?", "FRIENDS_SAVE_SUCCESS": "Friends saved successfully", "FRIENDS_SAVE_ERROR": "Error while saving friends", - "CBM_DETAIL_CONFIDENTIAL": "Confidential", "PUBLIC_BIRDS_STATISTIC_TABLE_COL_COUNT_SPECIES": "{{count}} species", "PUBLIC_BIRDS_STATISTIC_TABLE_COL_COUNT_RECORDS": "{{count}} observations", "NAV_PRIVATE_FORMS": "My and shared data", @@ -727,8 +722,8 @@ "HERPTILES_PUBLIC_LIST_TITLE_SECONDARY": "Observations of amphibians and reptiles", "MAMMALS_PUBLIC_LIST_TITLE_MAIN": "Form Mammals", "MAMMALS_PUBLIC_LIST_TITLE_SECONDARY": "Observations of mammals", - "INVERTEBRATES_PUBLIC_LIST_TITLE_MAIN": "Form Protected invertebrates", - "INVERTEBRATES_PUBLIC_LIST_TITLE_SECONDARY": "Observations of protected invertebrates", + "INVERTEBRATES_PUBLIC_LIST_TITLE_MAIN": "Form Invertebrates", + "INVERTEBRATES_PUBLIC_LIST_TITLE_SECONDARY": "Observations of invertebrates", "CICONIA_PUBLIC_LIST_TITLE_MAIN": "Form White stork", "CICONIA_PUBLIC_LIST_TITLE_SECONDARY": "Observations of White stork", "PUBLIC_LIST_FILTER_USER": "Observer", @@ -778,7 +773,6 @@ "PLANTS_DETAIL_UNSAVED_DATA_ALERT_TITLE": "There are unsaved changes!", "PLANTS_DETAIL_UNSAVED_DATA_ALERT_MESSAGE": "There are unsaved changes! Are you sure you want to continue?", "PLANTS_DETAIL_SPECIES": "Species", - "PLANTS_DETAIL_CONFIDENTIAL": "Confidential", "PLANTS_DETAIL_ELEVATION": "Elevation", "PLANTS_DETAIL_HABITAT": "Habitat", "PLANTS_DETAIL_REPORTING_UNIT": "Reporting unit", @@ -789,7 +783,7 @@ "PLANTS_DETAIL_ACCOMPANYING_SPECIES": "Accompanying species", "PUBLIC_PLANTS_TITLE": "Form Protected plants", "PUBLIC_PLANTS_SUBTITLE": "Observations of protected plants", - "PUBLIC_PLANTS_TEXT": "Plants are part of Smartbirds.org", + "PUBLIC_PLANTS_TEXT": "This part of www.SmartBirds.org is implemented in partnership with the National Museum of Natural History at the Bulgarian Academy of Sciences and with the financial support of the LIFE Program of the European Union.", "PUBLIC_PLANTS_MAP_SPECIES": "species", "PUBLIC_PLANTS_MAP_INDIVIDUALS": "Individuals", "LOGO_NMNHS_ALT": "NMNHS", @@ -841,8 +835,8 @@ "USER_LIST_TABLE_GDPR_CONSENT": "GDPR", "USER_DETAIL_GDPR_CONSENT_GRANTED": "Granted", "USER_DETAIL_GDPR_CONSENT_NOT_GRANTED": "Not granted", - "CAMPAIGN1_HEADER": "Let's count together up to the millionth record!", - "CAMPAIGN1_SUBHEADER": "Make the millionth record and win a tablet!", + "CAMPAIGN1_HEADER": "Let's count together up to the two millionth record!", + "CAMPAIGN1_SUBHEADER": "Make the two millionth record and win a voucher for the Sporting Goods Shops \"Stenata\"!", "CAMPAIGN1_BUTTON": "SIGN UP", "CAMPAIGN1_TEXT": "Start your data recording now!", "PUBLIC_HERPTILES_TOP_OBSERVERS_RECORDS_MONTH": "The most observations entered for the last 30 days", @@ -855,7 +849,7 @@ "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_DATE": "Date", - "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Location", + "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", "PUBLIC_HERPTILES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", "PUBLIC_MAMMALS_TOP_OBSERVERS_RECORDS_MONTH": "The most observations entered for the last 30 days", @@ -868,7 +862,7 @@ "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_DATE": "Date", - "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Location", + "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", "PUBLIC_MAMMALS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", "PUBLIC_INVERTEBRATES_TOP_OBSERVERS_RECORDS_MONTH": "The most observations entered for the last 30 days", @@ -881,7 +875,7 @@ "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_DATE": "Date", - "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Location", + "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", "PUBLIC_INVERTEBRATES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", "PUBLIC_PLANTS_TOP_OBSERVERS_RECORDS_MONTH": "The most observations entered for the last 30 days", @@ -894,7 +888,7 @@ "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_DATE": "Date", - "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Location", + "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", "PUBLIC_PLANTS_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", "PUBLIC_STATISTIC_TABLE_COL_COUNT_SPECIES": "{{count}} species", @@ -908,7 +902,7 @@ "STATS_MAMMALS_SUBTITLE": "Observations statistics", "STATS_PLANTS_TITLE": "Protected plants", "STATS_PLANTS_SUBTITLE": "Observations statistics", - "STATS_INVERTEBRATES_TITLE": "Protected invertebrates", + "STATS_INVERTEBRATES_TITLE": "Invertebrates", "STATS_INVERTEBRATES_SUBTITLE": "Observations statistics", "STATS_INTERESTING_SPECIES_TITLE_TAB": "Interesting species", "STATS_TOP_SPECIES_MONTH_TITLE_TAB": "The most frequently observed species", @@ -1004,6 +998,373 @@ "THREATS_LIST_FILTER_CATEGORY": "Category", "PUBLIC_THREATS_TITLE": "Form Threats", "PUBLIC_THREATS_SUBTITLE": "Observations of threats", - "PUBLIC_THREATS_TEXT": "Threats are part of Smartbirds.org", - "FORM_LABEL_THREATS": "Threats" -} \ No newline at end of file + "PUBLIC_THREATS_TEXT": "This module of SmartBirds.org has been implemented within the project {{linkStart}}\"Egyptian Vulture New LIFE\"{{linkEnd}} (LIFE16 NAT/BG/000874) financed by the LIFE + program The European Union and co-funded by the A. G. Leventis Foundation.", + "FORM_LABEL_THREATS": "Threats", + "LOGO_LIFENEOPHRON_ALT": "Program Life+", + "LOGO_BIRDLIFE_ALT": "BirdLife International", + "LOGO_LEVENTIS_ALT": "A. G. Leventis Foundation", + "LANGUAGE_ALBANIAN": "Albanian", + "LANGUAGE_MACEDONIAN": "Macedonian", + "PUBLIC_THREATS_FILTER_BY_THREAT": "Threat:", + "PUBLIC_THREATS_FILTER_ALL_THREATS": "- all threats -", + "PUBLIC_THREATS_FILTER_BY_DATE": "Period:", + "PUBLIC_THREATS_FILTER_ALL_TIME": "- all times -", + "PUBLIC_THREATS_FILTER_1_MONTH": "last month", + "PUBLIC_THREATS_FILTER_3_MONTHS": "last 3 months", + "PUBLIC_THREATS_FILTER_6_MONTHS": "last 6 months", + "PUBLIC_THREATS_FILTER_1_YEAR": "last year", + "NAV_ORGANIZATIONS": "Organizations", + "ORGANIZATIONS_TITLE": "Organizations", + "ORGANIZATIONS_BUTTON_SAVE": "Save", + "CHANGE_ORGANIZATION_WARNING_MESSAGE": "Changing organization will reset user's role to observer", + "LIST_TABLE_ORGANIZATION": "Organization", + "LIST_FILTER_ORGANIZATION": "Organization", + "USER_DETAIL_ORGANIZATION": "Organization", + "LIST_FILTER_SETTLEMENT": "Settlement", + "LIST_TABLE_SETTLEMENT": "Settlement", + "DASHBOARD_CARD_RETRY": "Retry", + "DASHBOARD_CARD_MODERATOR_REVIEW": "for review", + "FIELD_MODERATOR_REVIEW": "Moderator review", + "FIELD_MODERATOR_REVIEW_HELP": "If you want a moderator to review your record. Requires a picture attachment!", + "LIST_FILTER_MODERATOR_REVIEW": "Moderator review", + "FIELD_CONFIDENTIAL": "Confidential", + "FORM_DETAIL_BUTTON_CONFIRM": "Confirm", + "SETTLEMENT_VALUE_UNKNOWN": "Unknown", + "SETTLEMENT_VALUE_PENDING": "Processing…", + "NAV_ATLAS": "Atlas", + "NAV_ATLAS_DASHBOARD": "Dashboard", + "NAV_ATLAS_REQUEST": "Request square", + "ATLAS_DASHBOARD_TITLE": "Atlas Dashboard", + "ATLAS_REQUEST_TITLE": "Request squares", + "ATLAS_REQUEST_SUBTITLE": "Choose up to 10 squares", + "ATLAS_REQUEST_BTN_SAVE": "Save", + "ATLAS_REQUEST_ZONES_HEADING": "For each selected square it is indicated what percentage of the species registered during the previous atlas (Yankov, 2007) were also recorded after 2016.", + "ATLAS_REQUEST_ZONES_TEXT": "Selected squares (10 X 10 km) for atlas survey", + "ATLAS_LEGEND_LOW": "Poorly studied", + "ATLAS_LEGEND_MEDIUM": "Мedium studied", + "ATLAS_LEGEND_HIGH": "Well studied", + "ATLAS_REQUEST_ZONES_HINT": "Select the squares you want", + "ATLAS_DASHBOARD_ZONE_HEADING": "Square information", + "ATLAS_DASHBOARD_ZONE_HEADER_SPECIES": "Species", + "ATLAS_DASHBOARD_ZONE_HEADER_ATLAS": "Atlas 2007", + "ATLAS_DASHBOARD_ZONE_HEADER_OBSERVATION": "Personal observation", + "ATLAS_DASHBOARD_ZONE_TEXT": "Comparison of species found in the square in the previous period according to the Atlas of breeding birds in Bulgaria (Iankov, 2007), the species identified by the user and all species identified in the square after 01.01.2016", + "ATLAS_DASHBOARD_ZONE_NO_ROWS": "There are no known bird species either in the atlas or in the user's observations", + "PUBLIC_ATLAS_TITLE": "Atlas", + "PUBLIC_ATLAS_SUBTITLE": "Level of research after 01.01.2016 (base: UTM grid 10 x 10 km)", + "PUBLIC_ATLAS_PAGE_TITLE": "Birds Atlas", + "ATLAS_DASHBOARD_ZONE_HEADER_OTHER": "All observations", + "PUBLIC_ATLAS_TEXT": "This module was developed within the project \"Citizens' Science for the Benefit of Local Communities and Nature\" with financial support from the {{linkStart}} Active Citizens Fund {{linkEnd}} under the EEA Financial Mechanism", + "PUBLIC_ATLAS_LONG": "Atlas of Breeding Birds in Bulgaria", + "PUBLIC_ATLAS_SHORT": "Atlas", + "ATLAS_STATS_TOP_OBSERVERS_HEADING": "Select a square from the map", + "ATLAS_STATS_TOP_OBSERVERS_ROW_VALUE": "{{ species }} species", + "ATLAS_STATS_TOP_OBSERVERS_TEXT": "Total number of species: {{species}} species", + "ATLAS_STATS_TOP_OBSERVERS_HINT": "Select the square on the map to see information about it", + "ATLAS_STATS_TITLE": "Atlas of statistics", + "ATLAS_STATS_SUBTITLE": "Observers that entered data for the largest number of 10 x10 km UTM squares", + "ATLAS_STATS_TOP_OBSERVERS": "Most observations per square", + "ATLAS_STATS_TOP_OBSERVERS_TITLE_TAB": "Observers by squares", + "ATLAS_STATS_RANKING_LABEL_COUNT": "{{ count }} squres", + "ATLAS_STATS_RANKING_TITLE_TAB": "Most squares", + "ATLAS_STATS_RANKING": "Most visited squares by an observer", + "ATLAS_MISSING_SPECIES_TITLE": "Unidentified species by squares", + "NAV_ATLAS_MISSING_SPECIES": "Unidentified species", + "ATLAS_MISSING_SPECIES_ZONE_HEADING": "Information on unidentified species", + "ATLAS_MISSING_SPECIES_ZONE_TEXT": "Select a square on the map to see the unidentified species", + "ATLAS_MISSING_SPECIES_ZONE_HEADER_SPECIES": "Species", + "ATLAS_MISSING_SPECIES_ZONE_NO_ROWS": "There are no unidentified species for this square", + "FORM_ATLAS_SHORT": "Atlas Birds", + "LANGUAGE_GREEK": "Greek", + "LANGUAGE_TURKISH": "Turkish", + "LANGUAGE_ARABIC": "Arabic", + "LANGUAGE_FRENCH": "French", + "PUBLIC_HOME_FUNDING_LIST": "
  • {{linkLIFEStart}} European Union LIFE Program {{linkEnd}}\n
  • {{linkUS4BGStart}} America for Bulgaria Foundation {{linkEnd}}\n
  • Mtel eco grant program\n
  • {{linkEEAGrantsStart}} Program BG03 in Bulgaria under the Financial Mechanism of the European Economic Area 2009-2014 {{linkEnd}}\n
  • {{linkPUDOOSStart}} Environmental Management Company {{linkEnd}}\n
  • {{linkLeventisStart}} A. G. Leventis Foundation {{linkEnd}}\n
  • {{linkEurNaturStart}} EuroNatur Foundation, Germany. {{linkEnd}}\n
  • {{linkActiveCitizensFundStart}} Active Citizens Fund {{linkEnd}} under the EEA Financial Mechanism.", + "LOGO_ACTIVECITIZENSFUND_ALT": "Active Citizens Fund", + "LOGO_PUDOOS_ALT": "Environmental Management Company", + "FIELD_OBSERVATION_METHODOLOGY": "Observation methodology", + "NAV_ATLAS_INTEREST_MAP": "Rate of interest", + "ATLAS_INTEREST_MAP_TITLE": "Map rate of interest", + "ATLAS_INTEREST_MAP_NONE": "Free square", + "ATLAS_INTEREST_MAP_LOW": "Select", + "ATLAS_INTEREST_MAP_MEDIUM": "Medium interest", + "ATLAS_INTEREST_MAP_HIGH": "High interest", + "NAV_ATLAS_MODERATOR_PROGRESS": "Survey of squares (for moderators)", + "ATLAS_MODERATOR_PROGRESS_TITLE": "Square progress", + "ATLAS_MODERATOR_PROGRESS_SUBTITLE": "Found species compared to the old atlas", + "ATLAS_LEGEND_VHIGH": "Very well studied", + "ATLAS_MODERATOR_PROGRESS_ZONE_HEADING": "Select square", + "ATLAS_MODERATOR_PROGRESS_ZONE_TEXT": "Square stats by user and methodology", + "ATLAS_MODERATOR_PROGRESS_ZONE_COMPLETE": "Finish square", + "ATLAS_MODERATOR_PROGRESS_ZONE_UNCOMPLETE": "Open square", + "ATLAS_MODERATOR_PROGRESS_ZONE_COMPLETE_CONFIRM": "Please confirm finishing the square. This operation will remove the cell from all users selections and can only be reverted by an administrator.", + "ATLAS_MODERATOR_PROGRESS_ZONE_UNCOMPLETE_CONFIRM": "Please confirm reopening the square", + "ATLAS_LEGEND_COMPLETED": "Completed", + "ATLAS_NEW_SPECIES_TITLE": "New species by squares", + "NAV_ATLAS_NEW_SPECIES": "New species", + "ATLAS_NEW_SPECIES_ZONE_HEADING": "Information on new species", + "ATLAS_NEW_SPECIES_ZONE_TEXT": "Select a square on the map to see the new species", + "ATLAS_NEW_SPECIES_ZONE_HEADER_SPECIES": "Species", + "ATLAS_NEW_SPECIES_ZONE_NO_ROWS": "There are no new species for this square", + "FORM_LABEL_PYLONS": "Powerlines’ study", + "FORM_PYLONS_LONG": "Powerlines’ study", + "FORM_PYLONS_SHORT": "Powerlines’ study", + "FORM_LABEL_PYLONS_CASUALTIES": "Powerlines’ casualties", + "FORM_PYLONS_CASUALTIES_LONG": "Powerlines’ casualties", + "FORM_PYLONS_CASUALTIES_SHORT": "Powerlines’ casualties", + "PYLONS_LIST_TITLE_MAIN": "Powerlines’ study", + "PYLONS_LIST_TITLE_SECONDARY": "Form for Powerlines’ study", + "MONITORING_LIST_FILTER_USER": "Observer", + "MONITORING_LIST_FILTER_FROM_DATE": "From", + "MONITORING_LIST_FILTER_TO_DATE": "To", + "MONITORING_LIST_BUTTON_DELETE_CONFIRM_MESSAGE": "Confirm the deletion of {{count}} records", + "MONITORING_LIST_BUTTON_DELETE": "Delete", + "MONITORING_LIST_BUTTON_NEW_ENTRY": "New observation", + "MONITORING_LIST_FILTER_SPECIES": "Species", + "MONITORING_LIST_TAB_LIST": "List", + "MONITORING_LIST_TAB_MAP": "Map", + "MONITORING_LIST_MAP_BUTTON_SHOW_ALL": "Show all", + "MONITORING_LIST_MAP_SHOWN_RECORDS_WARNING": "Only the last {{count}} records are shown!", + "MONITORING_LIST_TABLE_USER": "Observer", + "MONITORING_LIST_TABLE_DATE": "Date/time", + "MONITORING_LIST_TABLE_UPDATED": "Updated", + "MONITORING_LIST_TABLE_SPECIES": "Species", + "MONITORING_LIST_TABLE_COUNT": "Count", + "PYLONS_CASUALTIES_LIST_TITLE_SECONDARY": "Casualties from pylons or electric grid", + "PYLONS_CASUALTIES_LIST_TITLE_MAIN": "Powerlines’ casualties", + "MONITORING_DETAIL_UNSAVED_DATA_ALERT_TITLE": "There are unsaved changes!", + "MONITORING_DETAIL_UNSAVED_DATA_ALERT_MESSAGE": "There are unsaved changes! Are you sure you want to continue?", + "MONITORING_DETAIL_TITLE_NEW": "New observation", + "MONITORING_DETAIL_TITLE_EDIT": "Edit record", + "FIELD_SPECIES_HELP_PROTECTED_SPECIES": "Species marked with * are \"protected\"", + "PYLONS_CASUALTIES_DETAIL_COUNT": "Count", + "PYLONS_CASUALTIES_DETAIL_AGE": "Age", + "PYLONS_CASUALTIES_DETAIL_SEX": "Sex", + "PYLONS_CASUALTIES_DETAIL_CAUSE_OF_DEATH": "Cause of death", + "PYLONS_CASUALTIES_DETAIL_BODY_CONDITION": "Body condition", + "PYLONS_DETAIL_HABITAT_100M_PRIME": "Primary habitat 100 m", + "PYLONS_DETAIL_HABITAT_100M_SECOND": "Secondary habitat 100 m", + "PYLONS_FIELD_PYLON_TYPE": "Pylon type", + "PYLONS_FIELD_SPECIES_NEST": "Species nest on pylon", + "PYLONS_FIELD_TYPE_NEST": "Nest type", + "PYLONS_FIELD_INSULATED": "Pylon insulated", + "PYLONS_LIST_TYPE": "Pylon type", + "PYLONS_CASUALTIES_LIST_CAUSE_OF_DEATH": "Cause of death", + "PYLONS_PUBLIC_LIST_TITLE_MAIN": "Pylons", + "PYLONS_PUBLIC_LIST_TITLE_SECONDARY": "pylons desc.", + "PYLONS_CASUALTIES_PUBLIC_LIST_TITLE_MAIN": "Powerlines’ casualties", + "PYLONS_CASUALTIES_PUBLIC_LIST_TITLE_SECONDARY": "casualties desc.", + "PYLONS_NEST_SPECIES": "Species nest on pylon", + "PYLONS_FIELD_DAMAGED_INSULATION": "Damaged insulation", + "NAV_DAILY_REPORT": "Daily report", + "DAILY_REPORT_FILTER_DATE": "Date", + "DAILY_REPORT_TITLE_MAIN": "Daily report", + "DAILY_REPORT_TITLE_SECONDARY": "Summary of recorded observation for a given day", + "DAILY_REPORT_SUBMIT": "Load report", + "DAILY_REPORT_LIST_TABLE_FORM": "Form", + "DAILY_REPORT_LIST_TABLE_LOCATION": "Settlement", + "DAILY_REPORT_LIST_TABLE_SPECIES": "Species", + "DAILY_REPORT_LIST_TABLE_COUNT": "Count", + "DAILY_REPORT_LIST_TABLE_SPECIES_LATIN": "Latin name", + "FORM_LABEL_BIRDS_MIGRATIONS": "Form Birds migrations", + "FORM_BIRDS_MIGRATIONS_LONG": "Observations of migratory soaring birds from a viewpoint", + "FORM_BIRDS_MIGRATIONS_SHORT": "Form Birds migrations", + "BIRDS_MIGRATIONS_LIST_TITLE_MAIN": "Form Birds migrations", + "BIRDS_MIGRATIONS_LIST_TITLE_SECONDARY": "Observations of migratory soaring birds from a viewpoint", + "MONITORING_LIST_BIRDS_MIGRATION_POINT": "Migration point", + "LIST_FILTER_BIRDS_MIGRATION_POINT": "List of birds migration point", + "BIRDS_MIGRATIONS_DETAIL_MIGRATION_POINT": "Migration point", + "BIRDS_MIGRATIONS_DETAIL_LOCATION_FROM_MIGRATION_POINT": "Direction from migration point", + "BIRDS_MIGRATIONS_DETAIL_SEX": "Sex", + "BIRDS_MIGRATIONS_DETAIL_PLUMAGE": "Pulmage", + "BIRDS_MIGRATIONS_DETAIL_AGE": "Age", + "BIRDS_MIGRATIONS_DETAIL_VISOCHINA_POLET": "Flight height, m", + "BIRDS_MIGRATIONS_DETAIL_POSOKA_POLET_FROM": "Flight direction from", + "BIRDS_MIGRATIONS_DETAIL_POSOKA_POLET_TO": "Flight direction to", + "BIRDS_MIGRATIONS_DETAIL_TYPE_FLIGHT": "Type of flight", + "DETAIL_DISTANCE_FROM_MIGRATION_POINT": "Distance", + "FORM_LABEL_FISHES": "Form Fishes", + "FORM_FISHES_LONG": "Form Fishes", + "FORM_FISHES_SHORT": "Form Fishes", + "FISHES_LIST_TITLE_MAIN": "Form Fishes", + "FISHES_LIST_TITLE_SECONDARY": "Observations of Fishes", + "FISHES_DETAIL_SEX": "Sex", + "FISHES_DETAIL_NAME_WATER_BODY": "Warerbody", + "FISHES_DETAIL_AGE": "Age", + "FISHES_DETAIL_SIZE_TL_MM": "Total length (TL), mm", + "FISHES_DETAIL_SIZE_SL_MM": "Standart length (Sl), mm", + "FISHES_DETAIL_MASA_GR": "Weight, gr", + "FISHES_DETAIL_FINDINGS": "Findings", + "FISHES_DETAIL_MONITORING_TYPE": "Monitoring type", + "FISHES_DETAIL_TRANSECT_LENGTH_M": "Transect length, m", + "FISHES_DETAIL_TRANSECT_WIDTH_M": "Transect width, m", + "FISHES_DETAIL_FISHING_AREA_M": "Fishing area, sq. m", + "FISHES_DETAIL_EXPOSITION": "Exposition", + "FISHES_DETAIL_MESH_SIZE": "Mesh size", + "FISHES_DETAIL_COUNT_NET_TRAP": "Count nets/traps", + "FISHES_DETAIL_WATER_TEMP": "Water temperature", + "FISHES_DETAIL_CONDUCTIVITY": "Conductivity", + "FISHES_DETAIL_PH": "pH", + "FISHES_DETAIL_O2MG_L": "O2 mg/l", + "FISHES_DETAIL_O2PERCENT": "O2 %", + "FISHES_DETAIL_SALINITY": "Salinity", + "FISHES_DETAIL_HABITAT_DESCRIPTION_TYPE": "Waterbody type", + "FISHES_DETAIL_SUBSTRATE_MUD": "Mud, %", + "FISHES_DETAIL_SUBSTRATE_SILT": "Silt, %", + "FISHES_DETAIL_SUBSTRATE_SAND": "Sand, %", + "FISHES_DETAIL_SUBSTRATE_GRAVEL": "Gravel, %", + "FISHES_DETAIL_SUBSTRATE_SMALL_STONES": "Small stones, %", + "FISHES_DETAIL_SUBSTRATE_COBBLE": "Cobble, %", + "FISHES_DETAIL_SUBSTRATE_BOULDER": "Boulder, %", + "FISHES_DETAIL_SUBSTRATE_ROCK": "Rock, %", + "FISHES_DETAIL_SUBSTRATE_OTHER": "Other, %", + "FISHES_DETAIL_WATER_LEVEL": "Water level", + "FISHES_DETAIL_RIVER_CURRENT": "River current", + "FISHES_DETAIL_TRANSECT_AV_DEPTH": "Average depth", + "FISHES_DETAIL_TRANSECT_MAX_DEPTH": "Max. depth", + "FISHES_DETAIL_SLOPE": "Slope", + "FISHES_DETAIL_BANK_TYPE": "Bank type", + "FISHES_DETAIL_SHADING": "Shading, %", + "FISHES_DETAIL_RIPARIAN_VEGETATION": "Riparian vegetation, %", + "FISHES_DETAIL_SHELTERS": "Shelters", + "FISHES_DETAIL_TRANSPARENCY": "Water transperancy", + "FISHES_DETAIL_VEGETATION_TYPE": "Vegetation type", + "FISHES_DETAIL_NATURAL_BARRIERS": "Natural barriers", + "CLASS_FISHES": "Fishes", + "PUBLIC_FISHES_TEXT": "This part of SmartBirds.org is implemented with the financial support of the LIFE Program of the European Union.", + "PUBLIC_FISHES_TITLE": "Form Fishes", + "PUBLIC_FISHES_SUBTITLE": "Observations of Fishes", + "PUBLIC_MAP_SPECIES": "species", + "PUBLIC_MAP_INDIVIDUALS": "Individuals", + "USER_DETAIL_ALLOW_DATA_MOSV": "I agree that the data collected by me will be provided to the MOEW", + "USER_DETAIL_ALLOW_DATA_SCIENCE_PUBLICATIONS": "I agree that the data collected by me will be published in a scientific article", + "NAV_BIRDS_MIGRATIONS": "Form Birds migrations", + "STATS_BIRDS_MIGRATIONS_SEASON_TOTALS_TITLE_TAB": "Total birds migrating per sesones", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_TOTALS": "Total number of migratory birds for each season - autumn (August 10 - October 30) and spring (March 1 to May 31)", + "STATS_BIRDS_MIGRATIONS_PEAK_DAILY_SPECIES_TITLE_TAB": "Peak daily numbers for a season", + "PUBLIC_BIRDS_MIGRATIONS_PEAK_DAILY_SPECIES": "Maximum (peak) daily numbers of migratory birds (with date selection)", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_DATE": "Date", + "PUBLIC_BIRDS_MIGRATIONS_COL_MIGRATION_POINT": "Migration point", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", + "PUBLIC_FORM_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_FALL": "fall", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_SPRING": "spring", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_DATE": "Date", + "PUBLIC_BIRDS_MIGRATIONS_PEAK_NO_RECORDS": "No records for the selected date", + "PUBLIC_BIRDS_MIGRATIONS_TITLE": "Form Birds migrations", + "PUBLIC_BIRDS_MIGRATIONS_SUBTITLE": "Observations of migratory soaring birds from a viewpoint", + "PUBLIC_BIRDS_MIGRATIONS_TEXT": "This part of www.SmartBirds.org is implemented in partnership with the National Museum of Natural History at the Bulgarian Academy of Sciences and with the financial support of the LIFE Program of the European Union.", + "PUBLIC_BIRDS_MIGRATIONS_TOP_SPECIES_MONTH": "Interesting species observed for the last 30 days", + "FISHES_PUBLIC_LIST_TITLE_MAIN": "Form Fishes", + "FISHES_PUBLIC_LIST_TITLE_SECONDARY": "Observations of Fishes", + "BIRDS_MIGRATIONS_PUBLIC_LIST_TITLE_MAIN": "Form Birds migrations", + "BIRDS_MIGRATIONS_PUBLIC_LIST_TITLE_SECONDARY": "Observations of migratory soaring birds from a viewpoint", + "STATS_BIRDS_MIGRATIONS_TITLE": "Birds migrations", + "STATS_BIRDS_MIGRATIONS_SUBTITLE": "Observations statistics", + "STATS_FISHES_TITLE": "Form Fishes", + "STATS_FISHES_SUBTITLE": "Observations statistics", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_SPECIES": "Species", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_DATE": "Date", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_LOCATION": "Settlement", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_COUNT": "Count", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES_COL_OBSERVER": "Observer", + "PUBLIC_FISHES_LAST_INTERESTING_SPECIES": "Interesting species observed for the last 30 days", + "PUBLIC_FISHES_TOP_SPECIES_MONTH": "The most often observed species for the last 30 days", + "PUBLIC_FISHES_TOP_OBSERVERS_RECORDS_YEAR": "Most observations recorded per calendar year", + "PUBLIC_FISHES_TOP_OBSERVERS_SPECIES_YEAR": "The most species observed per calendar year", + "PUBLIC_STATISTIC_TABLE_COL_SUM_COUNT": "{{count}} individuals", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_MIGRATION_POINT": "Migration point", + "PUBLIC_BIRDS_MIGRATIONS_SEASON_NO_RECORDS": "No records for the selected migration point", + "PUBLIC_NO_RECORDS": "No records", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_MIGRATION_POINT_PLACEHOLDER": "All", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_SPECIES": "Species", + "PUBLIC_BIRDS_MIGRATIONS_FILTER_SPECIES_PLACEHOLDER": "Choose a species", + "FORM_LIST_BUTTON_FULL_CSV_EXPORT_TOOLTIP": "Generating a csv file with all records found and all fields", + "NAV_MAP_LAYERS": "Map layers", + "MAP_LAYERS_TITLE": "Map layers", + "MAP_LAYERS_UNSAVED_DATA_TITLE": "There are unsaved changes!", + "MAP_LAYERS_UNSAVED_DATA_MESSAGE": "There are unsaved changes! Are you sure you want to continue?", + "MAP_LAYERS_COL_LABEL": "Label", + "MAP_LAYERS_COL_SUMMARY": "Summary", + "MAP_LAYERS_COL_URL": "URL", + "MAP_LAYERS_ROW_WIDTH": "Width (px)", + "MAP_LAYERS_COL_WIDTH": "Height (px)", + "MAP_LAYERS_BUTTON_ADD": "Add", + "MAP_LAYERS_BUTTON_SAVE": "Save", + "FORM_LIST_BUTTON_FULL_ZIP_EXPORT_TOOLTIP": "Generating a zip file with all records found and all fields, along with their corresponding pictures and gpx files", + "CLASS_BATS": "Bats", + "FORM_LABEL_BATS": "Form Bats", + "FORM_BATS_LONG": "Form Bats", + "FORM_BATS_SHORT": "Form Bats", + "BATS_LIST_TITLE_MAIN": "Form Bats", + "BATS_LIST_TITLE_SECONDARY": "Observations of bats", + "BATS_DETAIL_METODOLOGY": "Methodology", + "BATS_DETAIL_T_CAVE": "T°C Roost", + "BATS_DETAIL_H_CAVE": "H - Roost humidity (%)", + "BATS_DETAIL_TYPE_LOCATION": "Type of the locality", + "BATS_DETAIL_SUBLOCALITY": "Sublocality", + "BATS_DETAIL_SWARMING": "Swarming", + "BATS_DETAIL_AGE": "Age", + "BATS_DETAIL_SEX": "Sex", + "BATS_DETAIL_HABITAT": "Habitats", + "BATS_DETAIL_CONDITION": "Condition", + "BATS_DETAIL_TYPE_CONDITION": "Type of cont", + "BATS_DETAIL_REPRODUCTIVE_STATUS": "Reproductive status", + "BATS_DETAIL_RING": "Ring", + "BATS_DETAIL_RING_N": "Ring number", + "BATS_DETAIL_BODY_LENGTH": "L - Body length (mm)", + "BATS_DETAIL_TAIL_LENGTH": "C - Tail length (mm)", + "BATS_DETAIL_EAR_LENGTH": "A - Ear length (mm)", + "BATS_DETAIL_FOREARM_LENGTH": "FA - Forearm length (mm)", + "BATS_DETAIL_LENGTH_THIRD_DIGIT": "D3 - Length third digit (mm)", + "BATS_DETAIL_LENGTH_FIFTH_DIGIT": "D5 - Length fifth digit (mm)", + "BATS_DETAIL_LENGTH_WS": "WS - Length (mm)", + "BATS_DETAIL_WEIGHT": "Weight (g)", + "BATS_DETAIL_SWARMING_HELP": " ", + "BATS_DETAIL_AGE_HELP": "Age of the individual", + "BATS_DETAIL_SEX_HELP": "Sex of the individual", + "BATS_DETAIL_CONDITION_HELP": "Condition of the ind. bat", + "BATS_DETAIL_TRAGUS": "Tr - Tragus", + "BATS_DETAIL_UPPER_MOLAR": "Upper molar - CM3", + "BATS_PUBLIC_LIST_TITLE_MAIN": "Form Bats", + "BATS_PUBLIC_LIST_TITLE_SECONDARY": "Observations of bats", + "IMPORT_SUMMARY_TITLE": "Import Summary", + "IMPORT_SUMMARY_IMPORTING": "Importing...", + "IMPORT_SUMMARY_TOTAL": "Total", + "IMPORT_SUMMARY_PROCESSED": "Processed", + "IMPORT_SUMMARY_ROWS_INSERTED": "Inserted", + "IMPORT_SUMMARY_ROWS_UPDATED": "Updated", + "IMPORT_SUMMARY_ERRORS": "Errors", + "IMPORT_SUMMARY_ROW": "Row", + "BTN_FORCE_IMPORT": "Force import", + "FORCE_IMPORT_WARNING_MESSAGE": "There are errors in the provided file. Do you want to proceed and ignore the rows with errors?", + "IMPORT_RECORDS_MODAL_TITLE": "Import from file", + "IMPORT_RECORDS_CHOOSE_FILE": "Choose file", + "IMPORT_RECORDS_READING_FILE": "Loading file...", + "IMPORT_RECORDS_LANGUAGE": "Language", + "BTN_IMPORT": "Import", + "FORM_LIST_BUTTON_CSV_IMPORT_TOOLTIP": "Import from CSV file", + "FISHES_DETAIL_TOTAL_LENGTH": "Total length (TL) class, cm", + "IMPORT_RECORDS_IGNORE_ERRORS": "Ignore errors", + "You will be notified by email when your import is ready": "You will be notified by email when your import is ready", + "EBP_SETTINGS_BUTTON_SAVE": "Save", + "EBP_SETTINGS_BUTTON_ADD": "Add", + "EBP_SETTINGS_TITLE": "Settings", + "EBP_SETTINGS_ORGANIZATIONS": "Organizations", + "EBP_SETTINGS_SOURCES": "Sources", + "EBP_SETTINGS_PROTOCOL": "Protocol", + "EBP_SETTINGS_SPECIES": "Species", + "EBP_SETTINGS_SPECIES_STATUS": "Species status", + "EBP_SETTINGS_NO_PERMISSION": "You do not have permission to access this page", + "EBP_SETTINGS_ORGANIZATION": "Organization", + "EBP_SETTINGS_ORGANIZATION_ENABLED": "Enabled", + "EBP_SETTINGS_SOURCE": "Source", + "EBP_SETTINGS_SOURCE_ENABLED": "Enabled", + "EBP_SETTINGS_SPECIES_CODE": "EBP Code", + "EBP_SETTINGS_SPECIES_SB_NAME": "SB name", + "EBP_SETTINGS_SPECIES_EBP_NAME": "EBP name", + "EBP_SETTINGS_SPECIES_STATUS_CODE": "EBP status code", + "EBP_SETTINGS_SPECIES_STATUS_SB_NAME": "SB status name", + "NAV_EBP": "EBP" +}