]> _ Git - cube-wp-translate.git/commitdiff
Initial version of Cube Translate for Elementor plugin. WIP #3211 @30
authorStephen Cameron <stephen@cubedesigners.com>
Thu, 6 May 2021 18:24:09 +0000 (20:24 +0200)
committerStephen Cameron <stephen@cubedesigners.com>
Thu, 6 May 2021 18:24:09 +0000 (20:24 +0200)
.gitignore [new file with mode: 0644]
composer.json [new file with mode: 0644]
composer.lock [new file with mode: 0644]
index.php [new file with mode: 0644]
src/Admin.php [new file with mode: 0644]
src/Elementor/Translation.php [new file with mode: 0644]
src/Init.php [new file with mode: 0644]

diff --git a/.gitignore b/.gitignore
new file mode 100644 (file)
index 0000000..9f3397c
--- /dev/null
@@ -0,0 +1,4 @@
+.DS_Store
+.idea
+*.log
+/vendor
diff --git a/composer.json b/composer.json
new file mode 100644 (file)
index 0000000..dea1437
--- /dev/null
@@ -0,0 +1,15 @@
+{
+    "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"
+        }
+    }
+}
diff --git a/composer.lock b/composer.lock
new file mode 100644 (file)
index 0000000..72eef70
--- /dev/null
@@ -0,0 +1,1107 @@
+{
+    "_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"
+}
diff --git a/index.php b/index.php
new file mode 100644 (file)
index 0000000..a9a56ed
--- /dev/null
+++ b/index.php
@@ -0,0 +1,32 @@
+<?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();
+}
+
diff --git a/src/Admin.php b/src/Admin.php
new file mode 100644 (file)
index 0000000..21ed435
--- /dev/null
@@ -0,0 +1,19 @@
+<?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);
+        });
+    }
+}
diff --git a/src/Elementor/Translation.php b/src/Elementor/Translation.php
new file mode 100644 (file)
index 0000000..97023d9
--- /dev/null
@@ -0,0 +1,767 @@
+<?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 &nbsp; 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);">&laquo; 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;
+    }
+
+}
diff --git a/src/Init.php b/src/Init.php
new file mode 100644 (file)
index 0000000..61fa51f
--- /dev/null
@@ -0,0 +1,40 @@
+<?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();
+    }
+}