diff --git a/Api/Data/NodeTranslationInterface.php b/Api/Data/NodeTranslationInterface.php
new file mode 100644
index 00000000..d497167c
--- /dev/null
+++ b/Api/Data/NodeTranslationInterface.php
@@ -0,0 +1,44 @@
+imageFile = $imageFile;
$this->vueProvider = $vueProvider;
$this->customerGroupsProvider = $customerGroupsProvider;
+ $this->systemStore = $systemStore;
+ $this->nodeTranslationRepository = $nodeTranslationRepository;
}
public function renderNodes()
@@ -175,7 +191,43 @@ private function renderNodeList($level, $parent, $data)
}
$nodes = $data[$level][$parent];
$menu = [];
+
+ // Create store view lookup array
+ $storeViewLabels = [];
+ $websites = $this->systemStore->getWebsiteCollection();
+ $websiteNames = [];
+
+ // Create website name lookup array
+ foreach ($websites as $website) {
+ $websiteNames[$website->getId()] = $website->getName();
+ }
+
+ foreach ($this->systemStore->getStoreCollection() as $store) {
+ if ($store->isActive()) {
+ $websiteName = isset($websiteNames[$store->getWebsiteId()])
+ ? $websiteNames[$store->getWebsiteId()]
+ : '';
+ $storeViewLabels[$store->getId()] = [
+ 'value' => $store->getId(),
+ 'label' => sprintf('%s -> %s', $websiteName, $store->getName())
+ ];
+ }
+ }
+
foreach ($nodes as $node) {
+ $translations = $this->nodeTranslationRepository->getByNodeId($node->getId());
+ $translationData = [];
+ foreach ($translations as $translation) {
+ $storeId = $translation->getStoreId();
+ if (isset($storeViewLabels[$storeId])) {
+ $translationData[] = [
+ 'store_id' => $storeViewLabels[$storeId]['value'],
+ 'value' => $translation->getTitle(),
+ 'label' => $storeViewLabels[$storeId]['label']
+ ];
+ }
+ }
+
$menu[] = [
'is_active' => $node->getIsActive(),
'is_stored' => true,
@@ -194,7 +246,8 @@ private function renderNodeList($level, $parent, $data)
'image_height' => $node->getImageHeight(),
'columns' => $this->renderNodeList($level + 1, $node->getId(), $data) ?: [],
'selected_item_id' => $node->getSelectedItemId(),
- 'customer_groups' => $node->getCustomerGroups()
+ 'customer_groups' => $node->getCustomerGroups(),
+ 'translations' => $translationData
];
}
return $menu;
@@ -222,4 +275,39 @@ public function getCustomerGroups()
{
return $this->customerGroupsProvider->getAll();
}
+
+ /**
+ * Get store views for translations
+ *
+ * @return array
+ */
+ public function getStoreViews()
+ {
+ $storeViews = [];
+ $stores = $this->systemStore->getStoreCollection();
+ $websites = $this->systemStore->getWebsiteCollection();
+ $websiteNames = [];
+
+ // Create website name lookup array
+ foreach ($websites as $website) {
+ $websiteNames[$website->getId()] = $website->getName();
+ }
+
+ foreach ($stores as $store) {
+ if (!$store->isActive()) {
+ continue;
+ }
+
+ $websiteName = isset($websiteNames[$store->getWebsiteId()])
+ ? $websiteNames[$store->getWebsiteId()]
+ : '';
+
+ $storeViews[] = [
+ 'value' => $store->getId(),
+ 'label' => sprintf('%s -> %s', $websiteName, $store->getName())
+ ];
+ }
+
+ return $storeViews;
+ }
}
diff --git a/Block/Menu.php b/Block/Menu.php
index dd076269..f37aa034 100644
--- a/Block/Menu.php
+++ b/Block/Menu.php
@@ -15,6 +15,8 @@
use Snowdog\Menu\Model\NodeTypeProvider;
use Snowdog\Menu\Model\TemplateResolver;
use Magento\Store\Model\Store;
+use Snowdog\Menu\Api\NodeTranslationRepositoryInterface;
+use Magento\Store\Model\StoreManagerInterface;
/**
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
@@ -40,9 +42,19 @@ class Menu extends Template implements DataObject\IdentityInterface
*/
private $nodeTypeProvider;
- private $nodes;
+ /**
+ * @var NodeTranslationRepositoryInterface
+ */
+ private $nodeTranslationRepository;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ private $nodes;
private $menu = null;
+ private $nodeTranslations = [];
/**
* @var EventManager
@@ -97,6 +109,8 @@ public function __construct(
ImageFile $imageFile,
Escaper $escaper,
Context $httpContext,
+ NodeTranslationRepositoryInterface $nodeTranslationRepository,
+ StoreManagerInterface $storeManager,
array $data = []
) {
parent::__construct($context, $data);
@@ -110,6 +124,8 @@ public function __construct(
$this->setTemplate($this->getMenuTemplate($this->_template));
$this->submenuTemplate = $this->getSubmenuTemplate();
$this->httpContext = $httpContext;
+ $this->nodeTranslationRepository = $nodeTranslationRepository;
+ $this->storeManager = $storeManager;
}
/**
@@ -447,6 +463,20 @@ private function fetchData()
$customerGroupEnabled = $this->_scopeConfig->getValue(self::XML_SNOWMENU_GENERAL_CUSTOMER_GROUPS);
$result = [];
$types = [];
+ $nodeIds = [];
+
+ foreach($nodes as $node) {
+ $nodeIds[] = $node->getId();
+ }
+
+ if (!empty($nodeIds)) {
+ $storeId = (int)$this->storeManager->getStore()->getId();
+ $collection = $this->nodeTranslationRepository->getByNodeIds($nodeIds, $storeId);
+ foreach ($collection as $translation) {
+ $this->nodeTranslations[$translation->getNodeId()] = $translation;
+ }
+ }
+
foreach ($nodes as $node) {
if (!$node->getIsActive()) {
continue;
@@ -455,6 +485,8 @@ private function fetchData()
continue;
}
+ $node->setTitle($this->getNodeTitle($node));
+
$level = $node->getLevel();
$parent = $node->getParentId() ?: 0;
if (!isset($result[$level])) {
@@ -470,6 +502,7 @@ private function fetchData()
}
$types[$type][] = $node;
}
+
$this->nodes = $result;
foreach ($types as $type => $nodes) {
@@ -513,4 +546,22 @@ public function getCustomerGroupId()
{
return $this->httpContext->getValue(\Magento\Customer\Model\Context::CONTEXT_GROUP);
}
+
+ /**
+ * Get translated node title based on current store view
+ *
+ * @param NodeInterface $node
+ * @return string
+ */
+ private function getNodeTitle(NodeInterface $node): string
+ {
+ $nodeId = $node->getId();
+ if (isset($this->nodeTranslations[$nodeId])) {
+ $title = $this->nodeTranslations[$nodeId]->getTitle();
+ if ($title) {
+ return $title;
+ }
+ }
+ return $node->getTitle();
+ }
}
diff --git a/Model/ImportExport/Processor/Export/Node.php b/Model/ImportExport/Processor/Export/Node.php
index 77b51209..97970443 100644
--- a/Model/ImportExport/Processor/Export/Node.php
+++ b/Model/ImportExport/Processor/Export/Node.php
@@ -10,6 +10,7 @@
use Snowdog\Menu\Api\Data\NodeInterface;
use Snowdog\Menu\Api\NodeRepositoryInterface;
use Snowdog\Menu\Model\ImportExport\Processor\Export\Node\Tree as NodeTree;
+use Snowdog\Menu\Model\ImportExport\Processor\Export\Node\DataProcessor;
class Node
{
@@ -40,16 +41,23 @@ class Node
*/
private $nodeTree;
+ /**
+ * @var DataProcessor
+ */
+ private $dataProcessor;
+
public function __construct(
SearchCriteriaBuilder $searchCriteriaBuilder,
SortOrderBuilder $sortOrderBuilder,
NodeRepositoryInterface $nodeRepository,
- NodeTree $nodeTree
+ NodeTree $nodeTree,
+ DataProcessor $dataProcessor
) {
$this->searchCriteriaBuilder = $searchCriteriaBuilder;
$this->sortOrderBuilder = $sortOrderBuilder;
$this->nodeRepository = $nodeRepository;
$this->nodeTree = $nodeTree;
+ $this->dataProcessor = $dataProcessor;
}
public function getList(int $menuId): array
@@ -66,6 +74,10 @@ public function getList(int $menuId): array
$nodes = $this->nodeRepository->getList($searchCriteria)->getItems();
+ if (!empty($nodes)) {
+ $this->dataProcessor->preloadTranslations($nodes);
+ }
+
return $nodes ? $this->nodeTree->get($nodes) : [];
}
}
diff --git a/Model/ImportExport/Processor/Export/Node/DataProcessor.php b/Model/ImportExport/Processor/Export/Node/DataProcessor.php
index 4089a3e0..f0ff140c 100644
--- a/Model/ImportExport/Processor/Export/Node/DataProcessor.php
+++ b/Model/ImportExport/Processor/Export/Node/DataProcessor.php
@@ -6,6 +6,7 @@
use Snowdog\Menu\Api\Data\NodeInterface;
use Snowdog\Menu\Model\ImportExport\Processor\Export\Node\TypeContent;
+use Snowdog\Menu\Model\ImportExport\Processor\ExtendedFields;
class DataProcessor
{
@@ -13,10 +14,23 @@ class DataProcessor
* @var TypeContent
*/
private $typeContent;
+ private TranslationProcessor $translationProcessor;
- public function __construct(TypeContent $typeContent)
- {
+ public function __construct(
+ TypeContent $typeContent,
+ TranslationProcessor $translationProcessor
+ ) {
$this->typeContent = $typeContent;
+ $this->translationProcessor = $translationProcessor;
+ }
+
+ public function preloadTranslations(array $nodes): void
+ {
+ $nodeIds = array_map(
+ fn($node) => (int)$node->getId(),
+ $nodes
+ );
+ $this->translationProcessor->preloadTranslations($nodeIds);
}
public function getData(array $data): array
@@ -26,6 +40,14 @@ public function getData(array $data): array
$data[NodeInterface::CONTENT]
);
+ $translations = $this->translationProcessor->getTranslations(
+ (int)$data[NodeInterface::NODE_ID]
+ );
+
+ if (!empty($translations)) {
+ $data[ExtendedFields::TRANSLATIONS] = $translations;
+ }
+
return $data;
}
}
diff --git a/Model/ImportExport/Processor/Export/Node/TranslationProcessor.php b/Model/ImportExport/Processor/Export/Node/TranslationProcessor.php
new file mode 100644
index 00000000..a66b8a27
--- /dev/null
+++ b/Model/ImportExport/Processor/Export/Node/TranslationProcessor.php
@@ -0,0 +1,46 @@
+nodeTranslationRepository = $nodeTranslationRepository;
+ $this->storeManager = $storeManager;
+ }
+
+ public function preloadTranslations(array $nodeIds): void
+ {
+ if (empty($nodeIds)) {
+ return;
+ }
+
+ $stores = $this->storeManager->getStores();
+ foreach ($stores as $store) {
+ $translations = $this->nodeTranslationRepository->getByNodeIds($nodeIds, (int)$store->getId());
+ foreach ($translations as $translation) {
+ $nodeId = $translation->getNodeId();
+ if ($translation->getTitle()) {
+ $this->translationsCache[$nodeId][$store->getCode()] = $translation->getTitle();
+ }
+ }
+ }
+ }
+
+ public function getTranslations(int $nodeId): array
+ {
+ return $this->translationsCache[$nodeId] ?? [];
+ }
+}
diff --git a/Model/ImportExport/Processor/ExtendedFields.php b/Model/ImportExport/Processor/ExtendedFields.php
index 2d97e1a8..27c20ebb 100644
--- a/Model/ImportExport/Processor/ExtendedFields.php
+++ b/Model/ImportExport/Processor/ExtendedFields.php
@@ -9,8 +9,13 @@
*/
class ExtendedFields
{
- const STORES = 'stores';
const NODES = 'nodes';
+ const STORES = 'stores';
+ const TRANSLATIONS = 'translations';
- const FIELDS = [self::STORES, self::NODES];
+ const FIELDS = [
+ self::NODES,
+ self::STORES,
+ self::TRANSLATIONS
+ ];
}
diff --git a/Model/ImportExport/Processor/Import/Node.php b/Model/ImportExport/Processor/Import/Node.php
index 286f4e37..df8d27bd 100644
--- a/Model/ImportExport/Processor/Import/Node.php
+++ b/Model/ImportExport/Processor/Import/Node.php
@@ -9,6 +9,7 @@
use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\DataProcessor;
use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Validator;
use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\Validator\TreeTrace;
+use Snowdog\Menu\Model\ImportExport\Processor\Import\Node\TranslationProcessor;
use Snowdog\Menu\Model\ImportExport\Processor\ExtendedFields;
use Snowdog\Menu\Model\ImportExport\File\Yaml;
@@ -44,13 +45,19 @@ class Node
*/
private $yaml;
+ /**
+ * @var TranslationProcessor
+ */
+ private $translationProcessor;
+
public function __construct(
NodeInterfaceFactory $nodeFactory,
NodeRepositoryInterface $nodeRepository,
DataProcessor $dataProcessor,
Validator $validator,
TreeTrace $treeTrace,
- Yaml $yaml
+ Yaml $yaml,
+ TranslationProcessor $translationProcessor
) {
$this->nodeFactory = $nodeFactory;
$this->nodeRepository = $nodeRepository;
@@ -58,6 +65,7 @@ public function __construct(
$this->validator = $validator;
$this->treeTrace = $treeTrace;
$this->yaml = $yaml;
+ $this->translationProcessor = $translationProcessor;
}
public function createNodes(
@@ -75,11 +83,20 @@ public function createNodes(
$node = $this->nodeFactory->create();
$data = $this->dataProcessor->getData($nodeData, $menuId, $level, $position++, $parentId);
+ // Extract translations before saving node
+ $translations = $nodeData[ExtendedFields::TRANSLATIONS] ?? [];
+ unset($data[ExtendedFields::TRANSLATIONS]);
+
$node->setData($data);
$this->nodeRepository->save($node);
+ // Process translations after node is saved
+ if (!empty($translations)) {
+ $this->translationProcessor->processTranslations((int)$node->getId(), $translations);
+ }
+
if (isset($nodeData[ExtendedFields::NODES])) {
- $nodeId = $node->getId() ? (int) $node->getId() : null;
+ $nodeId = $node->getId() ? (int)$node->getId() : null;
$this->createNodes($nodeData[ExtendedFields::NODES], $menuId, ($level + 1), 0, $nodeId);
}
}
diff --git a/Model/ImportExport/Processor/Import/Node/TranslationProcessor.php b/Model/ImportExport/Processor/Import/Node/TranslationProcessor.php
new file mode 100644
index 00000000..30d2dc84
--- /dev/null
+++ b/Model/ImportExport/Processor/Import/Node/TranslationProcessor.php
@@ -0,0 +1,57 @@
+storeManager = $storeManager;
+ $this->nodeTranslationRepository = $nodeTranslationRepository;
+ $this->nodeTranslationFactory = $nodeTranslationFactory;
+ $this->initializeStoreMap();
+ }
+
+ private function initializeStoreMap(): void
+ {
+ $stores = $this->storeManager->getStores();
+ foreach ($stores as $store) {
+ $this->storeCodeToId[$store->getCode()] = (int)$store->getId();
+ }
+ }
+
+ public function processTranslations(int $nodeId, array $translations): void
+ {
+ if (empty($translations)) {
+ return;
+ }
+
+ foreach ($translations as $storeCode => $title) {
+ if (!isset($this->storeCodeToId[$storeCode])) {
+ continue; // Skip if store code doesn't exist
+ }
+
+ $storeId = $this->storeCodeToId[$storeCode];
+ $translation = $this->nodeTranslationFactory->create();
+ $translation->setNodeId($nodeId)
+ ->setStoreId($storeId)
+ ->setTitle($title);
+
+ $this->nodeTranslationRepository->save($translation);
+ }
+ }
+}
diff --git a/Model/ImportExport/Processor/Store.php b/Model/ImportExport/Processor/Store.php
index 9d94b59d..1158274c 100644
--- a/Model/ImportExport/Processor/Store.php
+++ b/Model/ImportExport/Processor/Store.php
@@ -7,6 +7,7 @@
use Magento\Framework\Exception\NoSuchEntityException;
use Magento\Store\Api\Data\StoreInterface;
use Magento\Store\Api\StoreRepositoryInterface;
+use Magento\Store\Model\StoreManagerInterface;
class Store
{
@@ -15,14 +16,20 @@ class Store
*/
private $storeRepository;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
/**
* @var array
*/
private $cachedStores = [];
- public function __construct(StoreRepositoryInterface $storeRepository)
+ public function __construct(StoreRepositoryInterface $storeRepository, StoreManagerInterface $storeManager)
{
$this->storeRepository = $storeRepository;
+ $this->storeManager = $storeManager;
}
/**
@@ -45,4 +52,14 @@ public function get($storeCode): ?StoreInterface
return $store;
}
+
+ public function getIdByCode(string $storeCode): ?int
+ {
+ try {
+ $store = $this->storeManager->getStore($storeCode);
+ return (int) $store->getId();
+ } catch (\Exception $e) {
+ return null;
+ }
+ }
}
diff --git a/Model/MenuManagement.php b/Model/MenuManagement.php
index 2c5ae695..227f71f0 100644
--- a/Model/MenuManagement.php
+++ b/Model/MenuManagement.php
@@ -9,6 +9,8 @@
use Magento\Catalog\Model\Category;
use Magento\Framework\Exception\LocalizedException;
use Magento\Framework\Exception\NoSuchEntityException;
+use Magento\Store\Model\StoreManagerInterface;
+use Magento\Catalog\Model\ResourceModel\Category\CollectionFactory;
class MenuManagement implements MenuManagementInterface
{
@@ -17,13 +19,34 @@ class MenuManagement implements MenuManagementInterface
*/
private $categoryManagement;
+ /**
+ * @var StoreManagerInterface
+ */
+ private $storeManager;
+
+ /**
+ * @var CollectionFactory
+ */
+ private $categoryCollectionFactory;
+
+ /**
+ * @var array
+ */
+ private $categoryNames = [];
+
/**
* @param CategoryManagement $categoryManagement
+ * @param StoreManagerInterface $storeManager
+ * @param CollectionFactory $categoryCollectionFactory
*/
public function __construct(
- CategoryManagement $categoryManagement
+ CategoryManagement $categoryManagement,
+ StoreManagerInterface $storeManager,
+ CollectionFactory $categoryCollectionFactory
) {
$this->categoryManagement = $categoryManagement;
+ $this->storeManager = $storeManager;
+ $this->categoryCollectionFactory = $categoryCollectionFactory;
}
/**
@@ -37,11 +60,60 @@ public function getCategoryNodeList($rootCategoryId = null, $depth = null): arra
{
$categoriesTree = $this->categoryManagement->getTree($rootCategoryId, $depth);
$categories = $this->generateCategoriesNode($categoriesTree);
+
+ // Preload all category translations
+ $this->preloadCategoryTranslations($categories);
+
$nodeList = $this->getCategoriesNodeList(0, 0, $categories);
return $nodeList;
}
+ /**
+ * Preload all category translations in one go
+ *
+ * @param array $categories
+ * @return void
+ */
+ private function preloadCategoryTranslations(array $categories): void
+ {
+ $categoryIds = [];
+ foreach ($categories as $level) {
+ foreach ($level as $parentId => $nodes) {
+ foreach ($nodes as $node) {
+ $categoryIds[] = $node['entity_id'];
+ }
+ }
+ }
+
+ if (empty($categoryIds)) {
+ return;
+ }
+
+ $stores = $this->storeManager->getStores();
+
+ foreach ($stores as $store) {
+ $storeId = $store->getId();
+ if ($storeId === 0) {
+ continue; // Skip admin store
+ }
+
+ /** @var \Magento\Catalog\Model\ResourceModel\Category\Collection $collection */
+ $collection = $this->categoryCollectionFactory->create();
+ $collection->setStoreId($storeId)
+ ->addAttributeToSelect('name')
+ ->addFieldToFilter('entity_id', ['in' => $categoryIds]);
+
+ foreach ($collection as $category) {
+ $categoryId = $category->getId();
+ if (!isset($this->categoryNames[$categoryId])) {
+ $this->categoryNames[$categoryId] = [];
+ }
+ $this->categoryNames[$categoryId][$storeId] = $category->getName();
+ }
+ }
+ }
+
/**
* @param Category $node
* @param array $data
@@ -88,8 +160,23 @@ private function getCategoriesNodeList($level, $parent, array $data): array
$nodes = $data[$level][$parent];
$nodeList = [];
+
foreach ($nodes as $node) {
- $nodeId = $node['id'];
+ $nodeId = $node['entity_id'];
+ $translations = [];
+
+ // Use preloaded translations
+ if (isset($this->categoryNames[$nodeId])) {
+ foreach ($this->categoryNames[$nodeId] as $storeId => $name) {
+ if ($name !== $node['name']) {
+ $translations[] = [
+ 'store_id' => (string)$storeId,
+ 'value' => $name
+ ];
+ }
+ }
+ }
+
$nodeList[] = [
'is_active' => '1',
'type' => 'category',
@@ -104,6 +191,7 @@ private function getCategoriesNodeList($level, $parent, array $data): array
'image_width' => null,
'image_height' => null,
'submenu_template' => null,
+ 'translations' => $translations,
'columns' => $this->getCategoriesNodeList($level + 1, $nodeId, $data) ?: []
];
}
diff --git a/Model/NodeTranslation.php b/Model/NodeTranslation.php
new file mode 100644
index 00000000..30fba480
--- /dev/null
+++ b/Model/NodeTranslation.php
@@ -0,0 +1,127 @@
+_init(NodeTranslationResource::class);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTranslationId(): ?int
+ {
+ return $this->getData(self::TRANSLATION_ID) === null
+ ? null
+ : (int)$this->getData(self::TRANSLATION_ID);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setTranslationId(int $id): NodeTranslationInterface
+ {
+ return $this->setData(self::TRANSLATION_ID, $id);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getNodeId(): int
+ {
+ return (int)$this->getData(self::NODE_ID);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setNodeId(int $nodeId): NodeTranslationInterface
+ {
+ return $this->setData(self::NODE_ID, $nodeId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getStoreId(): int
+ {
+ return (int)$this->getData(self::STORE_ID);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setStoreId(int $storeId): NodeTranslationInterface
+ {
+ return $this->setData(self::STORE_ID, $storeId);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getTitle(): ?string
+ {
+ return $this->getData(self::TITLE);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setTitle(string $title): NodeTranslationInterface
+ {
+ return $this->setData(self::TITLE, $title);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getCreatedAt(): string
+ {
+ return (string)$this->getData(self::CREATED_AT);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setCreatedAt(string $createdAt): NodeTranslationInterface
+ {
+ return $this->setData(self::CREATED_AT, $createdAt);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getUpdatedAt(): string
+ {
+ return (string)$this->getData(self::UPDATED_AT);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function setUpdatedAt(string $updatedAt): NodeTranslationInterface
+ {
+ return $this->setData(self::UPDATED_AT, $updatedAt);
+ }
+
+ public function getValue(): string
+ {
+ return (string)$this->getData('value');
+ }
+
+ public function setValue(string $value): void
+ {
+ $this->setData('value', $value);
+ }
+}
diff --git a/Model/NodeTranslationRepository.php b/Model/NodeTranslationRepository.php
new file mode 100644
index 00000000..85662a80
--- /dev/null
+++ b/Model/NodeTranslationRepository.php
@@ -0,0 +1,187 @@
+resource = $resource;
+ $this->translationFactory = $translationFactory;
+ $this->collectionFactory = $collectionFactory;
+ $this->collectionProcessor = $collectionProcessor;
+ $this->searchResultsFactory = $searchResultsFactory;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function save(NodeTranslationInterface $translation): NodeTranslationInterface
+ {
+ try {
+ $this->resource->save($translation);
+ } catch (\Exception $exception) {
+ throw new CouldNotSaveException(__($exception->getMessage()));
+ }
+ return $translation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getById(int $translationId): NodeTranslationInterface
+ {
+ $translation = $this->translationFactory->create();
+ $this->resource->load($translation, $translationId);
+ if (!$translation->getId()) {
+ throw new NoSuchEntityException(__('Node Translation with id "%1" does not exist.', $translationId));
+ }
+ return $translation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getByNodeAndStore(int $nodeId, int $storeId): NodeTranslationInterface
+ {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $collection->addFieldToFilter('node_id', $nodeId);
+ $collection->addFieldToFilter('store_id', $storeId);
+ $collection->setPageSize(1);
+
+ $translation = $collection->getFirstItem();
+
+ if (!$translation->getId()) {
+ throw new NoSuchEntityException(
+ __('Node translation for node ID "%1" and store ID "%2" does not exist.', $nodeId, $storeId)
+ );
+ }
+
+ return $translation;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getByNodeId(int $nodeId): array
+ {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $collection->addFieldToFilter('node_id', $nodeId);
+
+ return $collection->getItems();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getByNodeIds(array $nodeIds, int $storeId): array
+ {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+ $collection->addFieldToFilter('node_id', ['in' => $nodeIds]);
+ $collection->addFieldToFilter('store_id', $storeId);
+
+ return $collection->getItems();
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function getList(SearchCriteriaInterface $searchCriteria): SearchResultsInterface
+ {
+ /** @var Collection $collection */
+ $collection = $this->collectionFactory->create();
+
+ $this->collectionProcessor->process($searchCriteria, $collection);
+
+ /** @var SearchResultsInterface $searchResults */
+ $searchResults = $this->searchResultsFactory->create();
+ $searchResults->setSearchCriteria($searchCriteria);
+ $searchResults->setItems($collection->getItems());
+ $searchResults->setTotalCount($collection->getSize());
+
+ return $searchResults;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function delete(NodeTranslationInterface $translation): bool
+ {
+ try {
+ $this->resource->delete($translation);
+ } catch (\Exception $exception) {
+ throw new CouldNotDeleteException(__($exception->getMessage()));
+ }
+ return true;
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function deleteById(int $translationId): bool
+ {
+ return $this->delete($this->getById($translationId));
+ }
+
+ public function deleteByNodeId(int $nodeId): bool
+ {
+ try {
+ $collection = $this->collectionFactory->create();
+ $collection->addFieldToFilter('node_id', $nodeId);
+ foreach ($collection as $translation) {
+ $this->delete($translation);
+ }
+ } catch (\Exception $e) {
+ throw new CouldNotDeleteException(__('Could not delete node translations: %1', $e->getMessage()));
+ }
+ return true;
+ }
+}
diff --git a/Model/ResourceModel/NodeTranslation.php b/Model/ResourceModel/NodeTranslation.php
new file mode 100644
index 00000000..6ee23c06
--- /dev/null
+++ b/Model/ResourceModel/NodeTranslation.php
@@ -0,0 +1,28 @@
+_init(self::TABLE_NAME, self::ID_FIELD_NAME);
+ }
+}
diff --git a/Model/ResourceModel/NodeTranslation/Collection.php b/Model/ResourceModel/NodeTranslation/Collection.php
new file mode 100644
index 00000000..c4e500b7
--- /dev/null
+++ b/Model/ResourceModel/NodeTranslation/Collection.php
@@ -0,0 +1,19 @@
+_init(NodeTranslation::class, NodeTranslationResource::class);
+ }
+}
diff --git a/Plugin/Model/Menu/Node/AfterSave.php b/Plugin/Model/Menu/Node/AfterSave.php
new file mode 100644
index 00000000..5191a040
--- /dev/null
+++ b/Plugin/Model/Menu/Node/AfterSave.php
@@ -0,0 +1,83 @@
+getData('translations');
+
+ if (!is_array($translations)) {
+ return $result;
+ }
+
+ $nodeId = (int)$result->getId();
+ $existingTranslations = $this->nodeTranslationRepository->getByNodeId($nodeId);
+ $existingTranslationMap = [];
+
+ // Create a map of existing translations by store ID for easy lookup
+ foreach ($existingTranslations as $translation) {
+ $storeId = $translation->getStoreId();
+ $existingTranslationMap[$storeId] = $translation;
+ }
+
+ // Process new/updated translations
+ foreach ($translations as $translation) {
+ if (!isset($translation['store_id']) || !isset($translation['value'])) {
+ continue;
+ }
+
+ $storeId = (int)$translation['store_id'];
+ $newValue = $translation['value'];
+
+ // If translation exists for this store
+ if (isset($existingTranslationMap[$storeId])) {
+ $existingTranslation = $existingTranslationMap[$storeId];
+ // Only update if value has changed
+ if ($existingTranslation->getTitle() !== $newValue) {
+ $existingTranslation->setTitle($newValue);
+ $this->nodeTranslationRepository->save($existingTranslation);
+ }
+ // Remove from map as it's been processed
+ unset($existingTranslationMap[$storeId]);
+ } else {
+ // Create new translation
+ $nodeTranslation = $this->nodeTranslationFactory->create();
+ $nodeTranslation->setNodeId($nodeId);
+ $nodeTranslation->setStoreId($storeId);
+ $nodeTranslation->setTitle($newValue);
+ $this->nodeTranslationRepository->save($nodeTranslation);
+ }
+ }
+
+ // Delete translations that no longer exist
+ foreach ($existingTranslationMap as $translation) {
+ $this->nodeTranslationRepository->delete($translation);
+ }
+
+ return $result;
+ }
+}
diff --git a/Service/Menu/SaveRequestProcessor.php b/Service/Menu/SaveRequestProcessor.php
index 219a8072..e8984074 100644
--- a/Service/Menu/SaveRequestProcessor.php
+++ b/Service/Menu/SaveRequestProcessor.php
@@ -184,6 +184,11 @@ private function processNodeObject(
$nodeObject->setTarget($nodeData['target']);
}
+ // Handle translations
+ if (isset($nodeData['translations']) && is_array($nodeData['translations'])) {
+ $nodeObject->setData('translations', $nodeData['translations']);
+ }
+
$nodeTemplate = null;
if (isset($nodeData['node_template']) && $nodeData['type'] != $nodeData['node_template']) {
$nodeTemplate = $nodeData['node_template'];
diff --git a/composer.json b/composer.json
index 0f82d516..2a344596 100644
--- a/composer.json
+++ b/composer.json
@@ -1,5 +1,5 @@
{
- "name": "snowdog/module-menu",
+ "name": "magebitcom/snowdog-module-menu",
"description": "Provides powerful menu editor to replace category based menus in Magento 2",
"license": "MIT",
"type": "magento2-module",
diff --git a/etc/db_schema.xml b/etc/db_schema.xml
index 21d02c1d..17a7dcb6 100644
--- a/etc/db_schema.xml
+++ b/etc/db_schema.xml
@@ -60,4 +60,27 @@
{{ config.translation.store }} | +{{ config.translation.translation }} | +{{ config.translation.actions }} | +
---|---|---|
+ |
+ + + | ++ + | +
+ + | +