--- /dev/null
+.DS_Store
+.idea
+*.log
+/vendor
--- /dev/null
+{
+ "name": "cubedesigners/wp-translate",
+ "description": "WordPress translation tools",
+ "type": "wordpress-plugin",
+ "require": {
+ "atomastic/arrays": "^2.2",
+ "phpoffice/phpspreadsheet": "^1.17",
+ "jfcherng/php-diff": "^6.10"
+ },
+ "autoload": {
+ "psr-4": {
+ "CubeTranslate\\": "./src"
+ }
+ }
+}
--- /dev/null
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "a031b3dd0741b3f1beb0710e0d939746",
+ "packages": [
+ {
+ "name": "atomastic/arrays",
+ "version": "v2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/atomastic/arrays.git",
+ "reference": "77b60ad6abdfdd61730c42a336ddb5332d15f82a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/atomastic/arrays/zipball/77b60ad6abdfdd61730c42a336ddb5332d15f82a",
+ "reference": "77b60ad6abdfdd61730c42a336ddb5332d15f82a",
+ "shasum": ""
+ },
+ "require": {
+ "atomastic/macroable": "^1.0",
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "8.1.0",
+ "pestphp/pest": "^0.3.3",
+ "phpstan/phpstan": "^0.12.42"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Atomastic\\Arrays\\": "src/"
+ },
+ "files": [
+ "src/helpers.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sergey Romanenko",
+ "email": "sergey.romanenko@flextype.org",
+ "homepage": "https://github.com/Awilum"
+ }
+ ],
+ "description": "Arrays Component provide a fluent, object-oriented interface for working with arrays, allowing you to chain multiple arrays operations together using a more readable syntax compared to traditional PHP arrays functions.",
+ "keywords": [
+ "arrays",
+ "atomastic",
+ "php",
+ "php-arrays"
+ ],
+ "support": {
+ "issues": "https://github.com/atomastic/arrays/issues",
+ "source": "https://github.com/atomastic/arrays"
+ },
+ "time": "2020-12-10T13:47:43+00:00"
+ },
+ {
+ "name": "atomastic/macroable",
+ "version": "v1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/atomastic/macroable.git",
+ "reference": "f614e5908792ddcc1342949165f6b004f13e2edb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/atomastic/macroable/zipball/f614e5908792ddcc1342949165f6b004f13e2edb",
+ "reference": "f614e5908792ddcc1342949165f6b004f13e2edb",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "8.1.0",
+ "pestphp/pest": "^0.3.3",
+ "phpstan/phpstan": "^0.12.42"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Atomastic\\Macroable\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Sergey Romanenko",
+ "email": "sergey.romanenko@flextype.org",
+ "homepage": "https://github.com/Awilum"
+ }
+ ],
+ "description": "Macroable Component is a trait that, gives you the ability in effect to add new methods to a class at runtime.",
+ "keywords": [
+ "atomastic",
+ "macroable",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/atomastic/macroable/issues",
+ "source": "https://github.com/atomastic/macroable"
+ },
+ "time": "2020-12-05T08:19:25+00:00"
+ },
+ {
+ "name": "ezyang/htmlpurifier",
+ "version": "v4.13.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ezyang/htmlpurifier.git",
+ "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ezyang/htmlpurifier/zipball/08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+ "reference": "08e27c97e4c6ed02f37c5b2b20488046c8d90d75",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.2"
+ },
+ "require-dev": {
+ "simpletest/simpletest": "dev-master#72de02a7b80c6bb8864ef9bf66d41d2f58f826bd"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-0": {
+ "HTMLPurifier": "library/"
+ },
+ "files": [
+ "library/HTMLPurifier.composer.php"
+ ],
+ "exclude-from-classmap": [
+ "/library/HTMLPurifier/Language/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Edward Z. Yang",
+ "email": "admin@htmlpurifier.org",
+ "homepage": "http://ezyang.com"
+ }
+ ],
+ "description": "Standards compliant HTML filter written in PHP",
+ "homepage": "http://htmlpurifier.org/",
+ "keywords": [
+ "html"
+ ],
+ "support": {
+ "issues": "https://github.com/ezyang/htmlpurifier/issues",
+ "source": "https://github.com/ezyang/htmlpurifier/tree/master"
+ },
+ "time": "2020-06-29T00:56:53+00:00"
+ },
+ {
+ "name": "jfcherng/php-color-output",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jfcherng/php-color-output.git",
+ "reference": "2673074597eca9682d2fdfaee39a22418d4cc2f6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jfcherng/php-color-output/zipball/2673074597eca9682d2fdfaee39a22418d4cc2f6",
+ "reference": "2673074597eca9682d2fdfaee39a22418d4cc2f6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.15",
+ "phan/phan": "^2.2",
+ "phpunit/phpunit": "^7.2 || ^8.2 || ^9",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Jfcherng\\Utility\\": "src/"
+ },
+ "files": [
+ "src/helpers.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jack Cherng",
+ "email": "jfcherng@gmail.com"
+ }
+ ],
+ "description": "Make your PHP command-line application colorful.",
+ "keywords": [
+ "ansi-colors",
+ "color",
+ "command-line",
+ "str-color"
+ ],
+ "support": {
+ "issues": "https://github.com/jfcherng/php-color-output/issues",
+ "source": "https://github.com/jfcherng/php-color-output/tree/2.0.2"
+ },
+ "time": "2020-05-27T19:24:44+00:00"
+ },
+ {
+ "name": "jfcherng/php-diff",
+ "version": "6.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jfcherng/php-diff.git",
+ "reference": "808f042a3dc97e1070d9e1d825bf596cd9d020b4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jfcherng/php-diff/zipball/808f042a3dc97e1070d9e1d825bf596cd9d020b4",
+ "reference": "808f042a3dc97e1070d9e1d825bf596cd9d020b4",
+ "shasum": ""
+ },
+ "require": {
+ "jfcherng/php-color-output": "^2.0",
+ "jfcherng/php-mb-string": "^1.3",
+ "jfcherng/php-sequence-matcher": "^3.2.5",
+ "php": ">=7.1.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.18",
+ "liip/rmt": "^1.6",
+ "phan/phan": "^2.5 || ^3 || ^4",
+ "phpunit/phpunit": ">=7 <10",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Jfcherng\\Diff\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jack Cherng",
+ "email": "jfcherng@gmail.com"
+ },
+ {
+ "name": "Chris Boulton",
+ "email": "chris.boulton@interspire.com"
+ }
+ ],
+ "description": "A comprehensive library for generating differences between two strings in multiple formats (unified, side by side HTML etc).",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/jfcherng/php-diff/issues",
+ "source": "https://github.com/jfcherng/php-diff/tree/6.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/jfcherng/5usd",
+ "type": "custom"
+ }
+ ],
+ "time": "2021-03-19T09:20:25+00:00"
+ },
+ {
+ "name": "jfcherng/php-mb-string",
+ "version": "1.4.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jfcherng/php-mb-string.git",
+ "reference": "ff8dacb993d83b5e8e2d8e325b5a015f3fb2da7d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jfcherng/php-mb-string/zipball/ff8dacb993d83b5e8e2d8e325b5a015f3fb2da7d",
+ "reference": "ff8dacb993d83b5e8e2d8e325b5a015f3fb2da7d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.1.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.17",
+ "phan/phan": "^2 || ^3 || ^4",
+ "phpunit/phpunit": "^7.2 || ^8 || ^9"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Jfcherng\\Utility\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jack Cherng",
+ "email": "jfcherng@gmail.com"
+ }
+ ],
+ "description": "A high performance multibytes sting implementation for frequently reading/writing operations.",
+ "support": {
+ "issues": "https://github.com/jfcherng/php-mb-string/issues",
+ "source": "https://github.com/jfcherng/php-mb-string/tree/1.4.3"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/jfcherng/5usd",
+ "type": "custom"
+ }
+ ],
+ "time": "2021-01-16T11:51:16+00:00"
+ },
+ {
+ "name": "jfcherng/php-sequence-matcher",
+ "version": "3.2.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jfcherng/php-sequence-matcher.git",
+ "reference": "369933b7cfbc31979fd94bf6391451468f49c693"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jfcherng/php-sequence-matcher/zipball/369933b7cfbc31979fd94bf6391451468f49c693",
+ "reference": "369933b7cfbc31979fd94bf6391451468f49c693",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.3"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^2.16",
+ "phan/phan": "^2 || ^3",
+ "phpunit/phpunit": ">=7 <10",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Jfcherng\\Diff\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Jack Cherng",
+ "email": "jfcherng@gmail.com"
+ },
+ {
+ "name": "Chris Boulton",
+ "email": "chris.boulton@interspire.com"
+ }
+ ],
+ "description": "A longest sequence matcher. The logic is primarily based on the Python difflib package.",
+ "support": {
+ "issues": "https://github.com/jfcherng/php-sequence-matcher/issues",
+ "source": "https://github.com/jfcherng/php-sequence-matcher/tree/master"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/jfcherng/5usd",
+ "type": "custom"
+ }
+ ],
+ "time": "2020-09-03T13:48:27+00:00"
+ },
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/c4c5803cc1f93df3d2448478ef79394a5981cc58",
+ "reference": "c4c5803cc1f93df3d2448478ef79394a5981cc58",
+ "shasum": ""
+ },
+ "require": {
+ "myclabs/php-enum": "^1.5",
+ "php": ">= 7.1",
+ "psr/http-message": "^1.0",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "require-dev": {
+ "ext-zip": "*",
+ "guzzlehttp/guzzle": ">= 6.3",
+ "mikey179/vfsstream": "^1.6",
+ "phpunit/phpunit": ">= 7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/master"
+ },
+ "funding": [
+ {
+ "url": "https://opencollective.com/zipstream",
+ "type": "open_collective"
+ }
+ ],
+ "time": "2020-05-30T13:11:16+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "9999f1432fae467bc93c53f357105b4c31bb994c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/9999f1432fae467bc93c53f357105b4c31bb994c",
+ "reference": "9999f1432fae467bc93c53f357105b4c31bb994c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ },
+ "files": [
+ "classes/src/functions/abs.php",
+ "classes/src/functions/acos.php",
+ "classes/src/functions/acosh.php",
+ "classes/src/functions/acot.php",
+ "classes/src/functions/acoth.php",
+ "classes/src/functions/acsc.php",
+ "classes/src/functions/acsch.php",
+ "classes/src/functions/argument.php",
+ "classes/src/functions/asec.php",
+ "classes/src/functions/asech.php",
+ "classes/src/functions/asin.php",
+ "classes/src/functions/asinh.php",
+ "classes/src/functions/atan.php",
+ "classes/src/functions/atanh.php",
+ "classes/src/functions/conjugate.php",
+ "classes/src/functions/cos.php",
+ "classes/src/functions/cosh.php",
+ "classes/src/functions/cot.php",
+ "classes/src/functions/coth.php",
+ "classes/src/functions/csc.php",
+ "classes/src/functions/csch.php",
+ "classes/src/functions/exp.php",
+ "classes/src/functions/inverse.php",
+ "classes/src/functions/ln.php",
+ "classes/src/functions/log2.php",
+ "classes/src/functions/log10.php",
+ "classes/src/functions/negative.php",
+ "classes/src/functions/pow.php",
+ "classes/src/functions/rho.php",
+ "classes/src/functions/sec.php",
+ "classes/src/functions/sech.php",
+ "classes/src/functions/sin.php",
+ "classes/src/functions/sinh.php",
+ "classes/src/functions/sqrt.php",
+ "classes/src/functions/tan.php",
+ "classes/src/functions/tanh.php",
+ "classes/src/functions/theta.php",
+ "classes/src/operations/add.php",
+ "classes/src/operations/subtract.php",
+ "classes/src/operations/multiply.php",
+ "classes/src/operations/divideby.php",
+ "classes/src/operations/divideinto.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/PHP8"
+ },
+ "time": "2020-08-26T10:42:07+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/361c0f545c3172ee26c3d596a0aa03f0cef65e6a",
+ "reference": "361c0f545c3172ee26c3d596a0aa03f0cef65e6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0",
+ "phpcompatibility/php-compatibility": "^9.0",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.3",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.4"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ },
+ "files": [
+ "classes/src/Functions/adjoint.php",
+ "classes/src/Functions/antidiagonal.php",
+ "classes/src/Functions/cofactors.php",
+ "classes/src/Functions/determinant.php",
+ "classes/src/Functions/diagonal.php",
+ "classes/src/Functions/identity.php",
+ "classes/src/Functions/inverse.php",
+ "classes/src/Functions/minors.php",
+ "classes/src/Functions/trace.php",
+ "classes/src/Functions/transpose.php",
+ "classes/src/Operations/add.php",
+ "classes/src/Operations/directsum.php",
+ "classes/src/Operations/subtract.php",
+ "classes/src/Operations/multiply.php",
+ "classes/src/Operations/divideby.php",
+ "classes/src/Operations/divideinto.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/2.1.2"
+ },
+ "time": "2021-01-23T16:37:31+00:00"
+ },
+ {
+ "name": "myclabs/php-enum",
+ "version": "1.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/php-enum.git",
+ "reference": "46cf3d8498b095bd33727b13fd5707263af99421"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/php-enum/zipball/46cf3d8498b095bd33727b13fd5707263af99421",
+ "reference": "46cf3d8498b095bd33727b13fd5707263af99421",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "php": "^7.3 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "1.*",
+ "vimeo/psalm": "^4.5.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "MyCLabs\\Enum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP Enum contributors",
+ "homepage": "https://github.com/myclabs/php-enum/graphs/contributors"
+ }
+ ],
+ "description": "PHP Enum implementation",
+ "homepage": "http://github.com/myclabs/php-enum",
+ "keywords": [
+ "enum"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/php-enum/issues",
+ "source": "https://github.com/myclabs/php-enum/tree/1.8.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/mnapoli",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/php-enum",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-02-15T16:11:48+00:00"
+ },
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "1.17.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "c55269cb06911575a126dc225a05c0e4626e5fb4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/c55269cb06911575a126dc225a05c0e4626e5fb4",
+ "reference": "c55269cb06911575a126dc225a05c0e4626e5fb4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "ezyang/htmlpurifier": "^4.13",
+ "maennchen/zipstream-php": "^2.1",
+ "markbaker/complex": "^1.5||^2.0",
+ "markbaker/matrix": "^1.2||^2.0",
+ "php": "^7.2||^8.0",
+ "psr/http-client": "^1.0",
+ "psr/http-factory": "^1.0",
+ "psr/simple-cache": "^1.0"
+ },
+ "require-dev": {
+ "dompdf/dompdf": "^0.8.5",
+ "friendsofphp/php-cs-fixer": "^2.18",
+ "jpgraph/jpgraph": "^4.0",
+ "mpdf/mpdf": "^8.0",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^8.5||^9.3",
+ "squizlabs/php_codesniffer": "^3.5",
+ "tecnickcom/tcpdf": "^6.3"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)",
+ "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.17.1"
+ },
+ "time": "2021-03-02T17:54:11+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client/tree/master"
+ },
+ "time": "2020-06-29T06:28:15+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "reference": "12ac7fcd07e5b077433f5f2bee95b3a771bf61be",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/master"
+ },
+ "time": "2019-04-30T12:38:16+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/master"
+ },
+ "time": "2016-08-06T14:39:51+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "1.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "reference": "408d5eafb83c57f6365a3ca330ff23aa4a5fa39b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/master"
+ },
+ "time": "2017-10-23T01:57:42+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.22.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
+ "reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.22-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ },
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2021-01-22T09:19:47+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.0.0"
+}
--- /dev/null
+<?php namespace CubeTranslate;
+
+/**
+* Cube Translate
+*
+* This plugin provides tools for managing translations in WordPress + Elementor.
+*
+* @wordpress-plugin
+* Plugin Name: Cube Translate
+* Description: Translation tools for multilingual sites.
+* Version: 1.0.0
+* Author: Cubedesigners
+* Author URI: https://www.cubedesigners.com/
+* Text Domain: cube-translate
+* Domain Path: /languages
+*/
+
+// Namespaced constants for easier path and URL references
+define(__NAMESPACE__ . '\NS', __NAMESPACE__ . '\\'); // Namespace shortcut: NS
+define(NS . 'PLUGIN_PATH', trailingslashit(plugin_dir_path(__FILE__))); // Used as PLUGIN_PATH
+define(NS . 'PLUGIN_URL', trailingslashit(plugin_dir_url(__FILE__))); // Used as PLUGIN_URL
+
+// Load Composer libraries
+$autoloader = __DIR__ . '/vendor/autoload.php';
+if (file_exists($autoloader)) {
+ require_once $autoloader;
+}
+
+if (class_exists(NS . 'Init')) {
+ Init::register_classes();
+}
+
--- /dev/null
+<?php namespace CubeTranslate;
+
+class Admin
+{
+ public static $menu_title = 'Cube Translate';
+ public static $menu_slug = 'cube-translate';
+
+ public function register() {
+ // Main menu heading (just a heading, the link should go to the first sub-menu page)
+ add_action('admin_menu', function() {
+ add_menu_page(self::$menu_title, self::$menu_title, 'administrator', self::$menu_slug, '__return_false', 'dashicons-translation');
+ });
+
+ // Prevent the "Cube Translate" link appearing in the menu since it is blank
+ add_action( 'admin_head', function() {
+ remove_submenu_page(self::$menu_slug, self::$menu_slug);
+ });
+ }
+}
--- /dev/null
+<?php namespace CubeTranslate\Elementor;
+
+use CubeTranslate\Admin;
+use Elementor\Plugin;
+use Atomastic\Arrays\Arrays;
+use PhpOffice\PhpSpreadsheet\IOFactory;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Style\Fill;
+use PhpOffice\PhpSpreadsheet\Writer\Html;
+use Jfcherng\Diff\DiffHelper;
+use Jfcherng\Diff\Renderer\RendererConstant;
+
+class Translation
+{
+ protected $data;
+ protected $post;
+ protected $document;
+ protected $translatable_data = []; // Holds the extracted translatable content for exporting
+ protected $translatable_widgets = []; // Map of the translatable widgets + which fields should be translated
+ protected $ignored_widgets = []; // Widgets that won't be translated
+ protected $untranslated_widgets = []; // For debug purposes, record any unhandled widgets during content extraction
+ protected $updated_content = []; // Stores array of updated content items for confirmation purposes
+ protected $actions = [ // Used for nonces and form actions
+ 'download' => 'cube-translate_xls-download',
+ 'upload' => 'cube-translate_xls-upload',
+ 'confirm-import' => 'cube-translate_confirm-import',
+ ];
+ protected const TYPE_TEXT = 'text';
+ protected const TYPE_TEXTAREA = 'textarea';
+ protected const TYPE_HTML = 'html';
+
+ public function register() {
+
+ // Add menu item to side menu in dashboard under Cube Translate
+ add_action('admin_menu', function() {
+ add_submenu_page(Admin::$menu_slug, 'Elementor Bulk Translate', 'Elementor Translate', 'administrator', 'cube-translate-elementor', [$this, 'admin_page']);
+ });
+
+ // Add shortcut link to current page's translate menu
+ add_action('admin_bar_menu', function($admin_bar) {
+
+ global $pagenow;
+
+ // Only add the link when on admin page or on the frontend page
+ if (!($pagenow === 'post.php' && isset($_GET['post'])) && !is_page()) return false;
+
+ // Check if current page is an Elementor page
+ $elementor = new \WP_Query([
+ 'page_id' => get_the_ID(),
+ 'post_type' => 'page',
+ 'meta_key' => '_elementor_edit_mode',
+ 'meta_value' => 'builder',
+ ]);
+
+ // Check if user has correct permissions and if this is an Elementor page
+ if (in_array('administrator', wp_get_current_user()->roles) && $elementor->post_count > 0) {
+
+ $admin_bar->add_menu(array(
+ 'id' => 'wp-admin-bar-cube-translate-elementor',
+ 'title' => __('Cube Translate'),
+ 'href' => admin_url('admin.php') . '?page=cube-translate-elementor&id=' . get_the_ID(),
+ ));
+ }
+
+ }, 200 );
+
+ // Catch loading of this plugin page (wp-admin/admin.php?page=cube-translate-elementor)
+ // Action tag comes from menu slugs: Cube Translate (cube-translate) + _page_ + Elementor Translate (cube-translate-elementor)
+ // Ref: https://codex.wordpress.org/Plugin_API/Action_Reference#Actions_Run_During_an_Admin_Page_Request
+ // Ref: https://developer.wordpress.org/reference/hooks/load-page-php/#comment-4340
+ add_action('load-cube-translate_page_cube-translate-elementor', function() {
+
+ // This function will run as the admin page loads (wp-admin/admin.php?page=cube-translate-elementor)
+ // but before there is any output, making it suitable for triggering the XLS download function.
+ // The other way to do this is using the 'admin_post' action:
+ // Ref: https://wordpress.stackexchange.com/a/342643
+ // add_action('admin_post_cube-translate-download', [$this, 'export_XLS']);
+ // HTML form:
+ //<form action="'. admin_url('admin-post.php') .'" method="post">
+ //<input type="hidden" name="action" value="cube-translate-download">
+ //<input type="hidden" name="id" value="'. $_GET['id'] .'">
+ //<button class="button button-primary">Download Spreadsheet</button>
+ //</form>
+ // However, this requires a POST request, which is inconvenient because we just want to be able to link
+ // to this from anywhere and have it trigger the XLS download after checking permissions...
+
+ if (!$this->security_check()) {
+ wp_die("Sorry, you don't have permission to access this feature.");
+ }
+
+ if (isset($_GET['id']) && isset($_GET['download'])) {
+
+ // Ensure that the nonce is valid for this link
+ if (!isset($_GET['_wpnonce']) || !wp_verify_nonce($_GET['_wpnonce'], $this->actions['download'])) {
+ wp_die('Error validating download link. Please go back and try again.');
+ }
+
+ $this->export_XLS($_GET['id']);
+ }
+ });
+
+ }
+
+ public function __construct() {
+
+ // TODO: add a hook here so the translatable and ignored widgets can be overridden by individual themes / sites
+
+ // Definition of translatable widgets, which settings are translatable and what type of data it is
+ // Some widgets contain repeaters, which require special handling for sub-content + sub _id field
+ $this->translatable_widgets = [
+ 'heading' => [
+ 'title' => self::TYPE_TEXT,
+ ],
+ 'text-editor' => [
+ 'editor' => self::TYPE_HTML,
+ ],
+ 'button' => [
+ 'text' => self::TYPE_TEXT,
+ 'link.url' => self::TYPE_TEXT,
+ ],
+ 'image-box' => [
+ 'title_text' => self::TYPE_TEXT,
+ 'description_text' => self::TYPE_TEXTAREA,
+ ],
+ 'cube-text' => [
+ 'title' => self::TYPE_TEXT,
+ 'body' => self::TYPE_HTML,
+ 'cta_text' => self::TYPE_TEXT,
+ 'cta_link.url' => self::TYPE_TEXT,
+ ],
+ 'cube-picto-grid' => [
+ 'items' => [ // Repeater
+ 'title' => self::TYPE_TEXT,
+ 'description' => self::TYPE_TEXTAREA,
+ ],
+ ],
+ 'cube-timeline' => [
+ 'items' => [ // Repeater
+ 'title' => self::TYPE_TEXT,
+ 'details' => self::TYPE_HTML,
+ 'cta_text' => self::TYPE_TEXT,
+ 'cta_link.url' => self::TYPE_TEXT,
+ ],
+ ],
+ 'cube-timeline-horizontal' => [
+ 'items' => [ // Repeater
+ 'title' => self::TYPE_TEXT,
+ 'cta_text' => self::TYPE_TEXT,
+ 'cta_link.url' => self::TYPE_TEXT,
+ ],
+ ],
+ 'cube-testimonial-carousel' => [
+ 'testimonials' => [ // Repeater
+ 'name' => self::TYPE_TEXT,
+ 'testimonial' => self::TYPE_TEXTAREA,
+ 'notes' => self::TYPE_TEXT,
+ ],
+ ],
+ 'cube-dynamic-table' => [
+ 'column_headings' => self::TYPE_TEXTAREA,
+ 'rows' => [
+ 'cells' => self::TYPE_TEXTAREA
+ ],
+ ],
+ 'cube-fancy-list' => [
+ 'title' => self::TYPE_TEXT,
+ 'items' => [
+ 'content' => self::TYPE_HTML
+ ],
+ ],
+ 'cube-justified-list' => [
+ 'items' => [
+ 'title' => self::TYPE_HTML,
+ 'value' => self::TYPE_HTML,
+ ],
+ ],
+ 'cube-header-slideshow' => [
+ 'title' => self::TYPE_TEXTAREA,
+ 'body' => self::TYPE_TEXTAREA,
+ ],
+ 'cube-link-carousel' => [
+ 'links' => [
+ 'title' => self::TYPE_TEXT,
+ 'url' => self::TYPE_TEXT,
+ ],
+ ],
+ 'cube-photo-grid' => [
+ 'items' => [
+ 'caption' => self::TYPE_TEXT,
+ ],
+ ],
+
+ ];
+
+ // Widgets that shouldn't be translated
+ $this->ignored_widgets = [
+ 'image',
+ 'image-carousel',
+ 'spacer',
+ 'google_maps',
+ 'cube-bg-image',
+ 'cube-form',
+ 'cube-scientific-news',
+ 'cube-news-banner',
+ ];
+ }
+
+ public function init($post_ID) {
+ // Elementor stores the content in the wp_postmeta table as JSON in _elementor_data
+ // This JSON contains the an array of "sections" that have child "elements" of "columns"
+ // and then inside those, individual "widgets".
+ // Section > Column > Widget > Settings (content, possibly a multidimensional array for repeaters)
+
+ // Instead of getting the Elementor data from the database directly, we use the core Elementor functions
+ // because they handle all special cases for reading and saving the data (handling revisions etc).
+ // READING: See get_builder() function in elementor/includes/db.php
+ // SAVING: See ajax_save() function in elementor/core/documents-manager.php
+
+ $this->document = Plugin::$instance->documents->get($post_ID);
+ $this->post = $this->document->get_post();
+ $this->data = $this->document ? $this->document->get_elements_raw_data(null, false) : [];
+ }
+
+ // Check that user has correct permissions for this plugin
+ public function security_check() {
+ // Todo: consider also checking for a custom capability like "cube_translate" so that non-administrator users could be granted access
+ return in_array('administrator', wp_get_current_user()->roles);
+ }
+
+ public function admin_page() {
+ if (!isset($_REQUEST['id'])) {
+
+ $pages = new \WP_Query(
+ [
+ 'post_type' => 'page',
+ 'posts_per_page' => -1,
+ 'meta_key' => '_elementor_edit_mode',
+ 'meta_value' => 'builder',
+ ]
+ );
+
+ echo '<h1>Select a page to translate</h1>';
+ echo '<ul>';
+
+ foreach($pages->posts as $post) {
+ //$language = function_exists('pll_get_post_language') ? '('. pll_get_post_language($post->ID, 'name') .')' : '';
+ echo '<li><a href="'. admin_url('admin.php') .'?page=cube-translate-elementor&id='. $post->ID .'">'. $post->post_title .'</a></li>';
+ }
+
+ echo '</ul>';
+
+ return true;
+ }
+
+ $id = $_REQUEST['id'];
+
+ if (empty(get_post_meta($id, '_elementor_data', true))) {
+ wp_die('Error: this post does not contain any Elementor data.');
+ }
+
+ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
+
+ $action = $_POST['action'];
+
+ // Verify POST requests for security before acting on them.
+ // Only checks nonce and referrer because privilege check is done at a higher level
+ check_admin_referer($action);
+
+
+ switch($action) {
+ case $this->actions['upload']:
+ return $this->import_XLS();
+ case $this->actions['confirm-import']:
+ return $this->apply_updates();
+ }
+ }
+
+ $this->init($id);
+ $preview = $this->generate_HTML($id);
+ $language = function_exists('pll_get_post_language') ? pll_get_post_language($id, 'flag') : '';
+
+ echo '<h1>Cube Translate for Elementor</h1>';
+ echo "<h2 style='margin-top: 2rem'>
+ $language
+ <a href='{$this->document->get_permalink()}' style='margin-right:1em'>{$this->post->post_title}</a>
+ <a href='". admin_url('post.php') ."?action=edit&post={$this->post->ID}' class='button' style='vertical-align: middle'>Edit Page</a>
+ </h2>";
+
+
+ echo '<hr><br><form method="post" enctype="multipart/form-data" style="background: #fff; padding: 2em; border: 1px dashed #ccc; max-width: 400px;">
+ <input type="hidden" name="action" value="'. $this->actions['upload'] .'">
+ <input type="hidden" name="id" value="'. $id .'">
+ <label class="screen-reader-text" for="xls_upload">Excel spreadsheet</label>
+ <input type="file" id="xls_upload" name="xls_upload" accept=".xls,.xlsx" required>
+ <br><br>
+ <button class="button button-primary" style="font-size: 1.3em">Upload Translations (XLS) <span class="dashicons dashicons-upload" style="vertical-align:middle"></span></button>';
+
+ wp_nonce_field($this->actions['upload']); // Include nonce and referrer fields
+
+ echo '</form>';
+
+ echo '<br><hr><h2>Translatable Content Preview</h2>';
+ echo '<p><strong>'. count($this->translatable_data) .' translatable items found</strong></p>';
+
+ $download_link = admin_url('admin.php') .'?page=cube-translate-elementor&download=xls&id='. $id;
+ $download_link = wp_nonce_url($download_link, $this->actions['download']);
+
+ echo '<a href="'. $download_link .'" class="button" style="font-size: 1.3em">Download Spreadsheet <span class="dashicons dashicons-download" style="vertical-align:middle"></span></a>';
+
+ echo $preview;
+
+ }
+
+ public function generate_HTML($ID = null) {
+ $spreadsheet = $this->prepare_spreadsheet($ID);
+ $writer = new Html($spreadsheet);
+
+ $res = '<style>
+ table.sheet0 {
+ max-width: 98%;
+ border-collapse: collapse;
+ margin: 1.5em 0;
+ }
+ table.sheet0 .row0 > * {
+ background-color: rgba(0,0,0,0.1);
+ font-weight: bold;
+ }
+ table.sheet0 tr > * {
+ padding: 0.75em;
+ border: 1px solid;
+ background-color: #fff;
+ }
+
+ .column1, .column2 {
+ white-space: nowrap; /* Widget and Field columns */
+ }
+
+ table.sheet0 .column4 {
+ display: none; /* hide Translation column */
+ }
+
+ </style>';
+ $res .= $writer->generateSheetData();
+
+ return $res;
+ }
+
+ public function get_data() {
+ return $this->data;
+ }
+
+ public function get_translatable_content() {
+ $this->process_data($this->data);
+ return $this->translatable_data;
+ }
+
+ public function get_untranslated_widgets() {
+ return collect($this->untranslated_widgets)->unique();
+ }
+
+
+ // Find and extract translatable items from Elementor data structure
+ // This function is called recursively
+ protected function process_data($data) {
+ if (!is_array($data)) return false;
+
+ foreach ($data as $element) {
+ if ($element['elType'] === 'widget') {
+ $this->extract_translatable_content($element);
+ }
+
+ if (isset($element['elements']) && !empty($element['elements'])) {
+ $this->process_data($element['elements']);
+ }
+ }
+
+ }
+
+ protected function extract_translatable_content($widget) {
+ // Only process Elementor widgets that we have a definition for
+ if (!array_key_exists($widget['widgetType'], $this->translatable_widgets)) {
+ // Keep track of any widgets that aren't handled for translation
+ if (!in_array($widget['widgetType'], $this->ignored_widgets)) {
+ $this->untranslated_widgets[] = $widget['widgetType'];
+ }
+ return false;
+ }
+
+ // Get widget data into Arrays object for easier handling
+ $widget_data = Arrays::create($widget['settings']);
+
+ foreach ($this->translatable_widgets[$widget['widgetType']] as $field => $format) {
+
+ //=== Repeater fields
+ if (is_array($format)) { // Special case: when the field is a repeater, it will contain an array of details
+ $repeater_fields = $format;
+
+ // Loop over repeater items
+ foreach ($widget_data->get($field, []) as $repeater_item) {
+ $repeater_data = Arrays::create($repeater_item); // Use Arrays library so we can use dot notation for nested data
+
+ // Next, a loop for each translatable field
+ foreach ($repeater_fields as $repeater_field => $repeater_field_format) {
+ if ($repeater_data->get($repeater_field)) { // Invalid or empty fields will be skipped
+ $this->translatable_data[] = [
+ 'id' => $widget['id'],
+ 'widget' => $widget['widgetType'],
+ 'field' => "{$field}#{$repeater_item['_id']}.{$repeater_field}",
+ 'content' => $repeater_data->get($repeater_field), // Handles dot notation access
+ ];
+ }
+ }
+ }
+
+ //=== Normal fields (empty fields will be skipped)
+ } elseif ($widget_data->get($field)) {
+ $this->translatable_data[] = [
+ 'id' => $widget['id'],
+ 'widget' => $widget['widgetType'],
+ 'field' => $field,
+ 'content' => $widget_data->get($field), // Handles dot notation access
+ ];
+ }
+ }
+ }
+
+ public function update_contents($rows) {
+
+ foreach ($rows as $row) {
+
+ if (!key_exists('id', $row) || !key_exists('widget', $row) || !key_exists('field', $row) || !key_exists('translation', $row)) continue;
+
+ $element_ID = trim($row['id']);
+ $widget_type = trim($row['widget']);
+ $field_name = trim($row['field']);
+ $content = trim($row['translation']);
+ $this->update_element($this->data, $element_ID, $widget_type, $field_name, $content);
+ }
+
+ // dump("UPDATE COUNT", $this->update_count);
+ // dump(json_encode($this->data));
+
+
+ }
+
+ // Recursive function to search through the Elementor data structure and update content based on IDs
+ public function update_element(&$data, $element_ID, $widget_type, $field_name, $contents) {
+
+ $match = false;
+
+ // Search through all data until we find the item we're looking for
+ foreach ($data as &$element) {
+
+ if ($element['id'] === $element_ID && isset($element['widgetType']) && $element['widgetType'] === $widget_type) {
+
+ // Match found! Now update data...
+ $element = $this->update_field($element, $field_name, $contents);
+
+ // End loop and recursion
+ return $element;
+ }
+
+ // No match found so keep going recursively...
+ $match = $this->update_element($element['elements'], $element_ID, $widget_type, $field_name, $contents);
+
+ if ($match) {
+ break; // Once a match is found stop searching at this level
+ }
+ }
+
+ return $match;
+ }
+
+ protected function update_field($element, $field_name, $contents) {
+
+ // Special case: repeater elements have a different structure to normal element data
+ // The repeater subfield name and ID is stored with the field_name in the spreadsheet
+ // The format is: field_name#itemID.subfield (itemID and subfield are both optional)
+ // We can use dot notation to target nested data so both field_name and subfield can
+ // contain dots: eg. image.caption OR items#c697edd.cta_link.url
+ // Regex test: https://regex101.com/r/YLND5s/4
+ preg_match('/(?<field>[a-z0-9\-_\.]+)(?:#(?<id>[a-z0-9]+))?(?:\.(?<subfield>[a-z\-_\.]+))?/', $field_name, $matches);
+ extract($matches); // For easier access to $field, $id and $subfield
+
+ if (empty($field)) return $element; // Bail out if at least the field isn't found
+
+ //=== Find the field and update the value according to field pattern above
+ if (!empty($id) && !empty($subfield)) { // Repeater item (field name has both ID and subfield present)
+ foreach ($element['settings'][$field] as &$repeater_item) {
+
+ $repeater_data = Arrays::create($repeater_item); // Allows updates using dot notation
+
+ // Only perform update if contents have changed
+ if ($id === $repeater_data->get('_id') && $repeater_data->has($subfield)
+ && $this->has_changed($repeater_data->get($subfield), $contents)) {
+ $repeater_item = $repeater_data->set($subfield, $contents)->toArray();
+ return $element; // Match found, end loop
+ }
+ }
+
+ } else { // Normal field or field.subfield
+ $element_data = Arrays::create($element['settings']); // Allow updates using dot notation
+
+ // Only perform an update if the contents have actually changed
+ if ($element_data->has($field) && $this->has_changed($element_data->get($field), $contents)) {
+ $element['settings'] = $element_data->set($field, $contents)->toArray(); // Convert back to normal array after update
+ }
+ }
+
+ return $element;
+ }
+
+ protected function has_changed($old_value, $new_value) {
+
+ if ($old_value === $new_value) {
+ return false;
+ }
+
+ // Log the changes
+ $this->updated_content[] = [
+ 'old' => $old_value,
+ 'new' => $new_value,
+ ];
+
+ return true;
+ }
+
+ public function get_changes() {
+ return $this->updated_content;
+ }
+
+ public function count_changes() {
+ return count($this->get_changes());
+ }
+
+ public function prepare_spreadsheet($id = null) {
+ if ($id) $this->init($id);
+ $data = $this->get_translatable_content();
+
+ // Create new Spreadsheet object
+ $spreadsheet = new Spreadsheet();
+
+ // Set post title as document title
+ $spreadsheet->getProperties()->setTitle($this->post->post_title);
+
+ // Create header row
+ $headers = ['ID', 'Widget', 'Field', 'Content', 'Translation'];
+ $spreadsheet->setActiveSheetIndex(0)->fromArray([$headers]);
+ $spreadsheet->getActiveSheet()->getStyle('A1:E1')->applyFromArray(
+ [
+ 'font' => [
+ 'bold' => true,
+ ],
+ 'fill' => [
+ 'fillType' => Fill::FILL_SOLID,
+ 'color' => ['argb' => 'FFDDDDDD'], // AARRGGBB (Alpha Red Green Blue)
+ ],
+ ]
+ );
+
+ $sheet = $spreadsheet->getActiveSheet();
+
+ // Now populate the rest of the fields
+ for ($i = 0; $i < count($data); $i++) {
+ $x = $i + 2; // Offset the row number to allow for header row (i is zero-indexed but rows start from 1)
+ $sheet
+ ->setCellValue('A' . $x, $data[$i]['id'])
+ ->setCellValue('B' . $x, $data[$i]['widget'])
+ ->setCellValue('C' . $x, $data[$i]['field'])
+ ->setCellValue('D' . $x, $data[$i]['content'])->getStyle('D' . $x)->getAlignment()->setWrapText(true);
+
+ $sheet->getStyle('E' . $x)->getAlignment()->setWrapText(true);
+ }
+
+ // Set widths
+ $spreadsheet->getActiveSheet()->getColumnDimension('A')->setAutoSize(true);
+ $spreadsheet->getActiveSheet()->getColumnDimension('B')->setAutoSize(true);
+ $spreadsheet->getActiveSheet()->getColumnDimension('C')->setAutoSize(true);
+ $spreadsheet->getActiveSheet()->getColumnDimension('D')->setWidth(70); // Original Content
+ $spreadsheet->getActiveSheet()->getColumnDimension('E')->setWidth(70); // Translation
+
+ // Rename worksheet
+ $spreadsheet->getActiveSheet()->setTitle(substr($this->post->post_name,0,30)); // 31 char maximum for sheet title
+
+ // Set active sheet index to the first sheet, so Excel opens this as the first sheet
+ $spreadsheet->setActiveSheetIndex(0);
+
+ return $spreadsheet;
+ }
+
+ public function import_XLS() {
+
+ echo "<h1>Importing Translations</h1>";
+
+ // Make sure upload exists and there aren't any errors reported
+ if (!isset($_FILES['xls_upload']) || $_FILES['xls_upload']['error'] !== UPLOAD_ERR_OK) {
+ wp_die('There was an error uploading the spreadsheet. Please check the file and try again.');
+ }
+
+ // Check that upload is of the expected MIME type
+ $allowed_types = [
+ 'application/vnd.ms-excel', // XLS
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // XLSX
+ ];
+
+ if (!in_array($_FILES['xls_upload']['type'], $allowed_types)) {
+ wp_die('Error: unexpected file format ('. $_FILES['xls_upload']['type'] .'). File must be XLS or XLSX format');
+ }
+
+ $id = $_REQUEST['id'];
+ $spreadsheet = IOFactory::load($_FILES['xls_upload']['tmp_name']);
+ $highest = $spreadsheet->getActiveSheet()->getHighestRowAndColumn();
+ $data = $spreadsheet->getActiveSheet()->rangeToArray("A1:{$highest['column']}{$highest['row']}");
+
+ $keys = array_shift($data); // Take the first row to use as array key names
+ $keys = array_map('strtolower', array_map('trim', $keys)); // lowercase and trim key names
+
+ // Apply heading keys to each row's data
+ foreach ($data as &$row) {
+ $row = array_combine($keys, $row);
+ }
+
+ $this->init($id);
+ $this->update_contents($data);
+
+ if ($this->count_changes() > 0) {
+
+ echo "<div class='notice notice-large notice-info' style='margin: 2em 0'>{$this->count_changes()} updates were found. Please confirm the changes before applying them.</div>";
+
+ //=== Show a diff of changes
+ $differOptions = [
+ 'context' => 0, // How many neighbour lines to show
+ 'ignoreCase' => false, // Case-sensitive
+ 'ignoreWhitespace' => false, // Don't ignore whitespace differences
+ ];
+
+ // Diff renderer class options
+ $rendererOptions = [
+ 'detailLevel' => 'word', // how detailed the rendered HTML in-line diff is? (none, line, word, char)
+ 'lineNumbers' => false, // show line numbers in HTML renderers
+ 'separateBlock' => false, // show a separator between different diff hunks in HTML renderers
+ 'showHeader' => true, // show the (table) header
+ 'spacesToNbsp' => false, // Convert spaces to HTML Alternative is to use CSS whitespace: pre
+ 'tabSize' => 4, // HTML renderer tab width (negative = do not convert into spaces)
+ // this option is currently only for the Combined renderer.
+ // it determines whether a replace-type block should be merged or not
+ // depending on the content changed ratio, which values between 0 and 1.
+ 'mergeThreshold' => 0.8,
+ // this option is currently only for the Unified and the Context renderers.
+ // RendererConstant::CLI_COLOR_AUTO = colorize the output if possible (default)
+ // RendererConstant::CLI_COLOR_ENABLE = force to colorize the output
+ // RendererConstant::CLI_COLOR_DISABLE = force not to colorize the output
+ 'cliColorization' => RendererConstant::CLI_COLOR_AUTO,
+ // this option is currently effective when the "detailLevel" is "word"
+ // characters listed in this array can be used to make diff segments into a whole
+ // for example, making "<del>good</del>-<del>looking</del>" into "<del>good-looking</del>"
+ // this should bring better readability but set this to empty array if you do not want it
+ 'wordGlues' => [' ', '-'],
+ // change this value to a string as the returned diff if the two input strings are identical
+ 'resultForIdenticals' => null,
+ // extra HTML classes added to the DOM of the diff container
+ 'wrapperClasses' => ['translate-diff-wrapper'],
+ ];
+
+ $old = array_column($this->get_changes(), 'old'); // Get all "old" text in a single array
+ $new = array_column($this->get_changes(), 'new'); // Get all "new" text in a single array
+ $diff = DiffHelper::calculate($old, $new, 'SideBySide', $differOptions, $rendererOptions);
+ // Hack: The "diff" class in the output conflicts with WordPress styling so remove it...
+ echo str_replace('translate-diff-wrapper diff diff-html diff-side-by-side', 'translate-diff-wrapper diff-html diff-side-by-side', $diff);
+ // Styling for diff table
+ echo '<style>
+ table.translate-diff-wrapper {
+ border-collapse: collapse;
+ max-width: 98%;
+ }
+ table.translate-diff-wrapper th {
+ padding: 0.7em;
+ border: 1px solid;
+ background: rgba(0,0,0,0.1);
+ font-size: 1.2em;
+ }
+
+ table.translate-diff-wrapper td {
+ font-family: monospace;
+ padding: 0.5em;
+ border: 1px solid;
+ }
+
+ del {
+ background: #ffd3d3;
+ }
+
+ ins {
+ background: #afa;
+ text-decoration: none;
+ }
+
+ ins, del {
+ display: inline-block;
+ padding: 3px;
+ }
+ </style>';
+
+ //=== Confirmation form
+ echo '<br><br><form method="post">
+ <input type="hidden" name="action" value="'. $this->actions['confirm-import'] .'">
+ <input type="hidden" name="id" value="'. $id .'">
+ <input type="hidden" name="import_data" value="'. base64_encode(serialize($data)) .'">
+ <button class="button button-primary" style="font-size: 1.3em">Apply Changes <span class="dashicons dashicons-yes-alt" style="vertical-align:middle"></span></button>';
+
+ wp_nonce_field($this->actions['confirm-import']); // Include nonce and referrer fields
+
+ echo '</form>';
+
+
+ } else {
+ // No applicable changes found
+ echo "<div class='notice notice-large notice-warning' style='margin: 2em 2em 2em 0'>No possible updates were found in the uploaded file. Please check that you uploaded the correct spreadsheet or that changes haven't already been applied.</div>";
+ echo '<a href="javascript:history.go(-1);">« Go Back</a>';
+ }
+
+ return true;
+ }
+
+ public function apply_updates() {
+
+ echo "<h1>Updating Translations</h1>";
+
+ $id = $_POST['id'];
+ $data = unserialize(base64_decode($_POST['import_data']));
+
+ $this->init($id);
+ $this->update_contents($data);
+
+ if ($this->count_changes() > 0) {
+
+ // Tell Elementor to update the page content
+ $saved = $this->document->save([
+ 'elements' => $this->get_data()
+ ]);
+
+ if ($saved) {
+ echo "<div class='notice notice-large notice-success' style='margin: 2em 0'>Success! {$this->count_changes()} updates applied. <a href='" . get_permalink($_REQUEST['id']) . "'>View page</a>.</div>";
+ } else {
+ echo "<div class='notice notice-large notice-error' style='margin: 2em 0'>Error: the changes couldn't be applied.</div>";
+ }
+
+ }
+
+ return true;
+ }
+
+ public function export_XLS($id) {
+
+ $spreadsheet = $this->prepare_spreadsheet($id);
+
+ // Redirect output to the client's web browser (xls)
+ header('Content-Type: application/vnd.ms-excel');
+ header('Content-Disposition: attachment;filename="'. $this->post->post_name .'.xls"');
+ header('Cache-Control: max-age=0');
+
+ $writer = IOFactory::createWriter($spreadsheet, 'Xls');
+ $writer->save('php://output');
+ exit;
+ }
+
+}
--- /dev/null
+<?php namespace CubeTranslate;
+
+// Setup reference: https://www.youtube.com/watch?v=NdDRNiIfYDw
+
+final class Init
+{ // Marked as final because this class should never be extended (single init only!)
+
+ /**
+ * List of service classes to be used
+ * @return array Array of classes to be instantiated
+ */
+ public static function classes() {
+ return [
+ Admin::class,
+ Elementor\Translation::class,
+ ];
+ }
+
+ /**
+ * Register all services (custom post types, custom fields, shortcodes etc)
+ */
+ public static function register_classes() {
+ foreach (self::classes() as $class) {
+ $service = self::instantiate($class);
+
+ // If the class has a register method, call it.
+ if (method_exists($service, 'register')) {
+ $service->register();
+ }
+ }
+ }
+
+ /**
+ * @param class $class Class from the services array
+ * @return class instance New instance of the class
+ */
+ private static function instantiate($class) {
+ return new $class();
+ }
+}