diff --git a/client/dist/autotranslateField.css b/client/dist/autotranslateField.css index 70de859..d74bb3b 100644 --- a/client/dist/autotranslateField.css +++ b/client/dist/autotranslateField.css @@ -1,5 +1,32 @@ +.modal.level51-modal { + display: block; +} + .level51-autotranslateField { - margin-top: 6px; + margin-top: 4px; +} +.level51-autotranslateField .font-icon-translatable { + color: #0071c4; +} +.level51-autotranslateField .level51-modal h3 { + margin-bottom: 8px; +} +.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue { + padding: 8px; + background: #f0f0f0; + margin-bottom: 16px; +} +.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue p:first-child { + margin-top: 0; +} +.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue p:last-child { + margin-bottom: 0; +} +.level51-autotranslateField .level51-modal .level51-pricingHint p:first-child { + margin-top: 0; +} +.level51-autotranslateField .level51-modal .level51-pricingHint p:last-child { + margin-bottom: 0; } diff --git a/client/dist/autotranslateField.css.map b/client/dist/autotranslateField.css.map index a6e6e8e..355928b 100644 --- a/client/dist/autotranslateField.css.map +++ b/client/dist/autotranslateField.css.map @@ -1 +1 @@ -{"version":3,"sources":["webpack://silverstripe-fluent-autotranslate/./client/src/js/App.vue","webpack://silverstripe-fluent-autotranslate/./App.vue"],"names":[],"mappings":"AA6FA;EACE;AC5FF","file":"autotranslateField.css","sourcesContent":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.level51-autotranslateField {\n margin-top: 6px;\n}\n",".level51-autotranslateField {\n margin-top: 6px;\n}\n"],"sourceRoot":""} \ No newline at end of file +{"version":3,"sources":["webpack://silverstripe-fluent-autotranslate/./client/src/js/Modal.vue","webpack://silverstripe-fluent-autotranslate/./Modal.vue","webpack://silverstripe-fluent-autotranslate/./client/src/js/App.vue","webpack://silverstripe-fluent-autotranslate/./App.vue"],"names":[],"mappings":"AAwFA;EACE;ACvFF;;AC+KA;EACE;AChLF;AD+KA;EAII;AChLJ;AD4KA;EASM;AClLN;ADyKA;EAaM;EACA;EACA;ACnLN;ADsLQ;EACE;ACpLV;ADuLQ;EACE;ACrLV;AD4LQ;EACE;AC1LV;AD6LQ;EACE;AC3LV","file":"autotranslateField.css","sourcesContent":["\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@import (reference) '../styles/vars';\n\n.modal.level51-modal {\n display: block;\n}\n",".modal.level51-modal {\n display: block;\n}\n","\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n@import (reference) '../styles/vars';\n\n.level51-autotranslateField {\n margin-top: @space-1;\n\n .font-icon-translatable {\n color: @color-cta;\n }\n\n .level51-modal {\n h3 {\n margin-bottom: @space-2;\n }\n\n .level51-autotranslateField-sourceValue {\n padding: @space-2;\n background: @color-mono-94;\n margin-bottom: @space-3;\n\n p {\n &:first-child {\n margin-top: 0;\n }\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n }\n\n .level51-pricingHint {\n p {\n &:first-child {\n margin-top: 0;\n }\n\n &:last-child {\n margin-bottom: 0;\n }\n }\n }\n }\n}\n",".level51-autotranslateField {\n margin-top: 4px;\n}\n.level51-autotranslateField .font-icon-translatable {\n color: #0071c4;\n}\n.level51-autotranslateField .level51-modal h3 {\n margin-bottom: 8px;\n}\n.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue {\n padding: 8px;\n background: #f0f0f0;\n margin-bottom: 16px;\n}\n.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue p:first-child {\n margin-top: 0;\n}\n.level51-autotranslateField .level51-modal .level51-autotranslateField-sourceValue p:last-child {\n margin-bottom: 0;\n}\n.level51-autotranslateField .level51-modal .level51-pricingHint p:first-child {\n margin-top: 0;\n}\n.level51-autotranslateField .level51-modal .level51-pricingHint p:last-child {\n margin-bottom: 0;\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/client/dist/autotranslateField.js b/client/dist/autotranslateField.js index c284db0..c1c0c0b 100644 --- a/client/dist/autotranslateField.js +++ b/client/dist/autotranslateField.js @@ -1,6 +1,28 @@ /******/ (() => { // webpackBootstrap /******/ var __webpack_modules__ = ({ +/***/ "./client/src/lang/de.json": +/*!*********************************!*\ + !*** ./client/src/lang/de.json ***! + \*********************************/ +/***/ ((module) => { + +"use strict"; +module.exports = JSON.parse('{"field":{"translateCta":"Übersetze von {sourceLocale} auf {targetLocale}"},"modal":{"translateCta":"Jetzt übersetzen","headline":"Übersetze von {sourceLocale} auf {targetLocale}","sourceValueLabel":"Inhalt {locale}"},"google":{"pricingHint":"
{charCount} Zeichen werden mit Google Cloud Translation übersetzt.
Der aktuelle Inhalt des Feldes ({targetLocale}) wird dabei überschrieben.
"}}'); + +/***/ }), + +/***/ "./client/src/lang/en.json": +/*!*********************************!*\ + !*** ./client/src/lang/en.json ***! + \*********************************/ +/***/ ((module) => { + +"use strict"; +module.exports = JSON.parse('{"field":{"translateCta":"Translate from {sourceLocale} to {targetLocale}"},"modal":{"translateCta":"Translate now","headline":"Translate from {sourceLocale} to {targetLocale}","sourceValueLabel":"{locale} value"},"google":{"pricingHint":"{charCount} characters will be translated using Google Cloud Translation.
The current content of the field ({targetLocale}) will be overwritten.
"}}'); + +/***/ }), + /***/ "./node_modules/axios/index.js": /*!*************************************!*\ !*** ./node_modules/axios/index.js ***! @@ -1841,27 +1863,44 @@ module.exports = { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! vue */ "./node_modules/vue/dist/vue.common.dev.js"); -/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(vue__WEBPACK_IMPORTED_MODULE_2__); +/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! vue */ "./node_modules/vue/dist/vue.common.dev.js"); +/* harmony import */ var vue__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(vue__WEBPACK_IMPORTED_MODULE_4__); /* harmony import */ var src_App_vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! src/App.vue */ "./client/src/js/App.vue"); -/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./util */ "./client/src/js/util.js"); +/* harmony import */ var vue_i18n__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! vue-i18n */ "./node_modules/vue-i18n/dist/vue-i18n.esm.js"); +/* harmony import */ var _lang_en_json__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ../lang/en.json */ "./client/src/lang/en.json"); +/* harmony import */ var _lang_de_json__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ../lang/de.json */ "./client/src/lang/de.json"); +/* harmony import */ var _util__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./util */ "./client/src/js/util.js"); + + + var render = function render(el) { - new (vue__WEBPACK_IMPORTED_MODULE_2___default())({ + vue__WEBPACK_IMPORTED_MODULE_4___default().use(vue_i18n__WEBPACK_IMPORTED_MODULE_5__.default); + var payload = JSON.parse(el.dataset.payload); + var i18n = new vue_i18n__WEBPACK_IMPORTED_MODULE_5__.default({ + locale: payload.cmsLocale, + fallbackLocale: 'en', + messages: { + en: _lang_en_json__WEBPACK_IMPORTED_MODULE_1__, + de: _lang_de_json__WEBPACK_IMPORTED_MODULE_2__ + } + }); + new (vue__WEBPACK_IMPORTED_MODULE_4___default())({ + i18n: i18n, render: function render(h) { return h(src_App_vue__WEBPACK_IMPORTED_MODULE_0__.default, { props: { - payload: JSON.parse(el.dataset.payload) + payload: payload } }); } }).$mount("#".concat(el.id)); }; -(0,_util__WEBPACK_IMPORTED_MODULE_1__.default)('.level51-autotranslateFieldPlaceholder', function (el) { +(0,_util__WEBPACK_IMPORTED_MODULE_3__.default)('.level51-autotranslateFieldPlaceholder', function (el) { setTimeout(function () { render(el); }, 1); @@ -1947,6 +1986,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var axios__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(axios__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var qs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! qs */ "./node_modules/qs/lib/index.js"); /* harmony import */ var qs__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(qs__WEBPACK_IMPORTED_MODULE_1__); +/* harmony import */ var _Modal_vue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./Modal.vue */ "./client/src/js/Modal.vue"); function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } } function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; } @@ -1969,18 +2009,50 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi // // // +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// + /** - * @todo proper styles, icons etc - * @todo more functions? - * @todo confirmation step to prevent accidental translations? * @todo error reporting - * @todo show amount of characters (check how to count html content?) - * @todo i18n */ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({ + components: { + Modal: _Modal_vue__WEBPACK_IMPORTED_MODULE_2__.default + }, props: { payload: { type: Object, @@ -1989,24 +2061,73 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi }, data: function data() { return { - inputElement: null + inputElement: null, + isTranslateModalVisible: false }; }, mounted: function mounted() { - this.inputElement = document.getElementById(this.payload.id); + this.inputElement = document.getElementById(this.inputId); }, computed: { + inputId: function inputId() { + return this.payload.id; + }, + provider: function provider() { + return this.payload.provider; + }, + providerConfig: function providerConfig() { + return this.payload.providerConfig; + }, endpoint: function endpoint() { - var endpoint = this.payload.googleTranslateApi; + var endpoint = this.providerConfig.apiEndpoint; var params = {}; if (this.payload.getVars && _typeof(this.payload.getVars) === 'object') { params = _objectSpread({}, this.payload.getVars); } + if (this.providerConfig.getVars && _typeof(this.providerConfig.getVars) === 'object') { + params = _objectSpread({}, this.providerConfig.getVars); + } + return "".concat(endpoint, "?").concat(qs__WEBPACK_IMPORTED_MODULE_1___default().stringify(params, { encode: true })); + }, + sourceLocale: function sourceLocale() { + return this.payload.sourceLocale; + }, + targetLocale: function targetLocale() { + return this.payload.targetLocale; + }, + sourceValue: function sourceValue() { + return this.payload.sourceValue; + }, + charCount: function charCount() { + return this.payload.sourceValue.length; + }, + ctaLabel: function ctaLabel() { + return this.$t('field.translateCta', { + sourceLocale: this.sourceLocale.title, + targetLocale: this.targetLocale.title + }); + }, + modalTitle: function modalTitle() { + return this.$t('modal.headline', { + sourceLocale: this.sourceLocale.title, + targetLocale: this.targetLocale.title + }); + }, + modalHeadline: function modalHeadline() { + return this.$t('modal.headline', { + sourceLocale: "".concat(this.sourceLocale.title, " (").concat(this.sourceLocale.locale, ")"), + targetLocale: "".concat(this.targetLocale.title, " (").concat(this.targetLocale.locale, ")") + }); + }, + modalSourceValueLabel: function modalSourceValueLabel() { + return this.$t('modal.sourceValueLabel', { + locale: this.sourceLocale.title + }); } }, methods: { @@ -2022,6 +2143,9 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi return _this.translateWithGoogle(); case 2: + _this.isTranslateModalVisible = false; + + case 3: case "end": return _context.stop(); } @@ -2041,9 +2165,9 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi _context2.prev = 0; _context2.next = 3; return axios__WEBPACK_IMPORTED_MODULE_0___default().post(_this2.endpoint, { - source: _this2.payload.sourceLocale, - target: _this2.payload.targetLocale, - q: [_this2.payload.sourceValue] + source: _this2.sourceLocale.code, + target: _this2.targetLocale.code, + q: [_this2.sourceValue] }); case 3: @@ -2088,6 +2212,97 @@ function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "functi /***/ }), +/***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/Modal.vue?vue&type=script&lang=js&": +/*!***********************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/Modal.vue?vue&type=script&lang=js& ***! + \***********************************************************************************************************************************************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export */ __webpack_require__.d(__webpack_exports__, { +/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) +/* harmony export */ }); +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({ + props: { + title: { + type: String, + required: false, + "default": null + } + }, + data: function data() { + return { + containerClass: 'level51-modal' + }; + }, + mounted: function mounted() { + window.addEventListener('keyup', this.handleKeyup, false); + window.addEventListener('click', this.handleClick, false); + this.storeOffset(); + document.getElementsByTagName('body')[0].classList.add('level51-overflow--hidden'); + }, + beforeDestroy: function beforeDestroy() { + window.removeEventListener('keyup', this.handleKeyup, false); + window.removeEventListener('click', this.handleClick, false); + document.getElementsByTagName('body')[0].classList.remove('level51-overflow--hidden'); + this.restoreOffset(); + }, + methods: { + close: function close() { + this.$emit('close'); + }, + handleKeyup: function handleKeyup(e) { + if (e.target.nodeName === 'INPUT') return; + + if (e.keyCode === 27) { + this.close(); + } + }, + handleClick: function handleClick(e) { + if (e.target && e.target.classList && typeof e.target.classList.contains === 'function' && e.target.classList.contains(this.containerClass)) { + this.close(); + } + }, + storeOffset: function storeOffset() { + document.body.style.top = "-".concat(window.pageYOffset, "px"); + }, + restoreOffset: function restoreOffset() { + var offset = document.body.style.top; + document.body.style.top = ''; + window.scrollTo(0, parseInt(offset || '0') * -1); + } + } +}); + +/***/ }), + /***/ "./node_modules/core-js/es/index.js": /*!******************************************!*\ !*** ./node_modules/core-js/es/index.js ***! @@ -16570,6 +16785,19 @@ __webpack_require__.r(__webpack_exports__); // extracted by mini-css-extract-plugin +/***/ }), + +/***/ "./node_modules/mini-css-extract-plugin/dist/loader.js!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/less-loader/dist/cjs.js!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/Modal.vue?vue&type=style&index=0&lang=less&": +/*!**************************************************************************************************************************************************************************************************************************************************************************************************************************!*\ + !*** ./node_modules/mini-css-extract-plugin/dist/loader.js!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/less-loader/dist/cjs.js!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/Modal.vue?vue&type=style&index=0&lang=less& ***! + \**************************************************************************************************************************************************************************************************************************************************************************************************************************/ +/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +// extracted by mini-css-extract-plugin + + /***/ }), /***/ "./node_modules/qs/lib/formats.js": @@ -18205,10 +18433,10 @@ try { /***/ }), -/***/ "./client/src/js/App.vue": -/*!*******************************!*\ - !*** ./client/src/js/App.vue ***! - \*******************************/ +/***/ "./node_modules/vue-i18n/dist/vue-i18n.esm.js": +/*!****************************************************!*\ + !*** ./node_modules/vue-i18n/dist/vue-i18n.esm.js ***! + \****************************************************/ /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { "use strict"; @@ -18216,88 +18444,2390 @@ __webpack_require__.r(__webpack_exports__); /* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) /* harmony export */ }); -/* harmony import */ var _App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./App.vue?vue&type=template&id=29ae7a28& */ "./client/src/js/App.vue?vue&type=template&id=29ae7a28&"); -/* harmony import */ var _App_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./App.vue?vue&type=script&lang=js& */ "./client/src/js/App.vue?vue&type=script&lang=js&"); -/* harmony import */ var _App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./App.vue?vue&type=style&index=0&lang=less& */ "./client/src/js/App.vue?vue&type=style&index=0&lang=less&"); -/* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! !../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js"); +/*! + * vue-i18n v8.22.4 + * (c) 2021 kazuya kawaguchi + * Released under the MIT License. + */ +/* */ +/** + * constants + */ +var numberFormatKeys = [ + 'compactDisplay', + 'currency', + 'currencyDisplay', + 'currencySign', + 'localeMatcher', + 'notation', + 'numberingSystem', + 'signDisplay', + 'style', + 'unit', + 'unitDisplay', + 'useGrouping', + 'minimumIntegerDigits', + 'minimumFractionDigits', + 'maximumFractionDigits', + 'minimumSignificantDigits', + 'maximumSignificantDigits' +]; -; +/** + * utilities + */ +function warn (msg, err) { + if (typeof console !== 'undefined') { + console.warn('[vue-i18n] ' + msg); + /* istanbul ignore if */ + if (err) { + console.warn(err.stack); + } + } +} -/* normalize component */ +function error (msg, err) { + if (typeof console !== 'undefined') { + console.error('[vue-i18n] ' + msg); + /* istanbul ignore if */ + if (err) { + console.error(err.stack); + } + } +} -var component = (0,_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_3__.default)( - _App_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__.default, - _App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__.render, - _App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns, - false, - null, - null, - null - -) +var isArray = Array.isArray; -/* hot reload */ -if (false) { var api; } -component.options.__file = "client/src/js/App.vue" -/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (component.exports); +function isObject (obj) { + return obj !== null && typeof obj === 'object' +} -/***/ }), +function isBoolean (val) { + return typeof val === 'boolean' +} -/***/ "./client/src/js/App.vue?vue&type=script&lang=js&": -/*!********************************************************!*\ - !*** ./client/src/js/App.vue?vue&type=script&lang=js& ***! - \********************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { +function isString (val) { + return typeof val === 'string' +} -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) -/* harmony export */ }); -/* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_1_0_rules_0_use_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=script&lang=js& */ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-1[0].rules[0].use!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/App.vue?vue&type=script&lang=js&"); - /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_babel_loader_lib_index_js_clonedRuleSet_1_0_rules_0_use_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__.default); +var toString = Object.prototype.toString; +var OBJECT_STRING = '[object Object]'; +function isPlainObject (obj) { + return toString.call(obj) === OBJECT_STRING +} -/***/ }), +function isNull (val) { + return val === null || val === undefined +} -/***/ "./client/src/js/App.vue?vue&type=template&id=29ae7a28&": -/*!**************************************************************!*\ - !*** ./client/src/js/App.vue?vue&type=template&id=29ae7a28& ***! - \**************************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { +function isFunction (val) { + return typeof val === 'function' +} -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export */ __webpack_require__.d(__webpack_exports__, { -/* harmony export */ "render": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__.render), -/* harmony export */ "staticRenderFns": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns) -/* harmony export */ }); -/* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_template_id_29ae7a28___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=template&id=29ae7a28& */ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/App.vue?vue&type=template&id=29ae7a28&"); +function parseArgs () { + var args = [], len = arguments.length; + while ( len-- ) args[ len ] = arguments[ len ]; + var locale = null; + var params = null; + if (args.length === 1) { + if (isObject(args[0]) || isArray(args[0])) { + params = args[0]; + } else if (typeof args[0] === 'string') { + locale = args[0]; + } + } else if (args.length === 2) { + if (typeof args[0] === 'string') { + locale = args[0]; + } + /* istanbul ignore if */ + if (isObject(args[1]) || isArray(args[1])) { + params = args[1]; + } + } -/***/ }), + return { locale: locale, params: params } +} -/***/ "./client/src/js/App.vue?vue&type=style&index=0&lang=less&": -/*!*****************************************************************!*\ - !*** ./client/src/js/App.vue?vue&type=style&index=0&lang=less& ***! - \*****************************************************************/ -/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { +function looseClone (obj) { + return JSON.parse(JSON.stringify(obj)) +} -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var _node_modules_vue_style_loader_index_js_node_modules_mini_css_extract_plugin_dist_loader_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_less_loader_dist_cjs_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/vue-style-loader/index.js!../../../node_modules/mini-css-extract-plugin/dist/loader.js!../../../node_modules/css-loader/dist/cjs.js!../../../node_modules/vue-loader/lib/loaders/stylePostLoader.js!../../../node_modules/less-loader/dist/cjs.js!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./App.vue?vue&type=style&index=0&lang=less& */ "./node_modules/vue-style-loader/index.js!./node_modules/mini-css-extract-plugin/dist/loader.js!./node_modules/css-loader/dist/cjs.js!./node_modules/vue-loader/lib/loaders/stylePostLoader.js!./node_modules/less-loader/dist/cjs.js!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/App.vue?vue&type=style&index=0&lang=less&"); -/* harmony import */ var _node_modules_vue_style_loader_index_js_node_modules_mini_css_extract_plugin_dist_loader_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_less_loader_dist_cjs_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_node_modules_vue_style_loader_index_js_node_modules_mini_css_extract_plugin_dist_loader_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_less_loader_dist_cjs_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_0__); -/* harmony reexport (unknown) */ var __WEBPACK_REEXPORT_OBJECT__ = {}; -/* harmony reexport (unknown) */ for(const __WEBPACK_IMPORT_KEY__ in _node_modules_vue_style_loader_index_js_node_modules_mini_css_extract_plugin_dist_loader_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_less_loader_dist_cjs_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_0__) if(__WEBPACK_IMPORT_KEY__ !== "default") __WEBPACK_REEXPORT_OBJECT__[__WEBPACK_IMPORT_KEY__] = () => _node_modules_vue_style_loader_index_js_node_modules_mini_css_extract_plugin_dist_loader_js_node_modules_css_loader_dist_cjs_js_node_modules_vue_loader_lib_loaders_stylePostLoader_js_node_modules_less_loader_dist_cjs_js_node_modules_vue_loader_lib_index_js_vue_loader_options_App_vue_vue_type_style_index_0_lang_less___WEBPACK_IMPORTED_MODULE_0__[__WEBPACK_IMPORT_KEY__] -/* harmony reexport (unknown) */ __webpack_require__.d(__webpack_exports__, __WEBPACK_REEXPORT_OBJECT__); +function remove (arr, item) { + if (arr.length) { + var index = arr.indexOf(item); + if (index > -1) { + return arr.splice(index, 1) + } + } +} +function includes (arr, item) { + return !!~arr.indexOf(item) +} -/***/ }), +var hasOwnProperty = Object.prototype.hasOwnProperty; +function hasOwn (obj, key) { + return hasOwnProperty.call(obj, key) +} -/***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./client/src/js/App.vue?vue&type=template&id=29ae7a28&": -/*!*****************************************************************************************************************************************************************************************************!*\ +function merge (target) { + var arguments$1 = arguments; + + var output = Object(target); + for (var i = 1; i < arguments.length; i++) { + var source = arguments$1[i]; + if (source !== undefined && source !== null) { + var key = (void 0); + for (key in source) { + if (hasOwn(source, key)) { + if (isObject(source[key])) { + output[key] = merge(output[key], source[key]); + } else { + output[key] = source[key]; + } + } + } + } + } + return output +} + +function looseEqual (a, b) { + if (a === b) { return true } + var isObjectA = isObject(a); + var isObjectB = isObject(b); + if (isObjectA && isObjectB) { + try { + var isArrayA = isArray(a); + var isArrayB = isArray(b); + if (isArrayA && isArrayB) { + return a.length === b.length && a.every(function (e, i) { + return looseEqual(e, b[i]) + }) + } else if (!isArrayA && !isArrayB) { + var keysA = Object.keys(a); + var keysB = Object.keys(b); + return keysA.length === keysB.length && keysA.every(function (key) { + return looseEqual(a[key], b[key]) + }) + } else { + /* istanbul ignore next */ + return false + } + } catch (e) { + /* istanbul ignore next */ + return false + } + } else if (!isObjectA && !isObjectB) { + return String(a) === String(b) + } else { + return false + } +} + +/** + * Sanitizes html special characters from input strings. For mitigating risk of XSS attacks. + * @param rawText The raw input from the user that should be escaped. + */ +function escapeHtml(rawText) { + return rawText + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +/** + * Escapes html tags and special symbols from all provided params which were returned from parseArgs().params. + * This method performs an in-place operation on the params object. + * + * @param {any} params Parameters as provided from `parseArgs().params`. + * May be either an array of strings or a string->any map. + * + * @returns The manipulated `params` object. + */ +function escapeParams(params) { + if(params != null) { + Object.keys(params).forEach(function (key) { + if(typeof(params[key]) == 'string') { + params[key] = escapeHtml(params[key]); + } + }); + } + return params +} + +/* */ + +function extend (Vue) { + if (!Vue.prototype.hasOwnProperty('$i18n')) { + // $FlowFixMe + Object.defineProperty(Vue.prototype, '$i18n', { + get: function get () { return this._i18n } + }); + } + + Vue.prototype.$t = function (key) { + var values = [], len = arguments.length - 1; + while ( len-- > 0 ) values[ len ] = arguments[ len + 1 ]; + + var i18n = this.$i18n; + return i18n._t.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this ].concat( values )) + }; + + Vue.prototype.$tc = function (key, choice) { + var values = [], len = arguments.length - 2; + while ( len-- > 0 ) values[ len ] = arguments[ len + 2 ]; + + var i18n = this.$i18n; + return i18n._tc.apply(i18n, [ key, i18n.locale, i18n._getMessages(), this, choice ].concat( values )) + }; + + Vue.prototype.$te = function (key, locale) { + var i18n = this.$i18n; + return i18n._te(key, i18n.locale, i18n._getMessages(), locale) + }; + + Vue.prototype.$d = function (value) { + var ref; + + var args = [], len = arguments.length - 1; + while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ]; + return (ref = this.$i18n).d.apply(ref, [ value ].concat( args )) + }; + + Vue.prototype.$n = function (value) { + var ref; + + var args = [], len = arguments.length - 1; + while ( len-- > 0 ) args[ len ] = arguments[ len + 1 ]; + return (ref = this.$i18n).n.apply(ref, [ value ].concat( args )) + }; +} + +/* */ + +var mixin = { + beforeCreate: function beforeCreate () { + var options = this.$options; + options.i18n = options.i18n || (options.__i18n ? {} : null); + + if (options.i18n) { + if (options.i18n instanceof VueI18n) { + // init locale messages via custom blocks + if (options.__i18n) { + try { + var localeMessages = options.i18n && options.i18n.messages ? options.i18n.messages : {}; + options.__i18n.forEach(function (resource) { + localeMessages = merge(localeMessages, JSON.parse(resource)); + }); + Object.keys(localeMessages).forEach(function (locale) { + options.i18n.mergeLocaleMessage(locale, localeMessages[locale]); + }); + } catch (e) { + if (true) { + error("Cannot parse locale messages via custom blocks.", e); + } + } + } + this._i18n = options.i18n; + this._i18nWatcher = this._i18n.watchI18nData(); + } else if (isPlainObject(options.i18n)) { + var rootI18n = this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n + ? this.$root.$i18n + : null; + // component local i18n + if (rootI18n) { + options.i18n.root = this.$root; + options.i18n.formatter = rootI18n.formatter; + options.i18n.fallbackLocale = rootI18n.fallbackLocale; + options.i18n.formatFallbackMessages = rootI18n.formatFallbackMessages; + options.i18n.silentTranslationWarn = rootI18n.silentTranslationWarn; + options.i18n.silentFallbackWarn = rootI18n.silentFallbackWarn; + options.i18n.pluralizationRules = rootI18n.pluralizationRules; + options.i18n.preserveDirectiveContent = rootI18n.preserveDirectiveContent; + } + + // init locale messages via custom blocks + if (options.__i18n) { + try { + var localeMessages$1 = options.i18n && options.i18n.messages ? options.i18n.messages : {}; + options.__i18n.forEach(function (resource) { + localeMessages$1 = merge(localeMessages$1, JSON.parse(resource)); + }); + options.i18n.messages = localeMessages$1; + } catch (e) { + if (true) { + warn("Cannot parse locale messages via custom blocks.", e); + } + } + } + + var ref = options.i18n; + var sharedMessages = ref.sharedMessages; + if (sharedMessages && isPlainObject(sharedMessages)) { + options.i18n.messages = merge(options.i18n.messages, sharedMessages); + } + + this._i18n = new VueI18n(options.i18n); + this._i18nWatcher = this._i18n.watchI18nData(); + + if (options.i18n.sync === undefined || !!options.i18n.sync) { + this._localeWatcher = this.$i18n.watchLocale(); + } + + if (rootI18n) { + rootI18n.onComponentInstanceCreated(this._i18n); + } + } else { + if (true) { + warn("Cannot be interpreted 'i18n' option."); + } + } + } else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) { + // root i18n + this._i18n = this.$root.$i18n; + } else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) { + // parent i18n + this._i18n = options.parent.$i18n; + } + }, + + beforeMount: function beforeMount () { + var options = this.$options; + options.i18n = options.i18n || (options.__i18n ? {} : null); + + if (options.i18n) { + if (options.i18n instanceof VueI18n) { + // init locale messages via custom blocks + this._i18n.subscribeDataChanging(this); + this._subscribing = true; + } else if (isPlainObject(options.i18n)) { + this._i18n.subscribeDataChanging(this); + this._subscribing = true; + } else { + if (true) { + warn("Cannot be interpreted 'i18n' option."); + } + } + } else if (this.$root && this.$root.$i18n && this.$root.$i18n instanceof VueI18n) { + this._i18n.subscribeDataChanging(this); + this._subscribing = true; + } else if (options.parent && options.parent.$i18n && options.parent.$i18n instanceof VueI18n) { + this._i18n.subscribeDataChanging(this); + this._subscribing = true; + } + }, + + beforeDestroy: function beforeDestroy () { + if (!this._i18n) { return } + + var self = this; + this.$nextTick(function () { + if (self._subscribing) { + self._i18n.unsubscribeDataChanging(self); + delete self._subscribing; + } + + if (self._i18nWatcher) { + self._i18nWatcher(); + self._i18n.destroyVM(); + delete self._i18nWatcher; + } + + if (self._localeWatcher) { + self._localeWatcher(); + delete self._localeWatcher; + } + }); + } +}; + +/* */ + +var interpolationComponent = { + name: 'i18n', + functional: true, + props: { + tag: { + type: [String, Boolean, Object], + default: 'span' + }, + path: { + type: String, + required: true + }, + locale: { + type: String + }, + places: { + type: [Array, Object] + } + }, + render: function render (h, ref) { + var data = ref.data; + var parent = ref.parent; + var props = ref.props; + var slots = ref.slots; + + var $i18n = parent.$i18n; + if (!$i18n) { + if (true) { + warn('Cannot find VueI18n instance!'); + } + return + } + + var path = props.path; + var locale = props.locale; + var places = props.places; + var params = slots(); + var children = $i18n.i( + path, + locale, + onlyHasDefaultPlace(params) || places + ? useLegacyPlaces(params.default, places) + : params + ); + + var tag = (!!props.tag && props.tag !== true) || props.tag === false ? props.tag : 'span'; + return tag ? h(tag, data, children) : children + } +}; + +function onlyHasDefaultPlace (params) { + var prop; + for (prop in params) { + if (prop !== 'default') { return false } + } + return Boolean(prop) +} + +function useLegacyPlaces (children, places) { + var params = places ? createParamsFromPlaces(places) : {}; + + if (!children) { return params } + + // Filter empty text nodes + children = children.filter(function (child) { + return child.tag || child.text.trim() !== '' + }); + + var everyPlace = children.every(vnodeHasPlaceAttribute); + if ( true && everyPlace) { + warn('`place` attribute is deprecated in next major version. Please switch to Vue slots.'); + } + + return children.reduce( + everyPlace ? assignChildPlace : assignChildIndex, + params + ) +} + +function createParamsFromPlaces (places) { + if (true) { + warn('`places` prop is deprecated in next major version. Please switch to Vue slots.'); + } + + return Array.isArray(places) + ? places.reduce(assignChildIndex, {}) + : Object.assign({}, places) +} + +function assignChildPlace (params, child) { + if (child.data && child.data.attrs && child.data.attrs.place) { + params[child.data.attrs.place] = child; + } + return params +} + +function assignChildIndex (params, child, index) { + params[index] = child; + return params +} + +function vnodeHasPlaceAttribute (vnode) { + return Boolean(vnode.data && vnode.data.attrs && vnode.data.attrs.place) +} + +/* */ + +var numberComponent = { + name: 'i18n-n', + functional: true, + props: { + tag: { + type: [String, Boolean, Object], + default: 'span' + }, + value: { + type: Number, + required: true + }, + format: { + type: [String, Object] + }, + locale: { + type: String + } + }, + render: function render (h, ref) { + var props = ref.props; + var parent = ref.parent; + var data = ref.data; + + var i18n = parent.$i18n; + + if (!i18n) { + if (true) { + warn('Cannot find VueI18n instance!'); + } + return null + } + + var key = null; + var options = null; + + if (isString(props.format)) { + key = props.format; + } else if (isObject(props.format)) { + if (props.format.key) { + key = props.format.key; + } + + // Filter out number format options only + options = Object.keys(props.format).reduce(function (acc, prop) { + var obj; + + if (includes(numberFormatKeys, prop)) { + return Object.assign({}, acc, ( obj = {}, obj[prop] = props.format[prop], obj )) + } + return acc + }, null); + } + + var locale = props.locale || i18n.locale; + var parts = i18n._ntp(props.value, locale, key, options); + + var values = parts.map(function (part, index) { + var obj; + + var slot = data.scopedSlots && data.scopedSlots[part.type]; + return slot ? slot(( obj = {}, obj[part.type] = part.value, obj.index = index, obj.parts = parts, obj )) : part.value + }); + + var tag = (!!props.tag && props.tag !== true) || props.tag === false ? props.tag : 'span'; + return tag + ? h(tag, { + attrs: data.attrs, + 'class': data['class'], + staticClass: data.staticClass + }, values) + : values + } +}; + +/* */ + +function bind (el, binding, vnode) { + if (!assert(el, vnode)) { return } + + t(el, binding, vnode); +} + +function update (el, binding, vnode, oldVNode) { + if (!assert(el, vnode)) { return } + + var i18n = vnode.context.$i18n; + if (localeEqual(el, vnode) && + (looseEqual(binding.value, binding.oldValue) && + looseEqual(el._localeMessage, i18n.getLocaleMessage(i18n.locale)))) { return } + + t(el, binding, vnode); +} + +function unbind (el, binding, vnode, oldVNode) { + var vm = vnode.context; + if (!vm) { + warn('Vue instance does not exists in VNode context'); + return + } + + var i18n = vnode.context.$i18n || {}; + if (!binding.modifiers.preserve && !i18n.preserveDirectiveContent) { + el.textContent = ''; + } + el._vt = undefined; + delete el['_vt']; + el._locale = undefined; + delete el['_locale']; + el._localeMessage = undefined; + delete el['_localeMessage']; +} + +function assert (el, vnode) { + var vm = vnode.context; + if (!vm) { + warn('Vue instance does not exists in VNode context'); + return false + } + + if (!vm.$i18n) { + warn('VueI18n instance does not exists in Vue instance'); + return false + } + + return true +} + +function localeEqual (el, vnode) { + var vm = vnode.context; + return el._locale === vm.$i18n.locale +} + +function t (el, binding, vnode) { + var ref$1, ref$2; + + var value = binding.value; + + var ref = parseValue(value); + var path = ref.path; + var locale = ref.locale; + var args = ref.args; + var choice = ref.choice; + if (!path && !locale && !args) { + warn('value type not supported'); + return + } + + if (!path) { + warn('`path` is required in v-t directive'); + return + } + + var vm = vnode.context; + if (choice != null) { + el._vt = el.textContent = (ref$1 = vm.$i18n).tc.apply(ref$1, [ path, choice ].concat( makeParams(locale, args) )); + } else { + el._vt = el.textContent = (ref$2 = vm.$i18n).t.apply(ref$2, [ path ].concat( makeParams(locale, args) )); + } + el._locale = vm.$i18n.locale; + el._localeMessage = vm.$i18n.getLocaleMessage(vm.$i18n.locale); +} + +function parseValue (value) { + var path; + var locale; + var args; + var choice; + + if (isString(value)) { + path = value; + } else if (isPlainObject(value)) { + path = value.path; + locale = value.locale; + args = value.args; + choice = value.choice; + } + + return { path: path, locale: locale, args: args, choice: choice } +} + +function makeParams (locale, args) { + var params = []; + + locale && params.push(locale); + if (args && (Array.isArray(args) || isPlainObject(args))) { + params.push(args); + } + + return params +} + +var Vue; + +function install (_Vue) { + /* istanbul ignore if */ + if ( true && install.installed && _Vue === Vue) { + warn('already installed.'); + return + } + install.installed = true; + + Vue = _Vue; + + var version = (Vue.version && Number(Vue.version.split('.')[0])) || -1; + /* istanbul ignore if */ + if ( true && version < 2) { + warn(("vue-i18n (" + (install.version) + ") need to use Vue 2.0 or later (Vue: " + (Vue.version) + ").")); + return + } + + extend(Vue); + Vue.mixin(mixin); + Vue.directive('t', { bind: bind, update: update, unbind: unbind }); + Vue.component(interpolationComponent.name, interpolationComponent); + Vue.component(numberComponent.name, numberComponent); + + // use simple mergeStrategies to prevent i18n instance lose '__proto__' + var strats = Vue.config.optionMergeStrategies; + strats.i18n = function (parentVal, childVal) { + return childVal === undefined + ? parentVal + : childVal + }; +} + +/* */ + +var BaseFormatter = function BaseFormatter () { + this._caches = Object.create(null); +}; + +BaseFormatter.prototype.interpolate = function interpolate (message, values) { + if (!values) { + return [message] + } + var tokens = this._caches[message]; + if (!tokens) { + tokens = parse(message); + this._caches[message] = tokens; + } + return compile(tokens, values) +}; + + + +var RE_TOKEN_LIST_VALUE = /^(?:\d)+/; +var RE_TOKEN_NAMED_VALUE = /^(?:\w)+/; + +function parse (format) { + var tokens = []; + var position = 0; + + var text = ''; + while (position < format.length) { + var char = format[position++]; + if (char === '{') { + if (text) { + tokens.push({ type: 'text', value: text }); + } + + text = ''; + var sub = ''; + char = format[position++]; + while (char !== undefined && char !== '}') { + sub += char; + char = format[position++]; + } + var isClosed = char === '}'; + + var type = RE_TOKEN_LIST_VALUE.test(sub) + ? 'list' + : isClosed && RE_TOKEN_NAMED_VALUE.test(sub) + ? 'named' + : 'unknown'; + tokens.push({ value: sub, type: type }); + } else if (char === '%') { + // when found rails i18n syntax, skip text capture + if (format[(position)] !== '{') { + text += char; + } + } else { + text += char; + } + } + + text && tokens.push({ type: 'text', value: text }); + + return tokens +} + +function compile (tokens, values) { + var compiled = []; + var index = 0; + + var mode = Array.isArray(values) + ? 'list' + : isObject(values) + ? 'named' + : 'unknown'; + if (mode === 'unknown') { return compiled } + + while (index < tokens.length) { + var token = tokens[index]; + switch (token.type) { + case 'text': + compiled.push(token.value); + break + case 'list': + compiled.push(values[parseInt(token.value, 10)]); + break + case 'named': + if (mode === 'named') { + compiled.push((values)[token.value]); + } else { + if (true) { + warn(("Type of token '" + (token.type) + "' and format of value '" + mode + "' don't match!")); + } + } + break + case 'unknown': + if (true) { + warn("Detect 'unknown' type of token!"); + } + break + } + index++; + } + + return compiled +} + +/* */ + +/** + * Path parser + * - Inspired: + * Vue.js Path parser + */ + +// actions +var APPEND = 0; +var PUSH = 1; +var INC_SUB_PATH_DEPTH = 2; +var PUSH_SUB_PATH = 3; + +// states +var BEFORE_PATH = 0; +var IN_PATH = 1; +var BEFORE_IDENT = 2; +var IN_IDENT = 3; +var IN_SUB_PATH = 4; +var IN_SINGLE_QUOTE = 5; +var IN_DOUBLE_QUOTE = 6; +var AFTER_PATH = 7; +var ERROR = 8; + +var pathStateMachine = []; + +pathStateMachine[BEFORE_PATH] = { + 'ws': [BEFORE_PATH], + 'ident': [IN_IDENT, APPEND], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] +}; + +pathStateMachine[IN_PATH] = { + 'ws': [IN_PATH], + '.': [BEFORE_IDENT], + '[': [IN_SUB_PATH], + 'eof': [AFTER_PATH] +}; + +pathStateMachine[BEFORE_IDENT] = { + 'ws': [BEFORE_IDENT], + 'ident': [IN_IDENT, APPEND], + '0': [IN_IDENT, APPEND], + 'number': [IN_IDENT, APPEND] +}; + +pathStateMachine[IN_IDENT] = { + 'ident': [IN_IDENT, APPEND], + '0': [IN_IDENT, APPEND], + 'number': [IN_IDENT, APPEND], + 'ws': [IN_PATH, PUSH], + '.': [BEFORE_IDENT, PUSH], + '[': [IN_SUB_PATH, PUSH], + 'eof': [AFTER_PATH, PUSH] +}; + +pathStateMachine[IN_SUB_PATH] = { + "'": [IN_SINGLE_QUOTE, APPEND], + '"': [IN_DOUBLE_QUOTE, APPEND], + '[': [IN_SUB_PATH, INC_SUB_PATH_DEPTH], + ']': [IN_PATH, PUSH_SUB_PATH], + 'eof': ERROR, + 'else': [IN_SUB_PATH, APPEND] +}; + +pathStateMachine[IN_SINGLE_QUOTE] = { + "'": [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_SINGLE_QUOTE, APPEND] +}; + +pathStateMachine[IN_DOUBLE_QUOTE] = { + '"': [IN_SUB_PATH, APPEND], + 'eof': ERROR, + 'else': [IN_DOUBLE_QUOTE, APPEND] +}; + +/** + * Check if an expression is a literal value. + */ + +var literalValueRE = /^\s?(?:true|false|-?[\d.]+|'[^']*'|"[^"]*")\s?$/; +function isLiteral (exp) { + return literalValueRE.test(exp) +} + +/** + * Strip quotes from a string + */ + +function stripQuotes (str) { + var a = str.charCodeAt(0); + var b = str.charCodeAt(str.length - 1); + return a === b && (a === 0x22 || a === 0x27) + ? str.slice(1, -1) + : str +} + +/** + * Determine the type of a character in a keypath. + */ + +function getPathCharType (ch) { + if (ch === undefined || ch === null) { return 'eof' } + + var code = ch.charCodeAt(0); + + switch (code) { + case 0x5B: // [ + case 0x5D: // ] + case 0x2E: // . + case 0x22: // " + case 0x27: // ' + return ch + + case 0x5F: // _ + case 0x24: // $ + case 0x2D: // - + return 'ident' + + case 0x09: // Tab + case 0x0A: // Newline + case 0x0D: // Return + case 0xA0: // No-break space + case 0xFEFF: // Byte Order Mark + case 0x2028: // Line Separator + case 0x2029: // Paragraph Separator + return 'ws' + } + + return 'ident' +} + +/** + * Format a subPath, return its plain form if it is + * a literal string or number. Otherwise prepend the + * dynamic indicator (*). + */ + +function formatSubPath (path) { + var trimmed = path.trim(); + // invalid leading 0 + if (path.charAt(0) === '0' && isNaN(path)) { return false } + + return isLiteral(trimmed) ? stripQuotes(trimmed) : '*' + trimmed +} + +/** + * Parse a string path into an array of segments + */ + +function parse$1 (path) { + var keys = []; + var index = -1; + var mode = BEFORE_PATH; + var subPathDepth = 0; + var c; + var key; + var newChar; + var type; + var transition; + var action; + var typeMap; + var actions = []; + + actions[PUSH] = function () { + if (key !== undefined) { + keys.push(key); + key = undefined; + } + }; + + actions[APPEND] = function () { + if (key === undefined) { + key = newChar; + } else { + key += newChar; + } + }; + + actions[INC_SUB_PATH_DEPTH] = function () { + actions[APPEND](); + subPathDepth++; + }; + + actions[PUSH_SUB_PATH] = function () { + if (subPathDepth > 0) { + subPathDepth--; + mode = IN_SUB_PATH; + actions[APPEND](); + } else { + subPathDepth = 0; + if (key === undefined) { return false } + key = formatSubPath(key); + if (key === false) { + return false + } else { + actions[PUSH](); + } + } + }; + + function maybeUnescapeQuote () { + var nextChar = path[index + 1]; + if ((mode === IN_SINGLE_QUOTE && nextChar === "'") || + (mode === IN_DOUBLE_QUOTE && nextChar === '"')) { + index++; + newChar = '\\' + nextChar; + actions[APPEND](); + return true + } + } + + while (mode !== null) { + index++; + c = path[index]; + + if (c === '\\' && maybeUnescapeQuote()) { + continue + } + + type = getPathCharType(c); + typeMap = pathStateMachine[mode]; + transition = typeMap[type] || typeMap['else'] || ERROR; + + if (transition === ERROR) { + return // parse error + } + + mode = transition[0]; + action = actions[transition[1]]; + if (action) { + newChar = transition[2]; + newChar = newChar === undefined + ? c + : newChar; + if (action() === false) { + return + } + } + + if (mode === AFTER_PATH) { + return keys + } + } +} + + + + + +var I18nPath = function I18nPath () { + this._cache = Object.create(null); +}; + +/** + * External parse that check for a cache hit first + */ +I18nPath.prototype.parsePath = function parsePath (path) { + var hit = this._cache[path]; + if (!hit) { + hit = parse$1(path); + if (hit) { + this._cache[path] = hit; + } + } + return hit || [] +}; + +/** + * Get path value from path string + */ +I18nPath.prototype.getPathValue = function getPathValue (obj, path) { + if (!isObject(obj)) { return null } + + var paths = this.parsePath(path); + if (paths.length === 0) { + return null + } else { + var length = paths.length; + var last = obj; + var i = 0; + while (i < length) { + var value = last[paths[i]]; + if (value === undefined) { + return null + } + last = value; + i++; + } + + return last + } +}; + +/* */ + + + +var htmlTagMatcher = /<\/?[\w\s="/.':;#-\/]+>/; +var linkKeyMatcher = /(?:@(?:\.[a-z]+)?:(?:[\w\-_|.]+|\([\w\-_|.]+\)))/g; +var linkKeyPrefixMatcher = /^@(?:\.([a-z]+))?:/; +var bracketsMatcher = /[()]/g; +var defaultModifiers = { + 'upper': function (str) { return str.toLocaleUpperCase(); }, + 'lower': function (str) { return str.toLocaleLowerCase(); }, + 'capitalize': function (str) { return ("" + (str.charAt(0).toLocaleUpperCase()) + (str.substr(1))); } +}; + +var defaultFormatter = new BaseFormatter(); + +var VueI18n = function VueI18n (options) { + var this$1 = this; + if ( options === void 0 ) options = {}; + + // Auto install if it is not done yet and `window` has `Vue`. + // To allow users to avoid auto-installation in some cases, + // this code should be placed here. See #290 + /* istanbul ignore if */ + if (!Vue && typeof window !== 'undefined' && window.Vue) { + install(window.Vue); + } + + var locale = options.locale || 'en-US'; + var fallbackLocale = options.fallbackLocale === false + ? false + : options.fallbackLocale || 'en-US'; + var messages = options.messages || {}; + var dateTimeFormats = options.dateTimeFormats || {}; + var numberFormats = options.numberFormats || {}; + + this._vm = null; + this._formatter = options.formatter || defaultFormatter; + this._modifiers = options.modifiers || {}; + this._missing = options.missing || null; + this._root = options.root || null; + this._sync = options.sync === undefined ? true : !!options.sync; + this._fallbackRoot = options.fallbackRoot === undefined + ? true + : !!options.fallbackRoot; + this._formatFallbackMessages = options.formatFallbackMessages === undefined + ? false + : !!options.formatFallbackMessages; + this._silentTranslationWarn = options.silentTranslationWarn === undefined + ? false + : options.silentTranslationWarn; + this._silentFallbackWarn = options.silentFallbackWarn === undefined + ? false + : !!options.silentFallbackWarn; + this._dateTimeFormatters = {}; + this._numberFormatters = {}; + this._path = new I18nPath(); + this._dataListeners = []; + this._componentInstanceCreatedListener = options.componentInstanceCreatedListener || null; + this._preserveDirectiveContent = options.preserveDirectiveContent === undefined + ? false + : !!options.preserveDirectiveContent; + this.pluralizationRules = options.pluralizationRules || {}; + this._warnHtmlInMessage = options.warnHtmlInMessage || 'off'; + this._postTranslation = options.postTranslation || null; + this._escapeParameterHtml = options.escapeParameterHtml || false; + + /** + * @param choice {number} a choice index given by the input to $tc: `$tc('path.to.rule', choiceIndex)` + * @param choicesLength {number} an overall amount of available choices + * @returns a final choice index + */ + this.getChoiceIndex = function (choice, choicesLength) { + var thisPrototype = Object.getPrototypeOf(this$1); + if (thisPrototype && thisPrototype.getChoiceIndex) { + var prototypeGetChoiceIndex = (thisPrototype.getChoiceIndex); + return (prototypeGetChoiceIndex).call(this$1, choice, choicesLength) + } + + // Default (old) getChoiceIndex implementation - english-compatible + var defaultImpl = function (_choice, _choicesLength) { + _choice = Math.abs(_choice); + + if (_choicesLength === 2) { + return _choice + ? _choice > 1 + ? 1 + : 0 + : 1 + } + + return _choice ? Math.min(_choice, 2) : 0 + }; + + if (this$1.locale in this$1.pluralizationRules) { + return this$1.pluralizationRules[this$1.locale].apply(this$1, [choice, choicesLength]) + } else { + return defaultImpl(choice, choicesLength) + } + }; + + + this._exist = function (message, key) { + if (!message || !key) { return false } + if (!isNull(this$1._path.getPathValue(message, key))) { return true } + // fallback for flat key + if (message[key]) { return true } + return false + }; + + if (this._warnHtmlInMessage === 'warn' || this._warnHtmlInMessage === 'error') { + Object.keys(messages).forEach(function (locale) { + this$1._checkLocaleMessage(locale, this$1._warnHtmlInMessage, messages[locale]); + }); + } + + this._initVM({ + locale: locale, + fallbackLocale: fallbackLocale, + messages: messages, + dateTimeFormats: dateTimeFormats, + numberFormats: numberFormats + }); +}; + +var prototypeAccessors = { vm: { configurable: true },messages: { configurable: true },dateTimeFormats: { configurable: true },numberFormats: { configurable: true },availableLocales: { configurable: true },locale: { configurable: true },fallbackLocale: { configurable: true },formatFallbackMessages: { configurable: true },missing: { configurable: true },formatter: { configurable: true },silentTranslationWarn: { configurable: true },silentFallbackWarn: { configurable: true },preserveDirectiveContent: { configurable: true },warnHtmlInMessage: { configurable: true },postTranslation: { configurable: true } }; + +VueI18n.prototype._checkLocaleMessage = function _checkLocaleMessage (locale, level, message) { + var paths = []; + + var fn = function (level, locale, message, paths) { + if (isPlainObject(message)) { + Object.keys(message).forEach(function (key) { + var val = message[key]; + if (isPlainObject(val)) { + paths.push(key); + paths.push('.'); + fn(level, locale, val, paths); + paths.pop(); + paths.pop(); + } else { + paths.push(key); + fn(level, locale, val, paths); + paths.pop(); + } + }); + } else if (isArray(message)) { + message.forEach(function (item, index) { + if (isPlainObject(item)) { + paths.push(("[" + index + "]")); + paths.push('.'); + fn(level, locale, item, paths); + paths.pop(); + paths.pop(); + } else { + paths.push(("[" + index + "]")); + fn(level, locale, item, paths); + paths.pop(); + } + }); + } else if (isString(message)) { + var ret = htmlTagMatcher.test(message); + if (ret) { + var msg = "Detected HTML in message '" + message + "' of keypath '" + (paths.join('')) + "' at '" + locale + "'. Consider component interpolation with '{charCount} Zeichen werden mit Google Cloud Translation übersetzt.
Der aktuelle Inhalt des Feldes ({targetLocale}) wird dabei überschrieben.
" + } +} diff --git a/client/src/lang/en.json b/client/src/lang/en.json new file mode 100644 index 0000000..96ffed3 --- /dev/null +++ b/client/src/lang/en.json @@ -0,0 +1,13 @@ +{ + "field": { + "translateCta": "Translate from {sourceLocale} to {targetLocale}" + }, + "modal": { + "translateCta": "Translate now", + "headline": "Translate from {sourceLocale} to {targetLocale}", + "sourceValueLabel": "{locale} value" + }, + "google": { + "pricingHint": "{charCount} characters will be translated using Google Cloud Translation.
The current content of the field ({targetLocale}) will be overwritten.
" + } +} diff --git a/client/src/styles/vars.less b/client/src/styles/vars.less new file mode 100644 index 0000000..e14e945 --- /dev/null +++ b/client/src/styles/vars.less @@ -0,0 +1,10 @@ +@space-1: 4px; +@space-2: 8px; +@space-3: 16px; +@space-4: 24px; + +@color-mono-100: #fff; +@color-mono-94: #f0f0f0; +@color-mono-0: #000; +@color-text: #43536D; +@color-cta: #0071c4; diff --git a/composer.json b/composer.json index 6f57d85..a1c2b04 100644 --- a/composer.json +++ b/composer.json @@ -21,6 +21,7 @@ "require": { "php": ">=7.1", "ext-json": "*", + "ext-intl": "*", "silverstripe/framework": "^4" }, "extra": { diff --git a/package.json b/package.json index fff6bff..0fa3d1a 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "core-js": "^3.8.3", "qs": "^6.9.6", "regenerator-runtime": "^0.13.7", - "vue": "^2.6.12" + "vue": "^2.6.12", + "vue-i18n": "^8.22.4" } } diff --git a/src/AutotranslateFieldExtension.php b/src/AutotranslateFieldExtension.php index e3532a8..01f4dc2 100644 --- a/src/AutotranslateFieldExtension.php +++ b/src/AutotranslateFieldExtension.php @@ -9,8 +9,11 @@ use SilverStripe\Core\Extension; use SilverStripe\Forms\FormField; use SilverStripe\Forms\GridField\GridFieldDetailForm_ItemRequest; +use SilverStripe\i18n\Data\Intl\IntlLocales; +use SilverStripe\i18n\i18n; use SilverStripe\ORM\DataObject; use SilverStripe\View\Requirements; +use TractorCow\Fluent\Extension\FluentExtension; use TractorCow\Fluent\Model\Locale; use TractorCow\Fluent\State\FluentState; @@ -18,6 +21,9 @@ class AutotranslateFieldExtension extends Extension { use Configurable; + /** @var string */ + public const TRANSLATION_PROVIDER_GOOGLE = 'google'; + /** * @var DataObject|null The affected DataObject record */ @@ -56,6 +62,10 @@ private function isAutotranslateAvailableForField(): bool return false; } + if (!$record->hasExtension(FluentExtension::class)) { + return false; + } + return $this->isAutotranslateLocalizedField($this->owner->Name); } @@ -126,14 +136,14 @@ public function getAutotranslateFieldID(): string /** * @param bool $shortCodeOnly - * @return string The target locale + * @return string|Locale The target locale */ - private function getAutotranslateTargetLocale($shortCodeOnly = true): string + private function getAutotranslateTargetLocale($shortCodeOnly = false) { - $locale = Locale::getCurrentLocale()->Locale; + $locale = Locale::getCurrentLocale(); if ($shortCodeOnly) { - $bits = explode('_', $locale); + $bits = explode('_', $locale->Locale); return array_shift($bits); } @@ -142,20 +152,31 @@ private function getAutotranslateTargetLocale($shortCodeOnly = true): string /** * @param bool $shortCodeOnly - * @return string The source locale + * @return string|Locale The source locale */ - private function getAutotranslateSourceLocale($shortCodeOnly = true): string + private function getAutotranslateSourceLocale($shortCodeOnly = false) { - $locale = Locale::getDefault(Locale::getCurrentLocale()->Domain())->Locale; + $locale = Locale::getDefault(Locale::getCurrentLocale()->Domain()); if ($shortCodeOnly) { - $bits = explode('_', $locale); + $bits = explode('_', $locale->Locale); return array_shift($bits); } return $locale; } + /** + * Get the current cms locale as short language code (e.g. "en"). + * @return string + */ + private function getCMSLocaleForAutotranslateField(): string + { + $bits = explode('_', i18n::get_locale()); + + return array_shift($bits); + } + /** * Get the affected DataObject record. * @@ -186,6 +207,10 @@ private function getAutotranslateRecord(): ?DataObject */ private function fetchAutoTranslateRecord(): ?DataObject { + if (!$this->owner || !$this->owner->hasMethod('getForm') || !$this->owner->getForm()) { + return null; + } + $form = $this->owner->getForm(); $record = null; @@ -215,7 +240,7 @@ private function getAutotranslateSourceValue(): ?string FluentState::singleton() ->withState( function ($state) use ($context, $record, &$sourceValue) { - $state->setLocale($this->getAutotranslateSourceLocale(false)); + $state->setLocale($this->getAutotranslateSourceLocale()->Locale); $sourceRecord = DataObject::get(get_class($record))->byID($record->ID); $sourceValue = $sourceRecord->{$context->owner->Name}; } @@ -227,6 +252,45 @@ function ($state) use ($context, $record, &$sourceValue) { return $this->sourceValue; } + /** + * Get the active translation provider. + * + * @return string + */ + private function getTranslationProvider(): string + { + return self::TRANSLATION_PROVIDER_GOOGLE; + } + + /** + * Get the translation provider dependent config. + * + * @return array|null + */ + private function getConfigForTranslationProvider(): ?array + { + if ($this->getTranslationProvider() === self::TRANSLATION_PROVIDER_GOOGLE) { + return [ + 'apiEndpoint' => self::config()->get('google_translate_api'), + 'getVars' => [ + 'key' => self::config()->get('google_cloud_translation_api_key') + ], + ]; + } + + return null; + } + + /** + * @return string The field title / label. + */ + private function getAutotranslateFieldTitle(): string + { + $title = $this->owner->Title(); + + return method_exists($title, 'forTemplate') ? $title->forTemplate() : $title; + } + /** * Get the payload passed to the vue component. * @@ -235,16 +299,27 @@ function ($state) use ($context, $record, &$sourceValue) { public function getAutotranslateFieldPayload(): ?string { if ($this->isAutotranslateActionAvailable()) { + $targetLocale = $this->getAutotranslateTargetLocale(); + $sourceLocale = $this->getAutotranslateSourceLocale(); + return json_encode( [ - 'id' => $this->owner->ID(), - 'getVars' => [ - 'key' => self::config()->get('google_cloud_translation_api_key') + 'id' => $this->owner->ID(), + 'fieldTitle' => $this->getAutotranslateFieldTitle(), + 'cmsLocale' => $this->getCMSLocaleForAutotranslateField(), + 'targetLocale' => [ + 'code' => $this->getAutotranslateTargetLocale(true), + 'locale' => $targetLocale->Locale, + 'title' => IntlLocales::singleton()->languageName($targetLocale->Locale) + ], + 'sourceLocale' => [ + 'code' => $this->getAutotranslateSourceLocale(true), + 'locale' => $sourceLocale->Locale, + 'title' => IntlLocales::singleton()->languageName($sourceLocale->Locale) ], - 'targetLocale' => $this->getAutotranslateTargetLocale(), - 'sourceLocale' => $this->getAutotranslateSourceLocale(), - 'sourceValue' => $this->getAutotranslateSourceValue(), - 'googleTranslateApi' => self::config()->get('google_translate_api') + 'sourceValue' => $this->getAutotranslateSourceValue(), + 'provider' => $this->getTranslationProvider(), + 'providerConfig' => $this->getConfigForTranslationProvider() ] ); }