diff --git a/Readme.md b/Readme.md index 40a8947..d3a8a92 100644 --- a/Readme.md +++ b/Readme.md @@ -1,6 +1,14 @@ # Shopware Translation Bridge -This plugin provides a bridge to connect Shopware with any Translation Provider that is supported by the [Symfony Translation Component](https://symfony.com/doc/current/translation.html#translation-providers). It allows you to manage your storefront snippets via a third-party translation service. +This plugin provides a bridge to connect Shopware with any translation provider supported by the [Symfony Translation Component](https://symfony.com/doc/current/translation.html#translation-providers). It lets you manage your storefront snippets through a third-party translation service (e.g. Tolgee, Crowdin, Lokalise) instead of maintaining them by hand in the administration. + +At its core the plugin does two things: it applies provider translations to the storefront at runtime, and it provides CLI commands and an API to synchronise snippets between Shopware and the provider in both directions. + +## Requirements + +* PHP 8.3+ +* Shopware 6.7.1 or higher (`shopware/core` and `shopware/storefront`) +* `symfony/translation` 7.x ## Installation @@ -11,91 +19,93 @@ bin/console plugin:install --activate ShopwareTranslationBridge ## Configuration -The connection to the translation provider is configured via a DSN (Data Source Name). You need to create a configuration file, for example `config/packages/shopware_translation_bridge.yaml`, to set up the providers. +The Symfony translation providers themselves (their DSNs) are configured the usual Symfony way, e.g. in `config/packages/translation.yaml` under `framework.translator.providers`. The service name you give a provider there (for example `tolgee`) is the value you reference in the plugin settings. + +Everything specific to this plugin is configured as regular **Shopware plugin settings** — no extra config file needed. Open Administration → Extensions → My extensions → Translation Bridge → Config: -The plugin uses the DSN from the `ShopwareTranslationBridge.config.providerDsn` system config key as a default. You can also configure a specific DSN for each sales channel. +| Field | Type | Default | Description | +|---------------------------------|----------|---------|----------------------------------------------------------------------------------------------------------| +| Default translation provider | `string` | empty | Service name from `framework.translator.providers` (e.g. `tolgee`). Empty means no provider is used. | +| Respect local translation files | `bool` | `true` | Whether to overlay the snippets with the translation files from `framework.translator.default_path`. | -| option | type | default | info | -|---------------------------|----------------|---------|----------------------------------------------------------------------------------------------------| -| default_provider | `null\|string` | `null` | Service name from `framework.translator.providers`. If `null` there is no fallback provider. | -| respect_translation_files | `bool` | `true` | should it overlay the snippet files with the translation files `framework.translator.default_path` | -| sales_channel_providers | `array` | `[]` | SalesChannel specific providers. Like `default_provider` but individiual for every salesChannel | +Both fields can be overridden per sales channel using the sales channel selector at the top of the config screen — pick a sales channel, set a different value, and save. This uses Shopware's standard system-config inheritance: a sales channel without its own value automatically falls back to the global default. Leaving everything empty is a valid, safe state — the plugin simply stays inactive and does not alter Shopware's default translation behaviour. -### Example Configuration +## Commands + +### Pull snippets -Here is an example of how to configure different providers for different sales channels. +Pulls snippets from the configured provider(s) and writes them locally into the directory defined by `framework.translator.default_path`. -```yaml -# config/packages/shopware_translation_bridge.yaml -shopware_translation_bridge: - # Define a default provider for all sales channels - default_provider: 'providerServiceName' - respect_translation_files: true - sales_channel_providers: - # Assign a specific provider for a sales channel by its ID - 2b919afec10730f413cb5682bbed09fd: - provider: 'providerServiceName' +```bash +bin/console sw:snippets:pull ``` -## Commands +The command takes no arguments or options; it resolves everything from the plugin configuration: -This plugin provides three commands to manage translations. +* The global default provider (if configured) is pulled for all system locales and written to the `messages` translation domain. +* Every sales channel whose configured provider **differs** from the global default is pulled for that channel's locales and written to a domain named after the sales channel id. Sales channels that merely inherit the default are not pulled again — the `messages` domain already covers them. -### Push Snippets +If nothing is configured, the command prints a warning and exits without writing anything. -Pushes all local snippets to the configured translation provider. +### Push snippets + +Pushes local snippets to the configured provider. ```bash -bin/console sw:snippets:push [salesChannelId1] [salesChannelId2] +bin/console sw:snippets:push [salesChannelId ...] ``` **Arguments:** -* `salesChannelId` (optional, multiple): The sales channel ID(s) to push translations for. If "default" or empty, the default provider is used. +* `salesChannelId` (optional, multiple): the sales channel id(s) to push for. If omitted or set to `default`, the global default provider is used; otherwise the effective provider of each given sales channel is used. **Options:** -* `--force` / `-f`: Overwrite existing translations on the provider. -* `--delete-missing`: Delete translations on the provider that do not exist locally. -* `--locales` / `-l` (multiple): Specify the locales to push (e.g., `en-GB`, `de-DE`). If not provided, all relevant locales are pushed. +* `--force` / `-f`: overwrite translations that already exist on the provider (removes messages that are not synchronised). +* `--delete-missing`: delete translations on the provider that no longer exist locally. +* `--locales` / `-l` (multiple): restrict to specific locales (e.g. `de-DE`, `en-GB`). If omitted, all relevant locales are pushed. Locales that are not enabled cause the command to fail without touching the provider. -### Pull Snippets +### Flush translation cache -Pulls all snippets from the configured translation provider and saves them locally inside the translation directory defined by `framework.translator.default_path`. The default provider (if configured) is written to the `messages` translation domain, while every entry of `sales_channel_providers` is persisted to a domain that matches the configured sales channel id. +Invalidates the translation cache. Useful after a pull to make new translations visible in the storefront. ```bash -bin/console sw:snippets:pull [salesChannelId1] +bin/console sw:cache:translation:flush ``` -**Arguments:** +## API endpoints -* `salesChannelId` (optional, multiple): The sales channel ID(s) to pull translations for. If "default" or empty, the default provider is used. +Both endpoints live under the `/api` scope and require an authenticated admin API token. -**Options:** +### Trigger a translation update -* `--locales` / `-l` (multiple): Specify the locales to pull. If not provided, all relevant locales are pulled. +Dispatches an asynchronous translation refresh for the given sales channels. This is intended for webhooks from translation providers (e.g. fired when a translation job completes). -### Flush Translation Cache +* **URL:** `POST /api/_action/nlx-translation/update` +* **Body (JSON):** + ```json + { + "salesChannelIds": ["SALES_CHANNEL_ID_1", "SALES_CHANNEL_ID_2"] + } + ``` -Flushes the translation cache. This is useful after pulling new translations to make them visible in the storefront. +Sales channels without a resolvable provider are filtered out. If none remain, the endpoint responds with HTTP 503 and `errorMissingTranslationProvider`. -```bash -bin/console sw:cache:flush:translation -``` +### List available providers + +Returns the translation providers registered in `framework.translator.providers`. This backs the provider select field in the administration. -## API Endpoint +* **URL:** `GET /api/_action/nlx-translation/providers` -This plugin provides an API endpoint to trigger a translation update for specific sales channels. This is useful for integrating with webhooks from translation providers (e.g., when translations are completed). +## Asynchronous processing -* **URL:** `/api/_action/nlx/translation/update` -* **Method:** `POST` -* **Body (JSON):** - ```json - { - "salesChannelIds": ["SALES_CHANNEL_ID_1", "SALES_CHANNEL_ID_2"] - } - ``` +When the update endpoint is called, the sales channel ids are dispatched to the Shopware message queue in batches. A message handler processes the queue and, per sales channel, invalidates the translation cache and warms up the catalogue for each of the channel's domains — so the refresh happens in the background without blocking the request. -## Asynchronous Processing +## Development -When the API endpoint is called, a message is dispatched to the Shopware message queue for each specified sales channel. A message handler then processes the queue and updates the translations for each sales channel asynchronously in the background. +```bash +composer test # run the unit test suite (PHPUnit) +composer phpstan # static analysis +composer lint # mago lint +composer format:fix # mago formatter +``` diff --git a/src/Command/FlushTranslationCacheCommand.php b/src/Command/FlushTranslationCacheCommand.php index 6f08107..2a046bc 100644 --- a/src/Command/FlushTranslationCacheCommand.php +++ b/src/Command/FlushTranslationCacheCommand.php @@ -1,6 +1,6 @@ resolveTranslationPath(); - $this->ensureDirectoryExists($translationPath); $domainsFetched = 0; - if (is_string($this->defaultProvider) && $this->defaultProvider !== '') { - $domainsFetched += $this->fetchTranslations( - $this->defaultProvider, - self::REMOTE_DOMAIN, - $this->getAllLocales(), - $translationPath, - $io + if (!$this->translationProviderResolver->hasProvider()) { + $io->warning( + 'No translation provider configured. Configure a default provider or at least one sales channel provider.' ); + + return Command::SUCCESS; } - foreach ($this->salesChannelProviders as $salesChannelId => $providerName) { - if (!is_string($providerName)) { - continue; - } + $defaultProviderName = $this->configurationResolver->getProviderName(); + + $io->note('Fetching translations for keyProvider'); + $domainsFetched += $this->fetchTranslations( + $this->translationProviderResolver->getProvider(), + self::REMOTE_DOMAIN, + $this->getAllLocales(), + $translationPath, + $io + ); + + $salesChannelIdsWithProvider = $this->getSalesChannelIdsWithProviderOverride($defaultProviderName); + if (empty($salesChannelIdsWithProvider)) { + $io->note('No own sales-channels providers found.'); + } + foreach ($salesChannelIdsWithProvider as $salesChannelId) { + $io->note(sprintf('Fetching translations for Sales-Channel-ID: %s.', $salesChannelId)); $domainsFetched += $this->fetchTranslations( - $providerName, + $this->translationProviderResolver->getProvider($salesChannelId), $salesChannelId, $this->getLocalesForSalesChannel($salesChannelId), $translationPath, @@ -93,23 +102,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int * @param list $locales */ private function fetchTranslations( - string $providerName, + ProviderInterface $provider, string $targetDomain, array $locales, string $translationPath, SymfonyStyle $io ): int { + $providerName = $this->getProviderName($provider); + if ($locales === []) { $io->note(sprintf('Skipping "%s" because no locales were found.', $targetDomain)); return 0; } - if (!$this->providers->has($providerName)) { - throw new RuntimeException(sprintf('Provider "%s" not found.', $providerName)); - } - - $provider = $this->providers->get($providerName); $translationBag = $provider->read([self::REMOTE_DOMAIN], $locales); $written = 0; @@ -123,7 +129,9 @@ private function fetchTranslations( $newCatalogue = new MessageCatalogue($catalogue->getLocale()); $newCatalogue->add($messages, $targetDomain); - $this->translationWriter->write($newCatalogue, 'json', ['path' => $translationPath]); + $this->translationWriter->write($newCatalogue, 'json', [ + 'path' => $translationPath, + ]); ++$written; } @@ -143,12 +151,39 @@ private function fetchTranslations( return 1; } + private function getProviderName(ProviderInterface $provider): string + { + return parse_url((string) $provider, \PHP_URL_SCHEME) ?: 'unknown'; + } + + /** + * Returns sales channels whose configured provider differs from the global default, + * i.e. a real per-channel override. Channels that merely inherit the default are + * excluded so their translations are not pulled redundantly. + * + * @return list + */ + private function getSalesChannelIdsWithProviderOverride(string $defaultProviderName): array + { + $result = $this->salesChannelRepository->searchIds(new Criteria(), Context::createCLIContext()); + + return array_values(array_filter( + $result->getIds(), + function (string $salesChannelId) use ($defaultProviderName): bool { + $providerName = $this->configurationResolver->getProviderName($salesChannelId); + + return $providerName !== null && $providerName !== $defaultProviderName; + } + )); + } + /** * @return list */ private function getAllLocales(): array { - $criteria = new Criteria()->addAssociation('locale'); + $criteria = new Criteria() + ->addAssociation('locale'); $result = $this->languageRepository->search($criteria, Context::createCLIContext()); $languages = $result->getEntities(); assert($languages instanceof LanguageCollection); @@ -201,12 +236,6 @@ private function resolveTranslationPath(): string private function ensureDirectoryExists(string $path): void { - if (is_dir($path)) { - return; - } - - if (!mkdir($path, 0777, true) && !is_dir($path)) { - throw new RuntimeException(sprintf('Unable to create translation directory "%s".', $path)); - } + (new Filesystem())->mkdir($path); } } diff --git a/src/Command/PushSnippetsCommand.php b/src/Command/PushSnippetsCommand.php index 8bbadea..3a29857 100644 --- a/src/Command/PushSnippetsCommand.php +++ b/src/Command/PushSnippetsCommand.php @@ -1,6 +1,6 @@ translationProviderResolver->getDefaultProvider()]; + return [$this->translationProviderResolver->getProvider()]; } $providers = []; foreach ($salesChannelIds as $salesChannelId) { - $providers[] = $this->translationProviderResolver->getSalesChannelProvider($salesChannelId); + $providers[] = $this->translationProviderResolver->getProvider($salesChannelId); } return $providers; diff --git a/src/Core/Framework/Adapter/Translator/TranslationCacheInvalidation.php b/src/Core/Framework/Adapter/Translator/TranslationCacheInvalidation.php index 0cc2c61..b6df16f 100644 --- a/src/Core/Framework/Adapter/Translator/TranslationCacheInvalidation.php +++ b/src/Core/Framework/Adapter/Translator/TranslationCacheInvalidation.php @@ -1,6 +1,6 @@ ['api'], + '_acl' => ['system_config:read'], + ], + methods: ['GET'] +)] +class TranslationProviderOptionsController extends AbstractController +{ + public function __construct( + #[Autowire(service: 'translation.provider_collection')] + private readonly TranslationProviderCollection $providers, + ) { + } + + public function __invoke(): JsonApiResponse + { + $options = array_map( + static fn (string $name): array => [ + 'value' => $name, + 'label' => $name, + ], + $this->providers->keys() + ); + + return new JsonApiResponse([ + 'options' => $options, + ]); + } +} diff --git a/src/Core/Framework/Api/Controller/UpdateTranslationController.php b/src/Core/Framework/Api/Controller/UpdateTranslationController.php index c8db77b..f6d1f4b 100644 --- a/src/Core/Framework/Api/Controller/UpdateTranslationController.php +++ b/src/Core/Framework/Api/Controller/UpdateTranslationController.php @@ -1,6 +1,6 @@ ['api'], - '_acl' => ['system:cache:info'] + '_acl' => ['system:cache:info'], ], methods: ['POST'] )] class UpdateTranslationController extends AbstractController { - function __construct( + public function __construct( private readonly EntityRepository $salesChannelRepository, private readonly MessageBusInterface $messageBus, private readonly TranslationProviderResolverInterface $translationProviderResolver, @@ -34,24 +34,23 @@ function __construct( ) { } - function __invoke(): JsonApiResponse + public function __invoke(): JsonApiResponse { $salesChannelIds = $this->salesChannelRepository ->searchIds(new Criteria(), Context::createCLIContext()) ->getIds(); - // If there is no default provider we only have to update the salesChannels which have translation provider - if (!$this->translationProviderResolver->hasDefaultProvider()) { - $salesChannelIds = array_filter( - $salesChannelIds, - $this->translationProviderResolver->hasSalesChannelProvider(...) - ); - } + // Keep only sales channels that resolve to a provider. Thanks to Shopware's config + // inheritance this covers both a global default and channel-specific overrides. + $salesChannelIds = array_values(array_filter( + $salesChannelIds, + $this->translationProviderResolver->hasProvider(...) + )); if ($salesChannelIds === []) { return new JsonApiResponse([ 'success' => false, - 'error' => 'errorMissingTranslationProvider' + 'error' => 'errorMissingTranslationProvider', ], Response::HTTP_SERVICE_UNAVAILABLE); } @@ -59,6 +58,8 @@ function __invoke(): JsonApiResponse $this->messageBus->dispatch(new TranslationUpdateMessage(...$chunk)); } - return new JsonApiResponse(['success' => true]); + return new JsonApiResponse([ + 'success' => true, + ]); } } diff --git a/src/Core/System/RelevantLocaleResolver.php b/src/Core/System/RelevantLocaleResolver.php index 7930570..ff299ce 100644 --- a/src/Core/System/RelevantLocaleResolver.php +++ b/src/Core/System/RelevantLocaleResolver.php @@ -1,6 +1,6 @@ merge($salesChannelLanguages); } - return array_filter($languages->map(fn(LanguageEntity $language) => $language->getLocale()?->getCode())); + return array_filter($languages->map(fn (LanguageEntity $language) => $language->getLocale()?->getCode())); } } diff --git a/src/Core/System/RelevantLocaleResolverInterface.php b/src/Core/System/RelevantLocaleResolverInterface.php index d4c1de3..6d07fae 100644 --- a/src/Core/System/RelevantLocaleResolverInterface.php +++ b/src/Core/System/RelevantLocaleResolverInterface.php @@ -1,6 +1,6 @@ salesChannelId !== null + && $this->configurationResolver->respectTranslationFiles($extension->salesChannelId); + // Force usage of translation files - if ($this->respectTranslationFiles) { + if ($respectTranslationFiles) { $this->respectTranslationFiles($extension); } @@ -42,6 +44,7 @@ public function __invoke(StorefrontSnippetsExtension $extension): void public static function skip(callable $callback): void { self::$skip = true; + try { $callback(); } finally { @@ -71,10 +74,7 @@ private function applyProviderTranslations(StorefrontSnippetsExtension $extensio $provider = $this->providerResolver->getProvider($extension->salesChannelId); - $locales = array_unique(array_filter([ - $extension->locale, - $extension->fallbackLocale - ])); + $locales = array_unique(array_filter([$extension->locale, $extension->fallbackLocale])); $translationBag = $provider->read([self::TRANSLATION_DOMAIN], $locales); diff --git a/src/Core/System/Snippet/SalesChannelTranslationRefresher.php b/src/Core/System/Snippet/SalesChannelTranslationRefresher.php index 47b2e64..3d0b277 100644 --- a/src/Core/System/Snippet/SalesChannelTranslationRefresher.php +++ b/src/Core/System/Snippet/SalesChannelTranslationRefresher.php @@ -1,6 +1,6 @@ addFilter(new EqualsAnyFilter('salesChannelId', $salesChannelIds)); $criteria->addAssociation('language.locale'); + try { $this->salesChannelDomainRepository->search($criteria, Context::createCLIContext())->map( $this->warmUpTranslation(...) @@ -47,7 +48,9 @@ private function warmUpTranslation(SalesChannelDomainEntity $domain): void $this->translator->injectSettings( $domain->getSalesChannelId(), $domain->getLanguageId(), - $domain->getLanguage()->getLocale()->getCode(), + $domain->getLanguage() + ->getLocale() + ->getCode(), Context::createCLIContext() ); $this->translator->getCatalogue(); diff --git a/src/Core/System/Snippet/SalesChannelTranslationRefresherInterface.php b/src/Core/System/Snippet/SalesChannelTranslationRefresherInterface.php index 5ad68c8..b89c05d 100644 --- a/src/Core/System/Snippet/SalesChannelTranslationRefresherInterface.php +++ b/src/Core/System/Snippet/SalesChannelTranslationRefresherInterface.php @@ -1,6 +1,6 @@ defaultProvider !== null && $this->providerCollection->has($this->defaultProvider); - } - - public function getDefaultProvider(): ProviderInterface - { - if (!$this->hasDefaultProvider()) { - throw new RuntimeException(\sprintf('Provider "%s" not found.', $this->defaultProvider)); - } - - return $this->providers['default'] = $this->providerCollection->get($this->defaultProvider); - } - - public function hasSalesChannelProvider(string $salesChannelId): bool + public function hasProvider(?string $salesChannelId = null): bool { - if (!Uuid::isValid($salesChannelId)) { - throw new InvalidArgumentException(\sprintf('Provider "%s" is not a valid UUID.', $salesChannelId)); - } - - $providerName = $this->providerMap[$salesChannelId] ?? null; + $providerName = $this->configurationResolver->getProviderName($salesChannelId); return $providerName !== null && $this->providerCollection->has($providerName); } - public function getSalesChannelProvider(string $salesChannelId): ProviderInterface + public function getProvider(?string $salesChannelId = null): ProviderInterface { - if (array_key_exists($salesChannelId, $this->providers)) { - return $this->providers[$salesChannelId]; - } + $providerName = $this->configurationResolver->getProviderName($salesChannelId); - if (!$this->hasProvider($salesChannelId)) { - throw new RuntimeException(\sprintf('No provider for salesChannel "%s" not found.', $salesChannelId)); + if ($providerName === null) { + throw $salesChannelId === null + ? new MissingDefaultProviderException(code: 1785932654) + : new MissingSalesChannelProviderException(salesChannelId: $salesChannelId, code: 1785932765); } - $providerName = $this->providerMap[$salesChannelId]; - assert(is_string($providerName), 'Provider map value must be string'); - - return $this->providers[$salesChannelId] = $this->providerCollection->get($providerName); - } - - public function hasProvider(string $salesChannelId): bool - { - return $this->hasDefaultProvider() || $this->hasSalesChannelProvider($salesChannelId); - } - - public function getProvider(string $salesChannelId): ProviderInterface - { - if ($this->hasSalesChannelProvider($salesChannelId)) { - return $this->getSalesChannelProvider($salesChannelId); - } - if ($this->hasDefaultProvider()) { - return $this->getDefaultProvider(); + if (!$this->providerCollection->has($providerName)) { + throw new UnknownProviderException( + providerName: $providerName, + salesChannelId: $salesChannelId, + code: 1785932876 + ); } - throw new RuntimeException(\sprintf('No provider for salesChannel "%s" not found.', $salesChannelId)); - } - - public function reset(): void - { - unset($this->providers); + return $this->providerCollection->get($providerName); } } diff --git a/src/Core/System/Snippet/TranslationProviderResolverInterface.php b/src/Core/System/Snippet/TranslationProviderResolverInterface.php index c288d15..e7e1681 100644 --- a/src/Core/System/Snippet/TranslationProviderResolverInterface.php +++ b/src/Core/System/Snippet/TranslationProviderResolverInterface.php @@ -1,6 +1,6 @@ systemConfigService->getBool(self::KEY_RESPECT_TRANSLATION_FILES, $salesChannelId); + } + + /** + * Returns the effective provider name for the given scope. + * + * Relies on Shopware's SystemConfig inheritance: with a sales channel id the + * channel-specific value is returned if set, otherwise the global value. Returns + * null when nothing is configured so callers can safely no-op on a fresh install. + */ + public function getProviderName(?string $salesChannelId = null): ?string + { + $providerName = $this->systemConfigService->getString(self::KEY_DEFAULT_PROVIDER, $salesChannelId); + + return $providerName !== '' ? $providerName : null; + } +} diff --git a/src/Resources/app/administration/src/component/nlx-translation-provider-select/index.js b/src/Resources/app/administration/src/component/nlx-translation-provider-select/index.js new file mode 100644 index 0000000..ce7ffd9 --- /dev/null +++ b/src/Resources/app/administration/src/component/nlx-translation-provider-select/index.js @@ -0,0 +1,58 @@ +import template from './nlx-translation-provider-select.html.twig'; + +export default { + template, + + inject: ['nlxTranslationApiService'], + + props: { + value: { + type: String, + required: false, + default: null, + }, + label: { + required: false, + default: null, + }, + helpText: { + required: false, + default: null, + }, + error: { + type: Object, + required: false, + default: null, + }, + }, + + emits: ['update:value'], + + data() { + return { + isLoading: false, + options: [], + }; + }, + + created() { + this.createdComponent(); + }, + + methods: { + async createdComponent() { + this.isLoading = true; + + try { + const response = await this.nlxTranslationApiService.getProviders(); + this.options = response.options ?? []; + } finally { + this.isLoading = false; + } + }, + + onChange(value) { + this.$emit('update:value', value); + }, + }, +}; diff --git a/src/Resources/app/administration/src/component/nlx-translation-provider-select/nlx-translation-provider-select.html.twig b/src/Resources/app/administration/src/component/nlx-translation-provider-select/nlx-translation-provider-select.html.twig new file mode 100644 index 0000000..f88d345 --- /dev/null +++ b/src/Resources/app/administration/src/component/nlx-translation-provider-select/nlx-translation-provider-select.html.twig @@ -0,0 +1,11 @@ +{% block nlx_translation_provider_select %} + +{% endblock %} diff --git a/src/Resources/app/administration/src/main.js b/src/Resources/app/administration/src/main.js index 0faaa95..055db51 100644 --- a/src/Resources/app/administration/src/main.js +++ b/src/Resources/app/administration/src/main.js @@ -1,6 +1,4 @@ -import './module/nlx-translation-update' -import './overrride/module/sw-settings-cache-index' -import TranslationApiService from "./service/translationApi.Service"; +import TranslationApiService from './service/translationApi.Service'; const {Application} = Shopware; @@ -9,6 +7,11 @@ Shopware.Component.register( () => import('./module/nlx-translation-update') ); +Shopware.Component.register( + 'nlx-translation-provider-select', + () => import('./component/nlx-translation-provider-select') +); + Shopware.Component.override( 'sw-settings-cache-index', () => import('./overrride/module/sw-settings-cache-index') diff --git a/src/Resources/app/administration/src/service/translationApi.Service.js b/src/Resources/app/administration/src/service/translationApi.Service.js index dfbd583..8b3db90 100644 --- a/src/Resources/app/administration/src/service/translationApi.Service.js +++ b/src/Resources/app/administration/src/service/translationApi.Service.js @@ -3,7 +3,7 @@ const {ApiService} = Shopware.Classes; export default class NlxTranslationApiService extends ApiService { constructor(httpClient, loginService, apiEndpoint = '_action/nlx-translation') { super(httpClient, loginService, apiEndpoint); - this.name = 'nlxNeosContentApiService'; + this.name = 'nlxTranslationApiService'; } updateTranslation() { @@ -17,4 +17,15 @@ export default class NlxTranslationApiService extends ApiService { ) .then((response) => ApiService.handleResponse(response)); } + + getProviders() { + return this.httpClient + .get( + `${this.getApiBasePath()}/providers`, + { + headers: this.getBasicHeaders(), + } + ) + .then((response) => ApiService.handleResponse(response)); + } } diff --git a/src/Resources/config/config.xml b/src/Resources/config/config.xml new file mode 100644 index 0000000..ae16b9e --- /dev/null +++ b/src/Resources/config/config.xml @@ -0,0 +1,39 @@ + + + + Translation Bridge + Translation Bridge + + + defaultProvider + + + + Symfony Translation provider configured under "framework.translator.providers" in + translation.yaml. Select a sales channel above to override the provider for that sales + channel only. + + + Symfony-Translation-Provider, der unter "framework.translator.providers" in der + translation.yaml konfiguriert ist. Wählen Sie oben einen Verkaufskanal aus, um den Provider + nur für diesen Verkaufskanal zu überschreiben. + + + + + respectTranslationFiles + + + + When enabled, existing local snippet/translation files always take precedence over the + configured translation provider. + + + Wenn aktiviert, haben vorhandene lokale Snippet-/Übersetzungsdateien immer Vorrang vor dem + konfigurierten Übersetzungsanbieter. + + true + + + diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml new file mode 100644 index 0000000..7338c9f --- /dev/null +++ b/src/Resources/config/services.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + diff --git a/src/ShopwareTranslationBridge.php b/src/ShopwareTranslationBridge.php index 1a94b9c..4025996 100644 --- a/src/ShopwareTranslationBridge.php +++ b/src/ShopwareTranslationBridge.php @@ -1,115 +1,11 @@ rootNode() - ->children() - ->scalarNode('default_provider') - ->defaultNull() - ->end() - ->booleanNode('respect_translation_files') - ->defaultTrue() - ->end() - ->arrayNode('sales_channel_providers') - ->useAttributeAsKey('salesChannelId') - ->arrayPrototype() - ->beforeNormalization() - ->ifString() - ->then(static fn(string $v): array => ['provider' => $v]) - ->end() - ->children() - ->scalarNode('provider') - ->isRequired() - ->end() - ->end() - ->end() - ->end() - ->end(); - } - - public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void - { - } - - public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void - { - $services = $container->services()->defaults()->autowire()->autoconfigure(); - - $services->load(__NAMESPACE__ . '\\', '*'); - - $services->alias(TranslationProviderResolverInterface::class, TranslationProviderResolver::class); - $services->alias(SalesChannelTranslationRefresherInterface::class, SalesChannelTranslationRefresher::class); - $services->alias(TranslationCacheInvalidationInterface::class, TranslationCacheInvalidation::class); - - $defaultProvider = $config['default_provider'] ?? null; - assert($defaultProvider === null || is_string($defaultProvider)); - $container->parameters()->set('nlx_storefront_translation.default_provider', $defaultProvider); - - $respectTranslationFiles = $config['respect_translation_files'] ?? null; - assert(is_bool($respectTranslationFiles)); - $container->parameters()->set('nlx_storefront_translation.respect_translation_files', $respectTranslationFiles); - - $salesChannelProviders = $config['sales_channel_providers'] ?? null; - assert(is_array($salesChannelProviders)); - $container->parameters()->set( - 'nlx_storefront_translation.sales_channel_provider', - $this->processSalesChannelProviders($salesChannelProviders) - ); - } - - #[Override] - public function getContainerExtension(): ?ExtensionInterface - { - if (!isset($this->extensionAlias)) { - $this->extensionAlias = Container::underscore(preg_replace('/Bundle$/', '', $this->getName())); - } - - $this->extension ??= new BundleExtension($this, $this->extensionAlias); - - return $this->extension === false ? null : $this->extension; - } - - private function processSalesChannelProviders(array $salesChannelProviders): array - { - $providerMap = []; - foreach ($salesChannelProviders as $salesChannelId => $providerConfig) { - assert(is_string($salesChannelId)); - if (!Uuid::isValid($salesChannelId)) { - throw new RuntimeException('Invalid salesChannel UUID: ' . $salesChannelId); - } - - $providerName = $providerConfig['provider'] ?? null; - assert(is_string($providerName)); - $providerMap[$salesChannelId] = $providerName; - } - - return $providerMap; - } }