Add support for adding languages in extensions

This commit is contained in:
Clemente Raposo 2024-11-14 16:04:14 +00:00 committed by Jack Anderson
parent 66b4786320
commit 9c1124972f
11 changed files with 502 additions and 25 deletions

View file

@ -71,6 +71,7 @@ services:
$navbarAdministrationOverrides: '%navbar.administration_override%'
$quickActions: '%quick_actions%'
$graphqlShowDocs: '%graphql.graphql_show_docs%'
$language: '%language%'
$apiRecordMapperRunner: '@api.record.mapper.runner'
$apiRecordMapperRegistry: '@api.record.mapper.registry'
$apiFieldMapperRegistry: '@api.field.mapper.registry'
@ -144,7 +145,7 @@ services:
# this creates a service per class whose id is the fully-qualified class name
App\Extension\:
resource: '../extensions/*'
exclude: '../extensions/**/{config,DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
exclude: '../extensions/**/{config,language,DependencyInjection,Entity,Migrations,Tests,Kernel.php}'
# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class

0
config/language/.gitkeep Normal file
View file

View file

@ -44,6 +44,10 @@ imports:
- { resource: ../extensions/*/config/modules/*.php }
- { resource: ../extensions/*/config/modules/**/*.yaml }
- { resource: ../extensions/*/config/modules/**/*.php }
- { resource: ../extensions/*/modules/*/config/*.php }
- { resource: ../extensions/*/modules/*/config/*.yaml }
- { resource: ../extensions/*/modules/*/config/**/*.php }
- { resource: ../extensions/*/modules/*/config/**/*.yaml }
- { resource: core_services.yaml }
- { resource: ../extensions/*/config/*.yaml }
- { resource: ../extensions/*/config/*.php }

View file

@ -0,0 +1,49 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2024 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\DependecyInjection\Metadata;
class LanguageYamlFileLoader extends ParameterMergingYamlFileLoader
{
public function getParameterKey(): string
{
return 'language';
}
public function getBaseStructure(): array
{
return [
'*' => [
'application' => true,
'lists' => true,
'module' => [
'*' => true
]
]
];
}
}

View file

@ -0,0 +1,121 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2024 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\DependecyInjection\Metadata;
use Symfony\Component\Config\Definition\Configurator\DefinitionConfigurator;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\Config\Loader\DelegatingLoader;
use Symfony\Component\Config\Loader\LoaderResolver;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\AbstractExtension;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
use Symfony\Component\DependencyInjection\Loader\GlobFileLoader;
class MetadataExtension extends AbstractExtension
{
public function getAlias(): string
{
return 'metadata_extension';
}
public function configure(DefinitionConfigurator $definition): void
{
}
public function prependExtension(ContainerConfigurator $container, ContainerBuilder $builder): void
{
$builder->setParameter('language', []);
}
public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void
{
$locator = new FileLocator();
$languageLoader = $this->getLanguageLoader($builder, $locator);
$this->loadParameters('language', $languageLoader);
$language = $builder->getParameter('language');
$container->parameters()->set('language', $language);
}
/**
* @param string $folder
* @param DelegatingLoader $delegatingLoader
* @return void
* @throws \Exception
*/
protected function loadParameters(string $folder, DelegatingLoader $delegatingLoader): void
{
[$coreConfigDir, $extensionsDir, $modules] = $this->getBasePaths();
$fileExtensions = '.{php,xml,yaml,yml}';
// YamlUserLoader is used to load this resource because it supports
// files with the '.yaml' extension
$delegatingLoader->load($coreConfigDir . '/' . $folder . '/*' . $fileExtensions, 'glob');
$delegatingLoader->load($coreConfigDir . '/' . $folder . '/**/*' . $fileExtensions, 'glob');
$delegatingLoader->load($modules . '/*/' . $folder . '/*' . $fileExtensions, 'glob');
$delegatingLoader->load($modules . '/*/' . $folder . '/**/*' . $fileExtensions, 'glob');
$delegatingLoader->load($extensionsDir . '/*/' . $folder . '/*' . $fileExtensions, 'glob');
$delegatingLoader->load($extensionsDir . '/*/' . $folder . '/**/*' . $fileExtensions, 'glob');
$delegatingLoader->load($extensionsDir . '/*/modules/*/' . $folder . '/*' . $fileExtensions, 'glob');
$delegatingLoader->load($extensionsDir . '/*/modules/*/' . $folder . '/**/*' . $fileExtensions, 'glob');
}
/**
* @param ContainerBuilder $builder
* @param FileLocator $locator
* @return DelegatingLoader
*/
protected function getLanguageLoader(ContainerBuilder $builder, FileLocator $locator): DelegatingLoader
{
$resolver = new LoaderResolver(
[
new LanguageYamlFileLoader($builder, $locator),
new GlobFileLoader($builder, $locator)
]
);
return new DelegatingLoader($resolver);
}
/**
* @return string[]
*/
protected function getBasePaths(): array
{
$rootDir = dirname(__DIR__, 4);
$coreConfigDir = $rootDir . '/config';
$extensionsDir = $rootDir . '/extensions';
$modules = $rootDir . '/core/modules';
return [$coreConfigDir, $extensionsDir, $modules];
}
}

View file

@ -0,0 +1,163 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2024 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\DependecyInjection\Metadata;
use Symfony\Component\DependencyInjection\ContainerBuilder;
trait ParameterMergingLoaderTrait
{
/**
* @param string $key
* @param ContainerBuilder $container
* @param callable $loaderCallback
* @param array $structure
* @return void
*/
protected function loadAndMerge(string $key, ContainerBuilder $container, callable $loaderCallback, array $structure): void
{
$current = $container->getParameterBag()->get($key);
if (empty($current)) {
$current = [];
}
$loaderCallback();
$newValue = $container->getParameter($key);
if (empty($newValue)) {
$newValue = [];
}
$merged = $this->merge($structure, $current, $newValue);
$container->setParameter($key, $merged);
}
protected function merge(array|bool $structureValue, array $left, array $right): array
{
if (empty($structureValue)) {
return [];
}
if (empty($left) && empty($right)) {
return [];
}
if (!empty($left) && empty($right)) {
return $left;
}
if (empty($left) && !empty($right)) {
return $right;
}
if (is_bool($structureValue)) {
return $this->runMergeAll($structureValue, $left, $right);
}
$subMergeAll = $structureValue['*'] ?? '';
if ($subMergeAll !== '') {
if (is_array($subMergeAll)) {
return $this->subArrayMerge($structureValue['*'], $left, $right);
}
return $this->runSubMergeAll($subMergeAll, $left, $right);
}
$result = [];
foreach ($structureValue as $index => $value) {
$result[$index] = $this->merge($value, $left[$index] ?? [], $right[$index] ?? []);
}
return $result;
}
/**
* @param bool $structureValue
* @param array $left
* @param array $right
* @return array
*/
protected function runMergeAll(bool $structureValue, array $left, array $right): array
{
if ($structureValue === true) {
return array_merge($left, $right);
}
return [];
}
/**
* @param bool $mergeAll
* @param array $left
* @param array $right
* @return array
*/
protected function runSubMergeAll(bool $mergeAll, array $left, array $right): array
{
if ($mergeAll === true) {
$result = [];
foreach ($left as $leftIndex => $leftValue) {
$result[$leftIndex] = array_merge($leftValue ?? [], $right[$leftIndex] ?? []);
}
foreach ($right as $rightIndex => $rightValue) {
if (isset($result[$rightIndex])) {
continue;
}
$result[$rightIndex] = $rightValue;
}
return $result;
}
return [];
}
/**
* @param array $left
* @param $structureValue
* @param array $right
* @return array
*/
protected function subArrayMerge($structureValue, array $left, array $right): array
{
$result = [];
foreach ($left as $leftIndex => $leftValue) {
$result[$leftIndex] = $this->merge($structureValue, $leftValue ?? [], $right[$leftIndex] ?? []);
}
foreach ($right as $rightIndex => $rightValue) {
if (isset($result[$rightIndex])) {
continue;
}
$result[$rightIndex] = $this->merge($structureValue, [], $rightValue ?? []);
}
return $result;
}
}

View file

@ -0,0 +1,55 @@
<?php
/**
* SuiteCRM is a customer relationship management program developed by SalesAgility Ltd.
* Copyright (C) 2024 SalesAgility Ltd.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by the
* Free Software Foundation with the addition of the following permission added
* to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
* IN WHICH THE COPYRIGHT IS OWNED BY SALESAGILITY, SALESAGILITY DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* 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 Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Supercharged by SuiteCRM" logo. If the display of the logos is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Supercharged by SuiteCRM".
*/
namespace App\DependecyInjection\Metadata;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
abstract class ParameterMergingYamlFileLoader extends YamlFileLoader
{
use ParameterMergingLoaderTrait;
abstract public function getParameterKey(): string;
abstract public function getBaseStructure(): array;
public function load(mixed $resource, string $type = null): mixed
{
$key = $this->getParameterKey();
$this->loadAndMerge(
$key,
$this->container,
function () use ($resource, $type) {
parent::load($resource, $type);
},
$this->getBaseStructure()
);
return null;
}
}

View file

@ -29,6 +29,7 @@
namespace App;
use App\DependecyInjection\BackwardsCompatibility\LegacySAMLExtension;
use App\DependecyInjection\Metadata\MetadataExtension;
use Exception;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Component\Config\Loader\LoaderInterface;
@ -116,6 +117,7 @@ class Kernel extends BaseKernel
{
parent::build($container);
$container->registerExtension(new LegacySAMLExtension());
$container->registerExtension(new MetadataExtension());
}
/**

View file

@ -25,18 +25,40 @@
* the words "Supercharged by SuiteCRM".
*/
namespace App\Languages\LegacyHandler;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Exception\ItemNotFoundException;
use App\Engine\LegacyHandler\LegacyHandler;
use App\Engine\LegacyHandler\LegacyScopeState;
use App\Languages\Entity\AppListStrings;
use Symfony\Component\HttpFoundation\RequestStack;
class AppListStringsHandler extends LegacyHandler implements AppListStringsProviderInterface
{
protected const MSG_LANGUAGE_NOT_FOUND = 'Not able to get language: ';
public const HANDLER_KEY = 'app-list-strings';
protected array $language;
public function __construct(
string $projectDir,
string $legacyDir,
string $legacySessionName,
string $defaultSessionName,
LegacyScopeState $legacyScopeState,
RequestStack $requestStack,
array $language
) {
parent::__construct(
$projectDir,
$legacyDir,
$legacySessionName,
$defaultSessionName,
$legacyScopeState,
$requestStack
);
$this->language = $language;
}
/**
* @inheritDoc
@ -48,7 +70,7 @@ class AppListStringsHandler extends LegacyHandler implements AppListStringsProvi
/**
* Get app list strings for given $language
* @param $language
* @param string $language
* @return AppListStrings|null
*/
public function getAppListStrings(string $language): ?AppListStrings
@ -68,6 +90,8 @@ class AppListStringsHandler extends LegacyHandler implements AppListStringsProvi
$appListStringsArray = return_app_list_strings_language($language);
$appListStringsArray = $this->decodeLabels($appListStringsArray);
$appListStringsArray = $this->injectPluginAppListStrings($language, $appListStringsArray);
if (empty($appListStringsArray)) {
throw new ItemNotFoundException(self::MSG_LANGUAGE_NOT_FOUND . "'$language'");
}
@ -83,7 +107,7 @@ class AppListStringsHandler extends LegacyHandler implements AppListStringsProvi
protected function decodeLabels(array $appListStringsArray): array
{
foreach($appListStringsArray as $key => $string){
foreach ($appListStringsArray as $key => $string) {
if (!is_array($string)) {
$string = html_entity_decode($string ?? '', ENT_QUOTES);
}
@ -92,4 +116,15 @@ class AppListStringsHandler extends LegacyHandler implements AppListStringsProvi
return $appListStringsArray;
}
/**
* @param string $language
* @param array $appListStringsArray
* @return array
*/
protected function injectPluginAppListStrings(string $language, array $appListStringsArray): array
{
$aooListStrings = $this->language[$language]['lists'] ?? [];
return array_merge($appListStringsArray, $aooListStrings);
}
}

View file

@ -27,7 +27,7 @@
namespace App\Languages\LegacyHandler;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Exception\ItemNotFoundException;
use App\Engine\LegacyHandler\LegacyHandler;
use App\Engine\LegacyHandler\LegacyScopeState;
use App\Install\LegacyHandler\InstallHandler;
@ -64,6 +64,8 @@ class AppStringsHandler extends LegacyHandler
*/
private $installHandler;
protected array $language;
/**
* LegacyHandler constructor.
* @param string $projectDir
@ -73,6 +75,7 @@ class AppStringsHandler extends LegacyHandler
* @param LegacyScopeState $legacyScopeState
* @param RequestStack $session
* @param InstallHandler $installHandler
* @param array $language
*/
public function __construct(
string $projectDir,
@ -81,7 +84,8 @@ class AppStringsHandler extends LegacyHandler
string $defaultSessionName,
LegacyScopeState $legacyScopeState,
RequestStack $session,
InstallHandler $installHandler
InstallHandler $installHandler,
array $language
) {
parent::__construct(
$projectDir,
@ -93,6 +97,7 @@ class AppStringsHandler extends LegacyHandler
);
$this->installHandler = $installHandler;
$this->language = $language;
}
/**
@ -114,6 +119,9 @@ class AppStringsHandler extends LegacyHandler
return null;
}
error_log('Language');
error_log(json_encode($this->language));
if (!$this->isInstalled()) {
return $this->getInstallAppStrings($language);
}
@ -134,6 +142,8 @@ class AppStringsHandler extends LegacyHandler
throw new ItemNotFoundException(self::MSG_LANGUAGE_NOT_FOUND . "'$language'");
}
$appStringsArray = $this->injectPluginAppStrings($language, $appStringsArray);
foreach ($this->injectedModuleLanguages as $module => $languageKeys) {
$this->injectModuleLanguage($language, $module, $languageKeys, $appStringsArray);
}
@ -192,6 +202,8 @@ class AppStringsHandler extends LegacyHandler
$this->injectLicense($appStringsArray);
$appStringsArray = $this->injectPluginAppStrings($language, $appStringsArray);
$appStrings = new AppStrings();
$appStrings->setId($language);
$appStrings->setItems($appStringsArray);
@ -231,13 +243,15 @@ class AppStringsHandler extends LegacyHandler
*/
protected function removeEndingColon(array $appStringsArray): array
{
$appStringsArray = array_map(static function ($label) {
if (is_string($label)) {
return preg_replace('/:$/', '', $label);
}
$appStringsArray = array_map(
static function ($label) {
if (is_string($label)) {
return preg_replace('/:$/', '', $label);
}
return $label;
}, $appStringsArray);
return $label;
}, $appStringsArray
);
return $appStringsArray;
}
@ -275,7 +289,7 @@ class AppStringsHandler extends LegacyHandler
protected function decodeLabels(array $appStringsArray): array
{
foreach($appStringsArray as $key => $string){
foreach ($appStringsArray as $key => $string) {
if (!is_array($string)) {
$string = html_entity_decode($string ?? '', ENT_QUOTES);
}
@ -284,4 +298,15 @@ class AppStringsHandler extends LegacyHandler
return $appStringsArray;
}
/**
* @param string $language
* @param array $appStringsArray
* @return array
*/
protected function injectPluginAppStrings(string $language, array $appStringsArray): array
{
$applicationStrings = $this->language[$language]['application'] ?? [];
return array_merge($appStringsArray, $applicationStrings);
}
}

View file

@ -29,7 +29,7 @@
namespace App\Languages\LegacyHandler;
use ApiPlatform\Core\Exception\ItemNotFoundException;
use ApiPlatform\Exception\ItemNotFoundException;
use App\Engine\LegacyHandler\LegacyHandler;
use App\Engine\LegacyHandler\LegacyScopeState;
use App\Languages\Entity\ModStrings;
@ -59,6 +59,8 @@ class ModStringsHandler extends LegacyHandler
*/
private $moduleRegistry;
protected array $language;
/**
* SystemConfigHandler constructor.
* @param string $projectDir
@ -69,6 +71,7 @@ class ModStringsHandler extends LegacyHandler
* @param ModuleNameMapperInterface $moduleNameMapper
* @param ModuleRegistryInterface $moduleRegistry
* @param RequestStack $session
* @param array $language
*/
public function __construct(
string $projectDir,
@ -78,11 +81,13 @@ class ModStringsHandler extends LegacyHandler
LegacyScopeState $legacyScopeState,
ModuleNameMapperInterface $moduleNameMapper,
ModuleRegistryInterface $moduleRegistry,
RequestStack $session
RequestStack $session,
array $language
) {
parent::__construct($projectDir, $legacyDir, $legacySessionName, $defaultSessionName, $legacyScopeState, $session);
$this->moduleNameMapper = $moduleNameMapper;
$this->moduleRegistry = $moduleRegistry;
$this->language = $language;
}
/**
@ -95,7 +100,7 @@ class ModStringsHandler extends LegacyHandler
/**
* Get mod strings for given $language
* @param $language
* @param string $language
* @return ModStrings|null
*/
public function getModStrings(string $language): ?ModStrings
@ -121,6 +126,9 @@ class ModStringsHandler extends LegacyHandler
$frontendName = $this->moduleNameMapper->toFrontEnd($module);
$moduleStrings = return_module_language($language, $module) ?? [];
$moduleStrings = $this->decodeLabels($moduleStrings);
$moduleStrings = $this->injectPluginModStrings($language, $moduleStrings);
if (!empty($moduleStrings)) {
$moduleStrings = $this->removeEndingColon($moduleStrings);
}
@ -147,20 +155,22 @@ class ModStringsHandler extends LegacyHandler
*/
protected function removeEndingColon(array $stringArray): array
{
$stringArray = array_map(static function ($label) {
if (is_string($label)) {
return preg_replace('/:$/', '', $label);
}
$stringArray = array_map(
static function ($label) {
if (is_string($label)) {
return preg_replace('/:$/', '', $label);
}
return $label;
}, $stringArray);
return $label;
}, $stringArray
);
return $stringArray;
}
protected function decodeLabels(array $moduleStrings): array
{
foreach($moduleStrings as $key => $string){
foreach ($moduleStrings as $key => $string) {
if (!is_array($string)) {
$string = html_entity_decode($string ?? '', ENT_QUOTES);
}
@ -169,4 +179,16 @@ class ModStringsHandler extends LegacyHandler
return $moduleStrings;
}
/**
* @param string $language
* @param array $modStringsArray
* @return array
*/
protected function injectPluginModStrings(string $language, array $modStringsArray): array
{
$modStrings = $this->language[$language]['module']['accounts'] ?? [];
return array_merge($modStringsArray, $modStrings);
}
}