diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..03211e4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +vendor +var +bin +.idea \ No newline at end of file diff --git a/README.md b/README.md index 5a2c409..72a760f 100755 --- a/README.md +++ b/README.md @@ -34,4 +34,9 @@ The highest quality of translation is ensured by artificial intelligence. Extension uses [Open AI API](https://openai.com/product) - ChatGPT v3.5 ### Requirements: -* Akeneo PIM >= 6.x \ No newline at end of file +* Akeneo PIM >= 6.x + +## Contact +`Akeneo OpenAI translator` is brought to you by [Macopedia](https://macopedia.com/). + +[Contact us](https://macopedia.com/contact) \ No newline at end of file diff --git a/composer.json b/composer.json index cfc2834..78d4a7c 100755 --- a/composer.json +++ b/composer.json @@ -21,7 +21,8 @@ } }, "require": { - "orhanerday/open-ai": "^4.7" + "orhanerday/open-ai": "^4.7", + "akeneo/pim-community-dev": "^6.0.0||^7.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "^3.15", @@ -30,19 +31,20 @@ }, "scripts": { "phpstan": [ - "vendor/bin/phpstan --configuration=phpstan.neon" + "vendor/bin/phpstan analyse --configuration=\"config/phpstan.neon\"" ], "phpunit": [ - "vendor/bin/phpunit --colors=always" + "vendor/bin/phpunit --colors=always --configuration=\"config/phpunit.xml.dist\"" + ], + "cs:fix": [ + "vendor/bin/php-cs-fixer fix --config=\"config/.php-cs-fixer.php\" --using-cache no --allow-risky=yes" ], "test": [ - "vendor/bin/php-cs-fixer fix --dry-run", + "@cs:fix", "@phpstan", "@phpunit" - ], - "cs:fix": [ - "vendor/bin/php-cs-fixer fix" ] + }, "minimum-stability": "stable", "config": { diff --git a/config/.php-cs-fixer.php b/config/.php-cs-fixer.php new file mode 100644 index 0000000..338b1ce --- /dev/null +++ b/config/.php-cs-fixer.php @@ -0,0 +1,45 @@ +in(__DIR__ . '/../') + ->exclude( + [ + 'var', + 'node_modules', + 'upgrades', + ] + )->notPath( + [ + 'public/index.php', + 'public/api.php', + 'config/bootstrap.php', + 'src/Kernel.php', + ] + ); + +return (new PhpCsFixer\Config()) + ->setCacheFile(__DIR__ . '/../var/.php_cs.cache') + ->setRules( + [ + '@PSR12' => true, + 'blank_line_after_opening_tag' => true, + 'braces' => ['allow_single_line_closure' => true], + 'compact_nullable_typehint' => true, + 'single_quote' => true, + 'concat_space' => ['spacing' => 'one'], + 'declare_equal_normalize' => ['space' => 'none'], + 'function_typehint_space' => true, + 'new_with_braces' => true, + 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], + 'no_empty_statement' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_whitespace_in_blank_line' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'single_trait_insert_per_statement' => true, + 'array_syntax' => ['syntax' => 'short'], + 'global_namespace_import' => ['import_classes' => true, 'import_constants' => true, 'import_functions' => true], + 'declare_strict_types' => true + ] + ) + ->setFinder($finder); diff --git a/config/phpstan.neon b/config/phpstan.neon new file mode 100644 index 0000000..8e20c9c --- /dev/null +++ b/config/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: 6 + paths: + - ../src + #- ../tests + tmpDir: ../var/phpstan diff --git a/config/phpunit.xml.dist b/config/phpunit.xml.dist new file mode 100644 index 0000000..4899693 --- /dev/null +++ b/config/phpunit.xml.dist @@ -0,0 +1,19 @@ + + + + + + src + + + + + + + + + + ../tests + + + diff --git a/src/Macopedia/Translator/Client/OpenAiClient.php b/src/Macopedia/Translator/Client/OpenAiClient.php index f68f6ad..c484c98 100644 --- a/src/Macopedia/Translator/Client/OpenAiClient.php +++ b/src/Macopedia/Translator/Client/OpenAiClient.php @@ -1,7 +1,10 @@ client->chat($message); if ($response === false) { - throw new InvalidOpenAiResponseException(); + throw new InvalidOpenAiResponseException($message); } $response = Response::fromArray(json_decode($response, true)); @@ -50,6 +53,8 @@ public function ask(string $role, string $content): ?Response return $response; } + + #[ArrayShape(['model' => "string", 'messages' => "\string[][]"])] private function generateMessage(string $role, string $content): array { return [ @@ -61,4 +66,4 @@ private function generateMessage(string $role, string $content): array ] ]; } -} \ No newline at end of file +} diff --git a/src/Macopedia/Translator/Client/OpenAiClient/Choice.php b/src/Macopedia/Translator/Client/OpenAiClient/Choice.php index 5ed8575..b35755d 100644 --- a/src/Macopedia/Translator/Client/OpenAiClient/Choice.php +++ b/src/Macopedia/Translator/Client/OpenAiClient/Choice.php @@ -1,5 +1,7 @@ message; } -} \ No newline at end of file +} diff --git a/src/Macopedia/Translator/Client/OpenAiClient/Choices.php b/src/Macopedia/Translator/Client/OpenAiClient/Choices.php index beadff1..9d3d5f6 100644 --- a/src/Macopedia/Translator/Client/OpenAiClient/Choices.php +++ b/src/Macopedia/Translator/Client/OpenAiClient/Choices.php @@ -1,5 +1,7 @@ answers; } -} \ No newline at end of file +} diff --git a/src/Macopedia/Translator/Client/OpenAiClient/Message.php b/src/Macopedia/Translator/Client/OpenAiClient/Message.php index d0adc69..ad4448b 100644 --- a/src/Macopedia/Translator/Client/OpenAiClient/Message.php +++ b/src/Macopedia/Translator/Client/OpenAiClient/Message.php @@ -1,5 +1,7 @@ content; } -} \ No newline at end of file +} diff --git a/src/Macopedia/Translator/Client/OpenAiClient/Response.php b/src/Macopedia/Translator/Client/OpenAiClient/Response.php index d068263..b089f20 100644 --- a/src/Macopedia/Translator/Client/OpenAiClient/Response.php +++ b/src/Macopedia/Translator/Client/OpenAiClient/Response.php @@ -1,21 +1,23 @@ setTimestamp($data['created']), + (new DateTime())->setTimestamp($data['created']), $data['model'], new Choices($data['choices']), $data['usage'] @@ -54,4 +56,4 @@ public function getFirstChoiceMessage(): ?string } return $firstMessage->getMessage()->getContent(); } -} \ No newline at end of file +} diff --git a/src/Macopedia/Translator/Connector/Processor/MassEdit/TranslateAttributesProcessor.php b/src/Macopedia/Translator/Connector/Processor/MassEdit/TranslateAttributesProcessor.php index bd8e5ff..ac730d8 100644 --- a/src/Macopedia/Translator/Connector/Processor/MassEdit/TranslateAttributesProcessor.php +++ b/src/Macopedia/Translator/Connector/Processor/MassEdit/TranslateAttributesProcessor.php @@ -1,30 +1,21 @@ 'ecommerce', * 'sourceLocale' => 'pl_PL', * 'targetLocale' => 'en_US', - * 'translatedAttributes' => [ + * 'attributesToTranslate' => [ * 'name', * 'description', * ] @@ -54,44 +45,6 @@ public function process(mixed $item): ProductInterface|ProductModelInterface */ private function translateAttributes(mixed $product, array $action): ProductInterface|ProductModelInterface { - $sourceScope = $action['sourceChannel']; - $targetScope = $action['targetChannel']; - $sourceLocaleAkeneo = $action['sourceLocale']; - $targetLocaleAkeneo = $action['targetLocale']; - //$sourceLocale = Language::fromCode(explode('_', $sourceLocaleAkeneo)[0]); - $targetLocale = Language::fromCode(explode('_', $targetLocaleAkeneo)[0]); - $attributeCodes = $action['translatedAttributes']; - - foreach ($attributeCodes as $attributeCode) { - /** @var AttributeInterface|null $attribute */ - $attribute = $this->attributeRepository->findOneByIdentifier($attributeCode); - if (!$this->checkAttributeEditable->isEditable($product, $attribute)) { - continue; - } - - if (!$attribute->isScopable()) { - $sourceScope = null; - $targetScope = null; - } - /** @var ValueInterface|null $attributeValue */ - $attributeValue = $product->getValue($attributeCode, $sourceLocaleAkeneo, $sourceScope); - if ($attributeValue === null) { - continue; - } - - $sourceText = $attributeValue->getData(); - - $translatedText = $this->translator->translate( - $sourceText, - $targetLocale - ); - - $this->propertySetter->setData($product, $attributeCode, $translatedText, [ - 'locale' => $targetLocaleAkeneo, - 'scope' => $targetScope, - ]); - } - - return $product; + return $this->translateAttributesService->translateAttributes($product, $action); } } diff --git a/src/Macopedia/Translator/DependencyInjection/MacopediaTranslatorExtension.php b/src/Macopedia/Translator/DependencyInjection/MacopediaTranslatorExtension.php index a642208..1ef4b02 100644 --- a/src/Macopedia/Translator/DependencyInjection/MacopediaTranslatorExtension.php +++ b/src/Macopedia/Translator/DependencyInjection/MacopediaTranslatorExtension.php @@ -1,5 +1,7 @@ load('connector.yml'); $loader->load('services.yml'); } } diff --git a/src/Macopedia/Translator/Exception/InvalidOpenAiResponseException.php b/src/Macopedia/Translator/Exception/InvalidOpenAiResponseException.php index 187407a..ed975a0 100644 --- a/src/Macopedia/Translator/Exception/InvalidOpenAiResponseException.php +++ b/src/Macopedia/Translator/Exception/InvalidOpenAiResponseException.php @@ -1,11 +1,15 @@
- 0){ %> readonly="true"<% } %>> <% _.each(attributes, function (attribute){ %> - <% }); %> @@ -22,7 +22,7 @@
- 0){ %> readonly="true"<% } %>> <% _.each(channels, function (channel, key){ %> <% }); %> @@ -36,7 +36,7 @@
- 0){ %> readonly="true"<% } %>> <% _.each(locales, function (locale, key){ %> <% }); %> @@ -50,7 +50,7 @@
- 0){ %> readonly="true"<% } %>> <% _.each(channels, function (channel, key){ %> <% }); %> @@ -64,7 +64,7 @@
- 0){ %> readonly="true"<% } %>> <% _.each(locales, function (locale, key){ %> <% }); %> diff --git a/src/Macopedia/Translator/Resources/translations/messages.fr.yml b/src/Macopedia/Translator/Resources/translations/messages.fr.yml new file mode 100644 index 0000000..362e68f --- /dev/null +++ b/src/Macopedia/Translator/Resources/translations/messages.fr.yml @@ -0,0 +1,3 @@ +macopedia: + acl: + label: "Originaux d'attributs groupés" \ No newline at end of file diff --git a/src/Macopedia/Translator/Service/TranslateAttributesService.php b/src/Macopedia/Translator/Service/TranslateAttributesService.php new file mode 100644 index 0000000..292bbb3 --- /dev/null +++ b/src/Macopedia/Translator/Service/TranslateAttributesService.php @@ -0,0 +1,86 @@ +> $action + */ + public function translateAttributes(mixed $product, array $action): ProductInterface|ProductModelInterface + { + [$sourceScope, $targetScope, $sourceLocaleAkeneo, $targetLocaleAkeneo, $targetLocale, $attributesToTranslate] = $this->extractVariables($action); + + foreach ($attributesToTranslate as $attributeCode) { + $attribute = $this->attributeRepository->findOneByIdentifier($attributeCode); + if (!$this->checkAttributeEditable->isEditable($product, $attribute)) { + continue; + } + + if (!($attribute instanceof ScalarValue)) { + continue; + } + + if (!$attribute->isScopable()) { + $sourceScope = null; + $targetScope = null; + } + + $attributeValue = $product->getValue($attributeCode, $sourceLocaleAkeneo, $sourceScope); + if ($attributeValue === null) { + continue; + } + + $translatedText = $this->translator->translate( + $attributeValue->getData(), + $targetLocale + ); + + $this->propertySetter->setData($product, $attributeCode, $translatedText, [ + 'locale' => $targetLocaleAkeneo, + 'scope' => $targetScope, + ]); + } + + return $product; + } + + private function extractVariables(array $action): array + { + Assert::keyExists($action, 'sourceChannel'); + Assert::keyExists($action, 'targetChannel'); + Assert::keyExists($action, 'sourceLocale'); + Assert::keyExists($action, 'targetLocale'); + Assert::keyExists($action, 'attributesToTranslate'); + + return [ + 'sourceScope' => $action['sourceChannel'], + 'targetScope' => $action['targetChannel'], + 'sourceLocaleAkeneo' => $action['sourceLocale'], + 'targetLocaleAkeneo' => $action['targetLocale'], + 'targetLocale' => Language::fromCode(explode('_', $action['targetLocale'])[0]), + 'attributesToTranslate' => $action['attributesToTranslate'] + ]; + } +} diff --git a/src/Macopedia/Translator/Translator/Language.php b/src/Macopedia/Translator/Translator/Language.php index 9d0fc1e..9bf3fd9 100644 --- a/src/Macopedia/Translator/Translator/Language.php +++ b/src/Macopedia/Translator/Translator/Language.php @@ -1,5 +1,6 @@