From 508c2fa0a897384dc4776f1d17170bd89ba8a415 Mon Sep 17 00:00:00 2001 From: Georg Ringer Date: Tue, 2 Jul 2024 13:48:53 +0200 Subject: [PATCH] [TASK] Initial release --- .gitattributes | 7 + .github/workflows/core12.yml | 30 ++ .gitignore | 7 + Build/Scripts/runTests.sh | 481 ++++++++++++++++++ Build/php-cs-fixer/php-cs-fixer.php | 79 +++ Build/phpunit/FunctionalTests.xml | 52 ++ Build/phpunit/FunctionalTestsBootstrap.php | 30 ++ Build/phpunit/UnitTests.xml | 45 ++ Build/phpunit/UnitTestsBootstrap.php | 85 ++++ Build/testing-docker/docker-compose.yml | 427 ++++++++++++++++ Classes/Command/ConvertCommand.php | 61 +++ Classes/Domain/Model/Dto/Label.php | 22 + Classes/Service/ConvertService.php | 61 +++ Classes/Service/CsvReader.php | 45 ++ Classes/Service/XlfFileService.php | 100 ++++ Configuration/Services.yaml | 21 + LICENSE.txt | 345 +++++++++++++ Readme.md | 40 ++ Resources/Private/Examples/de.out.xlf | 16 + Resources/Private/Examples/in.csv | 3 + Resources/Private/Examples/out.xlf | 14 + Resources/Public/Icons/Extension.svg | 1 + Tests/Unit/Domain/Model/Dto/LabelTest.php | 28 + Tests/Unit/Service/ConvertServiceTest.php | 28 + Tests/Unit/Service/CsvReaderTest.php | 43 ++ Tests/Unit/Service/Fixtures/de.result.xlf | 16 + Tests/Unit/Service/Fixtures/example.csv | 3 + Tests/Unit/Service/Fixtures/reader/no-en.csv | 2 + .../Service/Fixtures/reader/no-header.csv | 2 + Tests/Unit/Service/Fixtures/reader/valid.csv | 5 + Tests/Unit/Service/Fixtures/reader/valid.json | 122 +++++ Tests/Unit/Service/Fixtures/result.xlf | 14 + Tests/Unit/Service/Result/.gitkeep | 0 Tests/Unit/Service/Result/de.result.xlf | 16 + Tests/Unit/Service/Result/result.xlf | 14 + composer.json | 31 ++ ext_emconf.php | 21 + 37 files changed, 2317 insertions(+) create mode 100644 .gitattributes create mode 100644 .github/workflows/core12.yml create mode 100644 .gitignore create mode 100755 Build/Scripts/runTests.sh create mode 100644 Build/php-cs-fixer/php-cs-fixer.php create mode 100644 Build/phpunit/FunctionalTests.xml create mode 100644 Build/phpunit/FunctionalTestsBootstrap.php create mode 100644 Build/phpunit/UnitTests.xml create mode 100644 Build/phpunit/UnitTestsBootstrap.php create mode 100644 Build/testing-docker/docker-compose.yml create mode 100644 Classes/Command/ConvertCommand.php create mode 100644 Classes/Domain/Model/Dto/Label.php create mode 100644 Classes/Service/ConvertService.php create mode 100644 Classes/Service/CsvReader.php create mode 100644 Classes/Service/XlfFileService.php create mode 100644 Configuration/Services.yaml create mode 100644 LICENSE.txt create mode 100644 Readme.md create mode 100644 Resources/Private/Examples/de.out.xlf create mode 100644 Resources/Private/Examples/in.csv create mode 100644 Resources/Private/Examples/out.xlf create mode 100644 Resources/Public/Icons/Extension.svg create mode 100644 Tests/Unit/Domain/Model/Dto/LabelTest.php create mode 100644 Tests/Unit/Service/ConvertServiceTest.php create mode 100644 Tests/Unit/Service/CsvReaderTest.php create mode 100644 Tests/Unit/Service/Fixtures/de.result.xlf create mode 100644 Tests/Unit/Service/Fixtures/example.csv create mode 100644 Tests/Unit/Service/Fixtures/reader/no-en.csv create mode 100644 Tests/Unit/Service/Fixtures/reader/no-header.csv create mode 100644 Tests/Unit/Service/Fixtures/reader/valid.csv create mode 100644 Tests/Unit/Service/Fixtures/reader/valid.json create mode 100644 Tests/Unit/Service/Fixtures/result.xlf create mode 100644 Tests/Unit/Service/Result/.gitkeep create mode 100644 Tests/Unit/Service/Result/de.result.xlf create mode 100644 Tests/Unit/Service/Result/result.xlf create mode 100644 composer.json create mode 100644 ext_emconf.php diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..11a8f24 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,7 @@ +/.github/ export-ignore +/Build/ export-ignore +/Tests/ export-ignore +/.editorconfig export-ignore +/.gitattributes export-ignore +/.github/ export-ignore +/.gitignore export-ignore diff --git a/.github/workflows/core12.yml b/.github/workflows/core12.yml new file mode 100644 index 0000000..011ce8c --- /dev/null +++ b/.github/workflows/core12.yml @@ -0,0 +1,30 @@ +name: core 12 + +on: [ push, pull_request ] + +jobs: + tests: + name: v12 + runs-on: ubuntu-20.04 + strategy: + # This prevents cancellation of matrix job runs, if one/two already failed and let the + # rest matrix jobs be executed anyway. + fail-fast: false + matrix: + php: [ '8.1', '8.2' ] + composerInstall: [ 'composerInstallLowest', 'composerInstallHighest' ] + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install testing system + run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s ${{ matrix.composerInstall }} + + - name: Lint PHP + run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s lint + + - name: Validate code against CGL + run: PHP_CS_FIXER_IGNORE_ENV=1 Build/Scripts/runTests.sh -t 12 -p 8.2 -s cgl -n + + - name: Unit Tests + run: Build/Scripts/runTests.sh -t 12 -p ${{ matrix.php }} -s unit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0c737a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/.Build +/composer.lock +/var +.php-cs-fixer.cache +/Build/testing-docker/.env +/Documentation-GENERATED-temp/ +/.DS_Store diff --git a/Build/Scripts/runTests.sh b/Build/Scripts/runTests.sh new file mode 100755 index 0000000..9a4b580 --- /dev/null +++ b/Build/Scripts/runTests.sh @@ -0,0 +1,481 @@ +#!/usr/bin/env bash + +# +# TYPO3 core test runner based on docker and docker-compose. +# +IMAGE_PREFIX="ghcr.io/typo3/" + +# Function to write a .env file in Build/testing-docker +# This is read by docker-compose and vars defined here are +# used in Build/testing-docker/docker-compose.yml +setUpDockerComposeDotEnv() { + # Delete possibly existing local .env file if exists + [ -e .env ] && rm .env + # Set up a new .env file for docker-compose + { + echo "COMPOSE_PROJECT_NAME=${PROJECT_NAME}" + # To prevent access rights of files created by the testing, the docker image later + # runs with the same user that is currently executing the script. docker-compose can't + # use $UID directly itself since it is a shell variable and not an env variable, so + # we have to set it explicitly here. + echo "HOST_UID=`id -u`" + # Your local user + echo "ROOT_DIR=${ROOT_DIR}" + echo "HOST_USER=${USER}" + echo "TEST_FILE=${TEST_FILE}" + echo "TYPO3_VERSION=${TYPO3_VERSION}" + echo "PHP_XDEBUG_ON=${PHP_XDEBUG_ON}" + echo "PHP_XDEBUG_PORT=${PHP_XDEBUG_PORT}" + echo "DOCKER_PHP_IMAGE=${DOCKER_PHP_IMAGE}" + echo "EXTRA_TEST_OPTIONS=${EXTRA_TEST_OPTIONS}" + echo "SCRIPT_VERBOSE=${SCRIPT_VERBOSE}" + echo "CGLCHECK_DRY_RUN=${CGLCHECK_DRY_RUN}" + echo "DATABASE_DRIVER=${DATABASE_DRIVER}" + echo "MARIADB_VERSION=${MARIADB_VERSION}" + echo "MYSQL_VERSION=${MYSQL_VERSION}" + echo "POSTGRES_VERSION=${POSTGRES_VERSION}" + echo "USED_XDEBUG_MODES=${USED_XDEBUG_MODES}" + echo "IMAGE_PREFIX=${IMAGE_PREFIX}" + } > .env +} + +# Options -a and -d depend on each other. The function +# validates input combinations and sets defaults. +handleDbmsAndDriverOptions() { + case ${DBMS} in + mysql|mariadb) + [ -z "${DATABASE_DRIVER}" ] && DATABASE_DRIVER="mysqli" + if [ "${DATABASE_DRIVER}" != "mysqli" ] && [ "${DATABASE_DRIVER}" != "pdo_mysql" ]; then + echo "Invalid option -a ${DATABASE_DRIVER} with -d ${DBMS}" >&2 + echo >&2 + echo "call \"./Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + postgres|sqlite) + if [ -n "${DATABASE_DRIVER}" ]; then + echo "Invalid option -a ${DATABASE_DRIVER} with -d ${DBMS}" >&2 + echo >&2 + echo "call \"./Build/Scripts/runTests.sh -h\" to display help and valid options" >&2 + exit 1 + fi + ;; + esac +} + +# Load help text into $HELP +read -r -d '' HELP <=20.10 for xdebug break pointing to work reliably, and +a recent docker-compose (tested >=1.21.2) is needed. + +Usage: $0 [options] [file] + +No arguments: Run all unit tests with PHP 7.4 + +Options: + -s <...> + Specifies which test suite to run + - cgl: cgl test and fix all php files + - clean: clean up build and testing related files + - composer: Execute "composer" command, using -e for command arguments pass-through, ex. -e "ci:php:stan" + - composerInstall: "composer update", handy if host has no PHP + - composerInstallLowest: "composer update", handy if host has no PHP + - composerInstallHighest: "composer update", handy if host has no PHP + - functional: functional tests + - lint: PHP linting + - unit: PHP unit tests + + -a + Only with -s acceptance,functional + Specifies to use another driver, following combinations are available: + - mysql + - mysqli (default) + - pdo_mysql + - mariadb + - mysqli (default) + - pdo_mysql + + -d + Only with -s acceptance,functional + Specifies on which DBMS tests are performed + - sqlite: (default) use sqlite + - mariadb: use mariadb + - mysql: use mysql + - postgres: use postgres + + -i <10.2|10.3|10.4|10.5|10.6|10.7> + Only with -d mariadb + Specifies on which version of mariadb tests are performed + - 10.2 (default) + - 10.3 + - 10.4 + - 10.5 + - 10.6 + - 10.7 + + -j <5.5|5.6|5.7|8.0> + Only with -d mysql + Specifies on which version of mysql tests are performed + - 5.5 (default) + - 5.6 + - 5.7 + - 8.0 + + -k <10|11|12|13|14> + Only with -d postgres + Specifies on which version of postgres tests are performed + - 10 (default) + - 11 + - 12 + - 13 + - 14 + + -p <7.4|8.0|8.1|8.2> + Specifies the PHP minor version to be used + - 7.4 (default): use PHP 7.4 + - 8.0: use PHP 8.0 + - 8.1: use PHP 8.1 + - 8.2: use PHP 8.2 + + -t <11|12> + Only with -s composerUpdate + Specifies the TYPO3 core major version to be used + - 11 (default): use TYPO3 core v11 + - 12: use TYPO3 core v12 + + -e "" + Only with -s functional|unit|composer + Additional options to send to phpunit (unit & functional tests) or codeception (acceptance + tests). For phpunit, options starting with "--" must be added after options starting with "-". + Example -e "-v --filter canRetrieveValueWithGP" to enable verbose output AND filter tests + named "canRetrieveValueWithGP" + + -x + Only with -s functional|unit + Send information to host instance for test or system under test break points. This is especially + useful if a local PhpStorm instance is listening on default xdebug port 9003. A different port + can be selected with -y + + -y + Send xdebug information to a different port than default 9003 if an IDE like PhpStorm + is not listening on default port. + + -z + Only with -x and -s functional|unit|acceptance + This sets the used xdebug modes. Defaults to 'debug,develop' + + -n + Only with -s cgl + Activate dry-run in CGL check that does not actively change files and only prints broken ones. + + -u + Update existing ${IMAGE_PREFIX}core-testing-*:latest docker images. Maintenance call to docker pull latest + versions of the main php images. The images are updated once in a while and only the youngest + ones are supported by core testing. Use this if weird test errors occur. Also removes obsolete + image versions of ${IMAGE_PREFIX}core-testing-*. + + -v + Enable verbose script output. Shows variables and docker commands. + + -h + Show this help. + +Examples: + # Run unit tests using PHP 7.4 + ./Build/Scripts/runTests.sh -s unit +EOF + +# Test if docker-compose exists, else exit out with error +if ! type "docker-compose" > /dev/null; then + echo "This script relies on docker and docker-compose. Please install" >&2 + exit 1 +fi + +# Go to the directory this script is located, so everything else is relative +# to this dir, no matter from where this script is called. +THIS_SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +cd "$THIS_SCRIPT_DIR" || exit 1 + +# Go to directory that contains the local docker-compose.yml file +cd ../testing-docker || exit 1 + +# Option defaults +if ! command -v realpath &> /dev/null; then + echo "This script works best with realpath installed" >&2 + ROOT_DIR="${PWD}/../../" +else + ROOT_DIR=`realpath ${PWD}/../../` +fi +TEST_SUITE="" +DBMS="sqlite" +PHP_VERSION="7.4" +TYPO3_VERSION="11" +PHP_XDEBUG_ON=0 +PHP_XDEBUG_PORT=9003 +EXTRA_TEST_OPTIONS="" +SCRIPT_VERBOSE=0 +CGLCHECK_DRY_RUN="" +DATABASE_DRIVER="" +MARIADB_VERSION="10.2" +MYSQL_VERSION="5.5" +POSTGRES_VERSION="10" +USED_XDEBUG_MODES="debug,develop" +#@todo the $$ would add the current process id to the name, keeping as plan b +#PROJECT_NAME="runTests-$(basename $(dirname $ROOT_DIR))-$(basename $ROOT_DIR)-$$" +PROJECT_NAME="runtestsfriendlycaptcha" +PROJECT_NAME="${PROJECT_NAME//[[:blank:]]/}" +echo $PROJECT_NAME + +# Option parsing +# Reset in case getopts has been used previously in the shell +OPTIND=1 +# Array for invalid options +INVALID_OPTIONS=(); +# Simple option parsing based on getopts (! not getopt) +while getopts ":s:a:d:i:j:k:p:t:e:xy:z:nhuv" OPT; do + case ${OPT} in + s) + TEST_SUITE=${OPTARG} + ;; + a) + DATABASE_DRIVER=${OPTARG} + ;; + d) + DBMS=${OPTARG} + ;; + i) + MARIADB_VERSION=${OPTARG} + if ! [[ ${MARIADB_VERSION} =~ ^(10.2|10.3|10.4|10.5|10.6|10.7)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + j) + MYSQL_VERSION=${OPTARG} + if ! [[ ${MYSQL_VERSION} =~ ^(5.5|5.6|5.7|8.0)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + k) + POSTGRES_VERSION=${OPTARG} + if ! [[ ${POSTGRES_VERSION} =~ ^(10|11|12|13|14)$ ]]; then + INVALID_OPTIONS+=("${OPTARG}") + fi + ;; + p) + PHP_VERSION=${OPTARG} + if ! [[ ${PHP_VERSION} =~ ^(7.4|8.0|8.1|8.2)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + t) + TYPO3_VERSION=${OPTARG} + if ! [[ ${TYPO3_VERSION} =~ ^(11|12)$ ]]; then + INVALID_OPTIONS+=("p ${OPTARG}") + fi + ;; + e) + EXTRA_TEST_OPTIONS=${OPTARG} + ;; + x) + PHP_XDEBUG_ON=1 + ;; + y) + PHP_XDEBUG_PORT=${OPTARG} + ;; + z) + USED_XDEBUG_MODES=${OPTARG} + ;; + h) + echo "${HELP}" + exit 0 + ;; + n) + CGLCHECK_DRY_RUN="-n" + ;; + u) + TEST_SUITE=update + ;; + v) + SCRIPT_VERBOSE=1 + ;; + \?) + INVALID_OPTIONS+=(${OPTARG}) + ;; + :) + INVALID_OPTIONS+=(${OPTARG}) + ;; + esac +done + +# Exit on invalid options +if [ ${#INVALID_OPTIONS[@]} -ne 0 ]; then + echo "Invalid option(s):" >&2 + for I in "${INVALID_OPTIONS[@]}"; do + echo "-"${I} >&2 + done + echo >&2 + echo "${HELP}" >&2 + exit 1 +fi + +# Move "7.2" to "php72", the latter is the docker container name +DOCKER_PHP_IMAGE=`echo "php${PHP_VERSION}" | sed -e 's/\.//'` + +# Set $1 to first mass argument, this is the optional test file or test directory to execute +shift $((OPTIND - 1)) +TEST_FILE=${1} + +if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x +fi + +if [ -z ${TEST_SUITE} ]; then + echo "${HELP}" + exit 0 +fi + +# Suite execution +case ${TEST_SUITE} in + cgl) + # Active dry-run for cgl needs not "-n" but specific options + if [[ ! -z ${CGLCHECK_DRY_RUN} ]]; then + CGLCHECK_DRY_RUN="--dry-run --diff" + fi + setUpDockerComposeDotEnv + docker-compose run cgl + SUITE_EXIT_CODE=$? + docker-compose down + ;; + clean) + rm -rf \ + ../../var/ \ + ../../.cache \ + ../../composer.lock \ + ../../.Build/ \ + ../../Tests/Acceptance/Support/_generated/ \ + ../../composer.json.testing + ;; + composer) + setUpDockerComposeDotEnv + docker-compose run composer + SUITE_EXIT_CODE=$? + docker-compose down + ;; + composerInstall) + setUpDockerComposeDotEnv + cp ../../composer.json ../../composer.json.orig + if [ -f "../../composer.json.testing" ]; then + cp ../../composer.json ../../composer.json.orig + fi + docker-compose run composer_install + cp ../../composer.json ../../composer.json.testing + mv ../../composer.json.orig ../../composer.json + SUITE_EXIT_CODE=$? + docker-compose down + ;; + composerInstallLowest) + setUpDockerComposeDotEnv + cp ../../composer.json ../../composer.json.orig + if [ -f "../../composer.json.testing" ]; then + cp ../../composer.json ../../composer.json.orig + fi + docker-compose run composer_install_lowest + cp ../../composer.json ../../composer.json.testing + mv ../../composer.json.orig ../../composer.json + SUITE_EXIT_CODE=$? + docker-compose down + ;; + composerInstallHighest) + setUpDockerComposeDotEnv + cp ../../composer.json ../../composer.json.orig + if [ -f "../../composer.json.testing" ]; then + cp ../../composer.json ../../composer.json.orig + fi + docker-compose run composer_install_highest + cp ../../composer.json ../../composer.json.testing + mv ../../composer.json.orig ../../composer.json + SUITE_EXIT_CODE=$? + docker-compose down + ;; + coveralls) + setUpDockerComposeDotEnv + docker-compose run coveralls + SUITE_EXIT_CODE=$? + docker-compose down + ;; + functional) + handleDbmsAndDriverOptions + setUpDockerComposeDotEnv + case ${DBMS} in + mariadb) + echo "Using driver: ${DATABASE_DRIVER}" + docker-compose run functional_mariadb + SUITE_EXIT_CODE=$? + ;; + mysql) + echo "Using driver: ${DATABASE_DRIVER}" + docker-compose run functional_mysql + SUITE_EXIT_CODE=$? + ;; + postgres) + docker-compose run functional_postgres + SUITE_EXIT_CODE=$? + ;; + sqlite) + # sqlite has a tmpfs as Web/typo3temp/var/tests/functional-sqlite-dbs/ + # Since docker is executed as root (yay!), the path to this dir is owned by + # root if docker creates it. Thank you, docker. We create the path beforehand + # to avoid permission issues. + mkdir -p ${ROOT_DIR}/Web/typo3temp/var/tests/functional-sqlite-dbs/ + docker-compose run functional_sqlite + SUITE_EXIT_CODE=$? + ;; + *) + echo "Invalid -d option argument ${DBMS}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 + esac + docker-compose down + ;; + lint) + setUpDockerComposeDotEnv + docker-compose run lint + SUITE_EXIT_CODE=$? + docker-compose down + ;; + phpstan) + setUpDockerComposeDotEnv + docker-compose run phpstan + SUITE_EXIT_CODE=$? + docker-compose down + ;; + phpstanGenerateBaseline) + setUpDockerComposeDotEnv + docker-compose run phpstan_generate_baseline + SUITE_EXIT_CODE=$? + docker-compose down + ;; + unit) + setUpDockerComposeDotEnv + docker-compose run unit + SUITE_EXIT_CODE=$? + docker-compose down + ;; + update) + # pull ${IMAGE_PREFIX}core-testing-*:latest versions of those ones that exist locally + docker images ${IMAGE_PREFIX}core-testing-*:latest --format "{{.Repository}}:latest" | xargs -I {} docker pull {} + # remove "dangling" ${IMAGE_PREFIX}core-testing-* images (those tagged as ) + docker images ${IMAGE_PREFIX}core-testing-* --filter "dangling=true" --format "{{.ID}}" | xargs -I {} docker rmi {} + ;; + *) + echo "Invalid -s option argument ${TEST_SUITE}" >&2 + echo >&2 + echo "${HELP}" >&2 + exit 1 +esac + +exit $SUITE_EXIT_CODE diff --git a/Build/php-cs-fixer/php-cs-fixer.php b/Build/php-cs-fixer/php-cs-fixer.php new file mode 100644 index 0000000..15788ae --- /dev/null +++ b/Build/php-cs-fixer/php-cs-fixer.php @@ -0,0 +1,79 @@ +setHeader( + 'This file is part of the "friendlycaptcha" Extension for TYPO3 CMS. + +For the full copyright and license information, please read the +LICENSE.txt file that was distributed with this source code.', + true +); +$config->setFinder( + (new PhpCsFixer\Finder()) + ->in(realpath(__DIR__ . '/../../')) + ->ignoreVCSIgnored(true) + ->notPath('/^.Build\//') + ->notPath('/^Build\/php-cs-fixer\/php-cs-fixer.php/') + ->notPath('/^Build\/phpunit\/(UnitTestsBootstrap|FunctionalTestsBootstrap).php/') + ->notPath('/^Configuration\//') + ->notPath('/^Documentation\//') + ->notName('/^ext_(emconf|localconf|tables).php/') +) + ->setRiskyAllowed(true) + ->setRules([ + '@DoctrineAnnotation' => true, + '@PER-CS' => true, + 'array_syntax' => ['syntax' => 'short'], + 'blank_line_after_opening_tag' => true, + 'control_structure_braces' => true, + 'control_structure_continuation_position' => true, + 'declare_parentheses' => true, + 'no_multiple_statements_per_line' => true, + 'braces_position' => true, + 'statement_indentation' => true, + 'no_extra_blank_lines' => true, + 'cast_spaces' => ['space' => 'none'], + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'dir_constant' => true, + 'new_with_parentheses' => true, + 'lowercase_cast' => true, + 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'modernize_types_casting' => true, + 'native_function_casing' => true, + 'no_alias_functions' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_null_property_initialization' => true, + 'no_short_bool_cast' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_singleline' => true, + 'no_unneeded_control_parentheses' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_nullsafe_operator' => true, + 'no_whitespace_in_blank_line' => true, + 'ordered_imports' => true, + 'php_unit_construct' => ['assertions' => ['assertEquals', 'assertSame', 'assertNotEquals', 'assertNotSame']], + 'php_unit_mock_short_will_return' => true, + 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], + 'phpdoc_no_access' => true, + 'phpdoc_no_empty_return' => true, + 'phpdoc_no_package' => true, + 'phpdoc_scalar' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], + 'return_type_declaration' => ['space_before' => 'none'], + 'single_quote' => true, + 'single_line_comment_style' => ['comment_types' => ['hash']], + 'single_trait_insert_per_statement' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'whitespace_after_comma_in_array' => ['ensure_single_space' => true], + 'yoda_style' => ['equal' => false, 'identical' => false, 'less_and_greater' => false], + ]); +return $config; diff --git a/Build/phpunit/FunctionalTests.xml b/Build/phpunit/FunctionalTests.xml new file mode 100644 index 0000000..80156d2 --- /dev/null +++ b/Build/phpunit/FunctionalTests.xml @@ -0,0 +1,52 @@ + + + + + + ../../Tests/Functional/ + + + + + + + + + + diff --git a/Build/phpunit/FunctionalTestsBootstrap.php b/Build/phpunit/FunctionalTestsBootstrap.php new file mode 100644 index 0000000..a95bc52 --- /dev/null +++ b/Build/phpunit/FunctionalTestsBootstrap.php @@ -0,0 +1,30 @@ +defineOriginalRootPath(); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/tests'); + $testbase->createDirectory(ORIGINAL_ROOT . 'typo3temp/var/transient'); +})(); diff --git a/Build/phpunit/UnitTests.xml b/Build/phpunit/UnitTests.xml new file mode 100644 index 0000000..d09d8ef --- /dev/null +++ b/Build/phpunit/UnitTests.xml @@ -0,0 +1,45 @@ + + + + + + ../../Tests/Unit/ + + + + + + + diff --git a/Build/phpunit/UnitTestsBootstrap.php b/Build/phpunit/UnitTestsBootstrap.php new file mode 100644 index 0000000..8b4ead3 --- /dev/null +++ b/Build/phpunit/UnitTestsBootstrap.php @@ -0,0 +1,85 @@ +getWebRoot(), '/')); + } + if (!getenv('TYPO3_PATH_WEB')) { + putenv('TYPO3_PATH_WEB=' . rtrim($testbase->getWebRoot(), '/')); + } + + $testbase->defineSitePath(); + + $composerMode = defined('TYPO3_COMPOSER_MODE') && TYPO3_COMPOSER_MODE === true; + $requestType = \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_BE | \TYPO3\CMS\Core\Core\SystemEnvironmentBuilder::REQUESTTYPE_CLI; + \TYPO3\TestingFramework\Core\SystemEnvironmentBuilder::run(0, $requestType, $composerMode); + + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3conf/ext'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/assets'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/tests'); + $testbase->createDirectory(\TYPO3\CMS\Core\Core\Environment::getPublicPath() . '/typo3temp/var/transient'); + + // Retrieve an instance of class loader and inject to core bootstrap + $classLoader = require $testbase->getPackagesPath() . '/autoload.php'; + \TYPO3\CMS\Core\Core\Bootstrap::initializeClassLoader($classLoader); + + // Initialize default TYPO3_CONF_VARS + $configurationManager = new \TYPO3\CMS\Core\Configuration\ConfigurationManager(); + $GLOBALS['TYPO3_CONF_VARS'] = $configurationManager->getDefaultConfiguration(); + + $cache = new \TYPO3\CMS\Core\Cache\Frontend\PhpFrontend( + 'core', + new \TYPO3\CMS\Core\Cache\Backend\NullBackend('production', []) + ); + + // Set all packages to active + if (interface_exists(\TYPO3\CMS\Core\Package\Cache\PackageCacheInterface::class)) { + $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, \TYPO3\CMS\Core\Core\Bootstrap::createPackageCache($cache)); + } else { + // v10 compatibility layer + // @deprecated Will be removed when v10 compat is dropped from testing-framework + $packageManager = \TYPO3\CMS\Core\Core\Bootstrap::createPackageManager(\TYPO3\CMS\Core\Package\UnitTestPackageManager::class, $cache); + } + + \TYPO3\CMS\Core\Utility\GeneralUtility::setSingletonInstance(\TYPO3\CMS\Core\Package\PackageManager::class, $packageManager); + \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::setPackageManager($packageManager); + + $testbase->dumpClassLoadingInformation(); + + \TYPO3\CMS\Core\Utility\GeneralUtility::purgeInstances(); +})(); diff --git a/Build/testing-docker/docker-compose.yml b/Build/testing-docker/docker-compose.yml new file mode 100644 index 0000000..8fcbd83 --- /dev/null +++ b/Build/testing-docker/docker-compose.yml @@ -0,0 +1,427 @@ +version: '2.3' +services: + #--------------------------------------------------------------------------------------------------------------------- + # additional services needed for functional tests to be linked, e.g. databases + #--------------------------------------------------------------------------------------------------------------------- + mysql: + image: mysql:${MYSQL_VERSION} + environment: + MYSQL_ROOT_PASSWORD: funcp + tmpfs: + - /var/lib/mysql/:rw,noexec,nosuid + + mariadb: + image: mariadb:${MARIADB_VERSION} + environment: + MYSQL_ROOT_PASSWORD: funcp + tmpfs: + - /var/lib/mysql/:rw,noexec,nosuid + + postgres: + image: postgres:${POSTGRES_VERSION}-alpine + environment: + POSTGRES_PASSWORD: funcp + POSTGRES_USER: funcu + tmpfs: + - /var/lib/postgresql/data:rw,noexec,nosuid + + #--------------------------------------------------------------------------------------------------------------------- + # composer related services + #--------------------------------------------------------------------------------------------------------------------- + composer: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + composer ${EXTRA_TEST_OPTIONS}; + " + + composer_install: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^11.5.24 + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^12.2 + fi + composer install --no-progress; + " + + composer_install_lowest: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^11.5.24 + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^12.0 + fi + composer update --no-ansi --no-interaction --no-progress --with-dependencies --prefer-lowest; + composer show; + " + + composer_install_highest: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + if [ ${TYPO3_VERSION} -eq 11 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^11.5.24 + fi + if [ ${TYPO3_VERSION} -eq 12 ]; then + composer require --no-ansi --no-interaction --no-progress --no-install \ + typo3/cms-core:^12.2 + fi + composer update --no-progress --no-interaction; + composer show; + " + + #--------------------------------------------------------------------------------------------------------------------- + # unit tests + #--------------------------------------------------------------------------------------------------------------------- + unit: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_host=host.docker.internal\" \ + .Build/bin/phpunit -c Build/phpunit/UnitTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " + + lint: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + find . -name \\*.php ! -path "./.Build/\\*" -print0 | xargs -0 -n1 -P4 php -dxdebug.mode=off -l >/dev/null + " + + #--------------------------------------------------------------------------------------------------------------------- + # functional tests against different dbms + #--------------------------------------------------------------------------------------------------------------------- + functional_sqlite: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + tmpfs: + - ${ROOT_DIR}/public/typo3temp/var/tests/functional-sqlite-dbs/:rw,noexec,nosuid,uid=${HOST_UID} + environment: + typo3DatabaseDriver: pdo_sqlite + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; + else + XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-sqlite ${TEST_FILE}; + fi + " + + functional_postgres: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + links: + - postgres + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + environment: + typo3DatabaseDriver: pdo_pgsql + typo3DatabaseName: bamboo + typo3DatabaseUsername: funcu + typo3DatabaseHost: postgres + typo3DatabasePassword: funcp + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z postgres 5432; do + sleep 1; + done; + echo Database is up; + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; + else + XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} --exclude-group not-postgres ${TEST_FILE}; + fi + " + + functional_mysql: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + links: + - mysql + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + environment: + typo3DatabaseDriver: "${DATABASE_DRIVER}" + typo3DatabaseName: func_test + typo3DatabaseUsername: root + typo3DatabasePassword: funcp + typo3DatabaseHost: mysql + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z mysql 3306; do + sleep 1; + done; + echo Database is up; + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " + + functional_mariadb: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + links: + - mariadb + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + environment: + typo3DatabaseDriver: "${DATABASE_DRIVER}" + typo3DatabaseName: func_test + typo3DatabaseUsername: root + typo3DatabasePassword: funcp + typo3DatabaseHost: mariadb + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + echo Waiting for database start...; + while ! nc -z mariadb 3306; do + sleep 1; + done; + echo Database is up; + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + XDEBUG_MODE=\"off\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + else + XDEBUG_MODE=\"${USED_XDEBUG_MODES}\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + .Build/bin/phpunit -c Build/phpunit/FunctionalTests.xml ${EXTRA_TEST_OPTIONS} ${TEST_FILE}; + fi + " + + #--------------------------------------------------------------------------------------------------------------------- + # code quality tools + #--------------------------------------------------------------------------------------------------------------------- + cgl: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + if [ ${PHP_XDEBUG_ON} -eq 0 ]; then + php -dxdebug.mode=off \ + .Build/bin/php-cs-fixer fix \ + -v \ + ${CGLCHECK_DRY_RUN} \ + --config=Build/php-cs-fixer/php-cs-fixer.php \ + --using-cache=no . + else + XDEBUG_MODE=\"debug,develop\" \ + XDEBUG_TRIGGER=\"foo\" \ + XDEBUG_CONFIG=\"client_port=${PHP_XDEBUG_PORT} client_host=host.docker.internal\" \ + PHP_CS_FIXER_ALLOW_XDEBUG=1 \ + .Build/bin/php-cs-fixer fix \ + -v \ + ${CGLCHECK_DRY_RUN} \ + --config=Build/php-cs-fixer/php-cs-fixer.php \ + --using-cache=no . + fi + " + + coveralls: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + php -v | grep '^PHP'; + XDEBUG_MODE=\"coverage\" \ + php -dxdebug.mode=off ./.Build/bin/php-coveralls --coverage_clover=./.Build/logs/clover.xml --json_path=./.Build/logs/coveralls-upload.json -v + " + + phpstan: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + mkdir -p .cache + php -dxdebug.mode=off .Build/bin/phpstan analyze -c ./phpstan.neon --no-progress + " + + phpstan_generate_baseline: + image: ${IMAGE_PREFIX}core-testing-${DOCKER_PHP_IMAGE}:latest + user: "${HOST_UID}" + volumes: + - ${ROOT_DIR}:${ROOT_DIR} + working_dir: ${ROOT_DIR} + extra_hosts: + - "host.docker.internal:host-gateway" + environment: + COMPOSER_HOME: ".cache/composer-home" + COMPOSER_CACHE_DIR: ".cache/composer" + command: > + /bin/sh -c " + if [ ${SCRIPT_VERBOSE} -eq 1 ]; then + set -x + fi + mkdir -p .cache + php -dxdebug.mode=off .Build/bin/phpstan analyze -c ./phpstan.neon --generate-baseline=./phpstan-baseline.neon --allow-empty-baseline + " diff --git a/Classes/Command/ConvertCommand.php b/Classes/Command/ConvertCommand.php new file mode 100644 index 0000000..c2b49f1 --- /dev/null +++ b/Classes/Command/ConvertCommand.php @@ -0,0 +1,61 @@ +addArgument('in', InputArgument::REQUIRED, 'CSV file to convert'); + $this->addArgument('out', InputArgument::REQUIRED, 'Path to save the XLF file'); + $this->addArgument('rebuild', InputArgument::OPTIONAL, 'Force clean build of XLF file'); + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $io->title($this->getDescription()); + + $in = $input->getArgument('in') ?? ''; + $out = $input->getArgument('out') ?? ''; + + if (!str_starts_with('/', $out)) { + $out = Environment::getProjectPath() . '/' . $out; + } + + $rebuild = (bool) ($input->getArgument('rebuild') ?? false); + + if (!file_exists($in)) { + $io->error(sprintf('File "%s" does not exist', $in)); + return Command::FAILURE; + } + + $outDir = dirname($out); + if (!is_dir($outDir)) { + $io->error(sprintf('Target directory "%s" does not exist', $outDir)); + return Command::FAILURE; + } + if (!is_writable($outDir)) { + $io->error(sprintf('Target directory "%s" is not writable', $outDir)); + return Command::FAILURE; + } + + $convertService = GeneralUtility::makeInstance(ConvertService::class); + $statistic = $convertService->convert($in, $out, $rebuild); + + $io->table(['Language', 'Count'], array_map(fn ($k, $v) => [$k, $v], array_keys($statistic), $statistic)); + + return Command::SUCCESS; + } +} diff --git a/Classes/Domain/Model/Dto/Label.php b/Classes/Domain/Model/Dto/Label.php new file mode 100644 index 0000000..756a5d3 --- /dev/null +++ b/Classes/Domain/Model/Dto/Label.php @@ -0,0 +1,22 @@ +language === 'en'; + } +} diff --git a/Classes/Service/ConvertService.php b/Classes/Service/ConvertService.php new file mode 100644 index 0000000..41c5aa5 --- /dev/null +++ b/Classes/Service/ConvertService.php @@ -0,0 +1,61 @@ +csvReader->getFromFile($csvFilePath); + + foreach ($data as $language => $labels) { + if ($language !== 'en') { + $fileInfo = pathinfo($out); + + $targetFilename = sprintf('%s/%s.%s', $fileInfo['dirname'], $language, $fileInfo['basename']); + } else { + $targetFilename = $out; + } + + if (!$forceRebuild) { + $labels = $this->addExistingLabels($labels, $language, $targetFilename); + } + $stats[$language] = count($labels); + + $xml = $this->xlfFileService->generateLanguageFile($labels, $language); + GeneralUtility::writeFile($targetFilename, $xml); + } + + return $stats; + } + + /** + * @param Label[] $labels + * @return Label[] + */ + protected function addExistingLabels(array $labels, string $language, string $path): array + { + if (!file_exists($path)) { + return $labels; + } + $existingLabels = $this->xlfFileService->getLabels($path, $language); + + return array_merge($existingLabels, $labels); + } + +} diff --git a/Classes/Service/CsvReader.php b/Classes/Service/CsvReader.php new file mode 100644 index 0000000..e5a906d --- /dev/null +++ b/Classes/Service/CsvReader.php @@ -0,0 +1,45 @@ +setHeaderOffset(0); + + $header = $csvReader->getHeader(); + if ($header[0] !== 'key') { + throw new \RuntimeException('CSV file has no "key" column on 1st position', 1719919250); + } + if ($header[1] !== 'en') { + throw new \RuntimeException('CSV file has no "en" column on 2nd position', 1719919251); + } + + $labels = []; + foreach ($csvReader->getRecords() as $row) { + $key = $row['key']; + $default = $row['en']; + unset($row['key'], $row['en']); + + $labels['en'][$key] = new Label($key, 'en', $default); + + foreach ($row as $language => $translation) { + if (!$language) { + continue; + } + $labels[$language][$key] = new Label($key, $language, $default, $translation, 'yes'); + } + } + + return $labels; + } + +} diff --git a/Classes/Service/XlfFileService.php b/Classes/Service/XlfFileService.php new file mode 100644 index 0000000..bc263cb --- /dev/null +++ b/Classes/Service/XlfFileService.php @@ -0,0 +1,100 @@ +file->body->children() as $translationElement) { + if ($translationElement->getName() === 'trans-unit' && !isset($translationElement['restype'])) { + $approved = (string)($translationElement['approved'] ?? ''); + $parsedData[(string)$translationElement['id']][0] = [ + 'source' => (string)$translationElement->source, + 'target' => (string)$translationElement->target, + ]; + $id = (string)$translationElement['id']; + $newLabels[$id] = new Label( + $id, + $language, + (string)$translationElement->source, + (string)$translationElement->target, + $approved + ); + } + } + return $newLabels; + } + + + /** + * @param Label[] $labels + * @return string|bool + */ + public function generateLanguageFile(array $labels, string $targetLanguage = '') + { + $isATranslation = $targetLanguage !== 'en'; + $domDocument = new \DOMDocument('1.0', 'utf-8'); + $domDocument->formatOutput = true; + + $domFile = $domDocument->createElement('file'); + $domFile->appendChild(new \DOMAttr('source-language', 'en')); + if ($isATranslation) { + $domFile->appendChild(new \DOMAttr('target-language', $targetLanguage)); + } + $domFile->appendChild(new \DOMAttr('datatype', 'plaintext')); + $domFile->appendChild(new \DOMAttr('original', 'messages')); + $domFile->appendChild($domDocument->createElement('header')); + $domBody = $domDocument->createElement('body'); + $domFile->appendChild($domBody); + + $xliff = $domDocument->createElement('xliff'); + $xliff->appendChild(new \DOMAttr('version', '1.2')); + $xliff->appendChild(new \DOMAttr('xmlns:t3', 'http://typo3.org/schemas/xliff')); + $xliff->appendChild(new \DOMAttr('xmlns', 'urn:oasis:names:tc:xliff:document:1.2')); + $xliff->appendChild($domFile); + $domDocument->appendChild($xliff); + + foreach ($labels as $label) { + $transUnit = $domDocument->createElement('trans-unit'); + $transUnit->appendChild(new \DOMAttr('id', $label->key)); + $transUnit->appendChild(new \DOMAttr('resname', $label->key)); + if ($isATranslation && $label->approved) { + $transUnit->appendChild(new \DOMAttr('approved', $label->approved)); + } + $source = $domDocument->createElement('source'); + $source->appendChild($domDocument->createTextNode($label->source)); + $transUnit->appendChild($source); + if ($isATranslation) { + $target = $domDocument->createElement('target'); + $target->appendChild($domDocument->createTextNode($label->translation)); + $transUnit->appendChild($target); + } + + $domBody->appendChild($transUnit); + } + + return $domDocument->saveXML(); + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml new file mode 100644 index 0000000..0c84e50 --- /dev/null +++ b/Configuration/Services.yaml @@ -0,0 +1,21 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + StudioMitte\Csv2Xlf\: + resource: '../Classes/*' + + StudioMitte\Csv2Xlf\Command\ConvertCommand: + tags: + - name: 'console.command' + command: 'csv2xlf:convert' + description: 'Convert CSV to XLF' + schedulable: true + + StudioMitte\Csv2Xlf\Service\ConvertService: + public: true + + StudioMitte\Csv2Xlf\Service\XlfFileService: + public: true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..95d36a7 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,345 @@ +Some icons used in the TYPO3 project are retrieved from the "Silk" icon set of +Mark James, which can be found at http://famfamfam.com/lab/icons/silk/. This +set is distributed under a Creative Commons Attribution 2.5 License. The +license can be found at http://creativecommons.org/licenses/by/2.5/. +--------------------------------- + + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..eb4fb82 --- /dev/null +++ b/Readme.md @@ -0,0 +1,40 @@ +# TYPO3 Extension `csv2xlf` + + +[![TYPO3 12](https://img.shields.io/badge/TYPO3-12-orange.svg)](https://get.typo3.org/version/12) + +This extension provides a command to generate a XLF file and its translations from a CSV file. + +The typical use case is to provide an online Excel/Google Docs for clients to provide translations which then are converted to XLF files. + +## Installation + +```bash +composer require studiomitte/csv2xlf +``` + +## Usage + +CSV looks like this +```csv +"key","en","de" +"example","This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +"example2","Datenschutzhinweis (bs)","Das ist ein Beispiel" +``` + +With the following requirements: +- The first row is the header +- The header starts with `key`, followed by `en` and afterwards the language codes +- Default is always `en` + + +```bash +./bin/typo3 csv2xlf:convert packages/csv2xlf/Resources/Private/Examples/in.csv packages/csv2xlf/Resources/Private/Examples/out.xlf +``` + + +## Credits + +This extension was created by [Studio Mitte](https://studiomitte.com) with ♥. + +[Find more TYPO3 extensions we have developed](https://www.studiomitte.com/loesungen/typo3) that provide additional features for TYPO3 sites. diff --git a/Resources/Private/Examples/de.out.xlf b/Resources/Private/Examples/de.out.xlf new file mode 100644 index 0000000..b6915cb --- /dev/null +++ b/Resources/Private/Examples/de.out.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example NEW + Das ist ein Beispiel + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + Das ist ein Beispiel + + + + diff --git a/Resources/Private/Examples/in.csv b/Resources/Private/Examples/in.csv new file mode 100644 index 0000000..e3ffbe9 --- /dev/null +++ b/Resources/Private/Examples/in.csv @@ -0,0 +1,3 @@ +"key","en","de" +"example","This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +"example2","Datenschutzhinweis (bs)","Das ist ein Beispiel" diff --git a/Resources/Private/Examples/out.xlf b/Resources/Private/Examples/out.xlf new file mode 100644 index 0000000..97d438f --- /dev/null +++ b/Resources/Private/Examples/out.xlf @@ -0,0 +1,14 @@ + + + +
+ + + This is an example NEW + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + + + + diff --git a/Resources/Public/Icons/Extension.svg b/Resources/Public/Icons/Extension.svg new file mode 100644 index 0000000..ea26630 --- /dev/null +++ b/Resources/Public/Icons/Extension.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tests/Unit/Domain/Model/Dto/LabelTest.php b/Tests/Unit/Domain/Model/Dto/LabelTest.php new file mode 100644 index 0000000..ca921d0 --- /dev/null +++ b/Tests/Unit/Domain/Model/Dto/LabelTest.php @@ -0,0 +1,28 @@ +key); + self::assertSame('de', $label->language); + self::assertSame('source', $label->source); + self::assertSame('translation', $label->translation); + self::assertSame('no', $label->approved); + self::assertFalse($label->isDefault()); + } + + public function testIsDefault(): void + { + $label = new Label('key', 'en', 'source'); + self::assertTrue($label->isDefault()); + } +} diff --git a/Tests/Unit/Service/ConvertServiceTest.php b/Tests/Unit/Service/ConvertServiceTest.php new file mode 100644 index 0000000..265ca42 --- /dev/null +++ b/Tests/Unit/Service/ConvertServiceTest.php @@ -0,0 +1,28 @@ +convert($exampleFile, $out, $forceRebuild); + + self::assertFileEquals(__DIR__ . '/Fixtures/result.xlf', $out, 'Conversion failed for default'); + self::assertFileEquals(__DIR__ . '/Fixtures/de.result.xlf', __DIR__ . '/Result/de.result.xlf', 'Conversion failed for de'); + } +} diff --git a/Tests/Unit/Service/CsvReaderTest.php b/Tests/Unit/Service/CsvReaderTest.php new file mode 100644 index 0000000..6e36950 --- /dev/null +++ b/Tests/Unit/Service/CsvReaderTest.php @@ -0,0 +1,43 @@ +subject = new CsvReader(); + parent::setUp(); + } + + public function testForKeyAs1stHeaderField(): void + { + $this->expectExceptionCode(1719919250); + $this->subject->getFromFile(__DIR__ . '/Fixtures/reader/no-header.csv'); + } + + public function testForEnAs2ndHeaderField(): void + { + $this->expectExceptionCode(1719919251); + $this->subject->getFromFile(__DIR__ . '/Fixtures/reader/no-en.csv'); + } + + /** + * @throws \JsonException + */ + public function testCsvReading():void + { + $labels = $this->subject->getFromFile(__DIR__ . '/Fixtures/reader/valid.csv'); + $json = json_encode($labels, JSON_THROW_ON_ERROR|JSON_UNESCAPED_UNICODE); + + $this->assertJsonStringEqualsJsonFile(__DIR__ . '/Fixtures/reader/valid.json', $json); + } +} diff --git a/Tests/Unit/Service/Fixtures/de.result.xlf b/Tests/Unit/Service/Fixtures/de.result.xlf new file mode 100644 index 0000000..b6915cb --- /dev/null +++ b/Tests/Unit/Service/Fixtures/de.result.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example NEW + Das ist ein Beispiel + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + Das ist ein Beispiel + + + + diff --git a/Tests/Unit/Service/Fixtures/example.csv b/Tests/Unit/Service/Fixtures/example.csv new file mode 100644 index 0000000..e3ffbe9 --- /dev/null +++ b/Tests/Unit/Service/Fixtures/example.csv @@ -0,0 +1,3 @@ +"key","en","de" +"example","This is an example NEW","Das ist ein Beispiel","Ceci est un exemple" +"example2","Datenschutzhinweis (bs)","Das ist ein Beispiel" diff --git a/Tests/Unit/Service/Fixtures/reader/no-en.csv b/Tests/Unit/Service/Fixtures/reader/no-en.csv new file mode 100644 index 0000000..f99167a --- /dev/null +++ b/Tests/Unit/Service/Fixtures/reader/no-en.csv @@ -0,0 +1,2 @@ +"key","de","en" +"example","Example","Beispiel" diff --git a/Tests/Unit/Service/Fixtures/reader/no-header.csv b/Tests/Unit/Service/Fixtures/reader/no-header.csv new file mode 100644 index 0000000..51f48b4 --- /dev/null +++ b/Tests/Unit/Service/Fixtures/reader/no-header.csv @@ -0,0 +1,2 @@ +"example","This is an example","Das ist ein Beispiel" +"example2","This is an example NEW","" diff --git a/Tests/Unit/Service/Fixtures/reader/valid.csv b/Tests/Unit/Service/Fixtures/reader/valid.csv new file mode 100644 index 0000000..bac61d3 --- /dev/null +++ b/Tests/Unit/Service/Fixtures/reader/valid.csv @@ -0,0 +1,5 @@ +"key","en","de","es","it" +"example","This is an example","Das ist ein Beispiel","Este es un ejemplo","Questo è un esempio" +"firstName","First name","Vorname","Nombre","Nome" +"lastName","Last name","Nachname","Apellido","Cognome" +"empty","This is an empty value","","","" diff --git a/Tests/Unit/Service/Fixtures/reader/valid.json b/Tests/Unit/Service/Fixtures/reader/valid.json new file mode 100644 index 0000000..f79e617 --- /dev/null +++ b/Tests/Unit/Service/Fixtures/reader/valid.json @@ -0,0 +1,122 @@ +{ + "en": { + "example": { + "key": "example", + "language": "en", + "source": "This is an example", + "translation": "", + "approved": "" + }, + "firstName": { + "key": "firstName", + "language": "en", + "source": "First name", + "translation": "", + "approved": "" + }, + "lastName": { + "key": "lastName", + "language": "en", + "source": "Last name", + "translation": "", + "approved": "" + }, + "empty": { + "key": "empty", + "language": "en", + "source": "This is an empty value", + "translation": "", + "approved": "" + } + }, + "de": { + "example": { + "key": "example", + "language": "de", + "source": "This is an example", + "translation": "Das ist ein Beispiel", + "approved": "yes" + }, + "firstName": { + "key": "firstName", + "language": "de", + "source": "First name", + "translation": "Vorname", + "approved": "yes" + }, + "lastName": { + "key": "lastName", + "language": "de", + "source": "Last name", + "translation": "Nachname", + "approved": "yes" + }, + "empty": { + "key": "empty", + "language": "de", + "source": "This is an empty value", + "translation": "", + "approved": "yes" + } + }, + "es": { + "example": { + "key": "example", + "language": "es", + "source": "This is an example", + "translation": "Este es un ejemplo", + "approved": "yes" + }, + "firstName": { + "key": "firstName", + "language": "es", + "source": "First name", + "translation": "Nombre", + "approved": "yes" + }, + "lastName": { + "key": "lastName", + "language": "es", + "source": "Last name", + "translation": "Apellido", + "approved": "yes" + }, + "empty": { + "key": "empty", + "language": "es", + "source": "This is an empty value", + "translation": "", + "approved": "yes" + } + }, + "it": { + "example": { + "key": "example", + "language": "it", + "source": "This is an example", + "translation": "Questo è un esempio", + "approved": "yes" + }, + "firstName": { + "key": "firstName", + "language": "it", + "source": "First name", + "translation": "Nome", + "approved": "yes" + }, + "lastName": { + "key": "lastName", + "language": "it", + "source": "Last name", + "translation": "Cognome", + "approved": "yes" + }, + "empty": { + "key": "empty", + "language": "it", + "source": "This is an empty value", + "translation": "", + "approved": "yes" + } + } +} diff --git a/Tests/Unit/Service/Fixtures/result.xlf b/Tests/Unit/Service/Fixtures/result.xlf new file mode 100644 index 0000000..97d438f --- /dev/null +++ b/Tests/Unit/Service/Fixtures/result.xlf @@ -0,0 +1,14 @@ + + + +
+ + + This is an example NEW + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + + + + diff --git a/Tests/Unit/Service/Result/.gitkeep b/Tests/Unit/Service/Result/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/Tests/Unit/Service/Result/de.result.xlf b/Tests/Unit/Service/Result/de.result.xlf new file mode 100644 index 0000000..b6915cb --- /dev/null +++ b/Tests/Unit/Service/Result/de.result.xlf @@ -0,0 +1,16 @@ + + + +
+ + + This is an example NEW + Das ist ein Beispiel + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + Das ist ein Beispiel + + + + diff --git a/Tests/Unit/Service/Result/result.xlf b/Tests/Unit/Service/Result/result.xlf new file mode 100644 index 0000000..97d438f --- /dev/null +++ b/Tests/Unit/Service/Result/result.xlf @@ -0,0 +1,14 @@ + + + +
+ + + This is an example NEW + + + <![CDATA[<h3>Datenschutzhinweis (bs)</h3> + + + + diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9181d74 --- /dev/null +++ b/composer.json @@ -0,0 +1,31 @@ +{ + "name": "studiomitte/csv2xlf", + "description": "Convert CSV to XLF", + "license": "GPL-2.0-or-later", + "type": "typo3-cms-extension", + "authors": [ + { + "name": "Georg Ringer", + "role": "Developer" + } + ], + "require": { + "typo3/cms-core": "^12, ^13", + "league/csv": "^9.16" + }, + "autoload": { + "psr-4": { + "StudioMitte\\Csv2Xlf\\": "Classes" + } + }, + "autoload-dev": { + "psr-4": { + "StudioMitte\\Csv2Xlf\\Tests\\": "Tests" + } + }, + "extra": { + "typo3/cms": { + "extension-key": "csv2xlf" + } + } +} diff --git a/ext_emconf.php b/ext_emconf.php new file mode 100644 index 0000000..67f64d9 --- /dev/null +++ b/ext_emconf.php @@ -0,0 +1,21 @@ + 'Integration of Friendly Captcha', + 'description' => 'FriendlyCaptcha Integration for EXT:powermail and EXT:form and your custom implementation', + 'category' => 'plugin', + 'constraints' => [ + 'depends' => [ + 'typo3' => '11.5.0-12.9.99', + ], + 'conflicts' => [ + ], + ], + 'autoload' => [ + 'psr-4' => [ + 'StudioMitte\\FriendlyCaptcha\\' => 'Classes', + ], + ], + 'state' => 'beta', + 'version' => '0.1.6', +];