From 071b763624403d546bc985b0c45f22538a6c53a5 Mon Sep 17 00:00:00 2001 From: Andrey Borysenko Date: Tue, 20 Feb 2024 19:41:05 +0200 Subject: [PATCH] feat(l10n): L10N support for ExApps (#227) Signed-off-by: Andrey Borysenko Signed-off-by: Alexander Piskun Co-authored-by: Alexander Piskun Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com> --- css/settings.css | 6 +- docs/tech_details/Translations.rst | 50 ++++++++++ docs/tech_details/api/fileactionsmenu.rst | 2 + docs/tech_details/api/settings.rst | 2 + docs/tech_details/api/topmenu.rst | 2 + docs/tech_details/index.rst | 1 + lib/AppInfo/Application.php | 2 + lib/Command/ExApp/Register.php | 14 +++ lib/Command/ExApp/Update.php | 46 ++++++--- lib/Fetcher/ExAppArchiveFetcher.php | 111 ++++++++++++++++++++-- lib/Middleware/ExAppUIL10NMiddleware.php | 68 +++++++++++++ lib/Middleware/ExAppUiMiddleware.php | 13 ++- lib/Notifications/ExAppNotifier.php | 7 +- lib/Service/AppAPIService.php | 6 ++ lib/Service/ExAppService.php | 36 +++---- lib/Service/UI/TopMenuService.php | 5 +- src/components/Apps/AppDetails.vue | 2 +- src/components/Apps/AppList.vue | 13 --- src/components/Apps/ScopesDetails.vue | 2 +- src/filesplugin.js | 98 +++++++++---------- src/filesplugin28.js | 43 +++++---- 21 files changed, 391 insertions(+), 138 deletions(-) create mode 100644 docs/tech_details/Translations.rst create mode 100644 lib/Middleware/ExAppUIL10NMiddleware.php diff --git a/css/settings.css b/css/settings.css index f40a406e..5a5f8b7e 100644 --- a/css/settings.css +++ b/css/settings.css @@ -278,15 +278,15 @@ .apps-list .toolbar { height: 60px; - padding: 8px; - padding-left: 60px; + padding: 38px; + padding-left: 70px; width: 100%; background-color: var(--color-main-background); position: sticky; top: 0; z-index: 1; display: flex; - align-items: center + align-items: center; } .apps-list.installed { diff --git a/docs/tech_details/Translations.rst b/docs/tech_details/Translations.rst new file mode 100644 index 00000000..5b3ef0a5 --- /dev/null +++ b/docs/tech_details/Translations.rst @@ -0,0 +1,50 @@ +Translations +============ + +ExApps translations work in the `same way as for PHP apps `_ with a few adjustments +and differences. + +In short, you just have to provide a ``l10n/.js`` (for front-end) and ``l10n/.json`` (for back-end) files for your app. + + +Front-end +********* + +For the front-end part AppAPI will inject the current user's locale ``l10n/.js`` script, so that access to translated strings in kept the same as was before in PHP apps. + +.. note:: + + ExApp l10n files are included only on the ExApp UI pages (:ref:`Top Menu `), Files (for :ref:`FileAction `) and Settings (for :ref:`DeclarativeSettings `). + + +Back-end +******** + +For the back-end part of ExApp which can be written in different programming languages it is **up to the developer to decide** how to handle and translations files. +There is an example repository with translations: `UI example with translations `_. + + +Manual install +************** + +For ``manual-install`` type administrator will have to manually extract to the server's `writable apps directory `_ ``l10n`` folder of ExApp +(e.g. ``/path/to/apps-writable//l10n/*.(js|json)``). +This will allow server to access ExApp's strings with translations. + +.. note:: + + Only ``l10n`` folder must be present on the server side, ``appinfo/info.xml`` could lead to be misdetected by server as PHP app folder. + + + +Docker install +************** + +For ``docker-install`` type AppAPI will extract ``l10n`` folder to the server automatically during installation from ExApp release archive. + + +Translation tool +**************** + +To add support for your language in Nextcloud `translationtool `_ feel free to create an issue in the `nextcloud/docker-ci `_ repository +or open a pull request with the changes made in ``createPotFile`` function to extract and convert translation strings. diff --git a/docs/tech_details/api/fileactionsmenu.rst b/docs/tech_details/api/fileactionsmenu.rst index 88866c7e..1571db61 100644 --- a/docs/tech_details/api/fileactionsmenu.rst +++ b/docs/tech_details/api/fileactionsmenu.rst @@ -1,3 +1,5 @@ +.. _file_actions_menu_section: + ================= File Actions Menu ================= diff --git a/docs/tech_details/api/settings.rst b/docs/tech_details/api/settings.rst index fa9639cc..b379ac8b 100644 --- a/docs/tech_details/api/settings.rst +++ b/docs/tech_details/api/settings.rst @@ -1,3 +1,5 @@ +.. _declarative_settings_section: + ==================== Declarative Settings ==================== diff --git a/docs/tech_details/api/topmenu.rst b/docs/tech_details/api/topmenu.rst index 6afdcdfe..22e572e1 100644 --- a/docs/tech_details/api/topmenu.rst +++ b/docs/tech_details/api/topmenu.rst @@ -1,3 +1,5 @@ +.. _top_menu_section: + ============== Top Menu Entry ============== diff --git a/docs/tech_details/index.rst b/docs/tech_details/index.rst index 11513d6b..6e6f7a06 100644 --- a/docs/tech_details/index.rst +++ b/docs/tech_details/index.rst @@ -9,4 +9,5 @@ Technical details ApiScopes Deployment Authentication + Translations api/index.rst diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index e2c2bcb1..8b0a0c89 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -13,6 +13,7 @@ use OCA\AppAPI\Listener\SabrePluginAuthInitListener; use OCA\AppAPI\Listener\UserDeletedListener; use OCA\AppAPI\Middleware\AppAPIAuthMiddleware; +use OCA\AppAPI\Middleware\ExAppUIL10NMiddleware; use OCA\AppAPI\Middleware\ExAppUiMiddleware; use OCA\AppAPI\Notifications\ExAppAdminNotifier; use OCA\AppAPI\Notifications\ExAppNotifier; @@ -63,6 +64,7 @@ public function register(IRegistrationContext $context): void { $context->registerCapability(PublicCapabilities::class); $context->registerMiddleware(AppAPIAuthMiddleware::class); $context->registerMiddleware(ExAppUiMiddleware::class); + $context->registerMiddleware(ExAppUIL10NMiddleware::class, true); $context->registerEventListener(SabrePluginAuthInitEvent::class, SabrePluginAuthInitListener::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerNotifierService(ExAppNotifier::class); diff --git a/lib/Command/ExApp/Register.php b/lib/Command/ExApp/Register.php index a47e6ea5..4035192c 100644 --- a/lib/Command/ExApp/Register.php +++ b/lib/Command/ExApp/Register.php @@ -7,6 +7,7 @@ use OCA\AppAPI\AppInfo\Application; use OCA\AppAPI\DeployActions\DockerActions; use OCA\AppAPI\DeployActions\ManualActions; +use OCA\AppAPI\Fetcher\ExAppArchiveFetcher; use OCA\AppAPI\Service\AppAPIService; use OCA\AppAPI\Service\DaemonConfigService; use OCA\AppAPI\Service\ExAppApiScopeService; @@ -40,6 +41,7 @@ public function __construct( private readonly ExAppService $exAppService, private readonly ISecureRandom $random, private readonly LoggerInterface $logger, + private readonly ExAppArchiveFetcher $exAppArchiveFetcher, ) { parent::__construct(); } @@ -172,6 +174,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + if (!empty($appInfo['external-app']['translations_folder'])) { + $result = $this->exAppArchiveFetcher->installTranslations($appId, $appInfo['external-app']['translations_folder']); + if ($result) { + $this->logger->error(sprintf('Failed to install translations for %s. Reason: %s', $appId, $result)); + if ($outputConsole) { + $output->writeln(sprintf('Failed to install translations for %s. Reason: %s', $appId, $result)); + } + $this->exAppService->unregisterExApp($appId); + return 3; + } + } + $auth = []; if ($daemonConfig->getAcceptsDeployId() === $this->dockerActions->getAcceptsDeployId()) { $deployParams = $this->dockerActions->buildDeployParams($daemonConfig, $appInfo); diff --git a/lib/Command/ExApp/Update.php b/lib/Command/ExApp/Update.php index f864983e..6cef3efc 100644 --- a/lib/Command/ExApp/Update.php +++ b/lib/Command/ExApp/Update.php @@ -6,6 +6,8 @@ use OCA\AppAPI\Db\ExAppScope; use OCA\AppAPI\DeployActions\DockerActions; +use OCA\AppAPI\DeployActions\ManualActions; +use OCA\AppAPI\Fetcher\ExAppArchiveFetcher; use OCA\AppAPI\Service\AppAPIService; use OCA\AppAPI\Service\DaemonConfigService; use OCA\AppAPI\Service\ExAppApiScopeService; @@ -30,7 +32,9 @@ public function __construct( private readonly ExAppApiScopeService $exAppApiScopeService, private readonly DaemonConfigService $daemonConfigService, private readonly DockerActions $dockerActions, + private readonly ManualActions $manualActions, private readonly LoggerInterface $logger, + private readonly ExAppArchiveFetcher $exAppArchiveFetcher, ) { parent::__construct(); } @@ -81,12 +85,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int } return 2; } - if ($daemonConfig->getAcceptsDeployId() === 'manual-install') { - $this->logger->error('For "manual-install" deployId update is done manually'); + + $actionsDeployIds = [ + $this->dockerActions->getAcceptsDeployId(), + $this->manualActions->getAcceptsDeployId(), + ]; + if (!in_array($daemonConfig->getAcceptsDeployId(), $actionsDeployIds)) { + $this->logger->error(sprintf('Daemon config %s actions for %s not found.', $daemonConfig->getName(), $daemonConfig->getAcceptsDeployId())); if ($outputConsole) { - $output->writeln('For "manual-install" deployId update is done manually'); + $output->writeln(sprintf('Daemon config %s actions for %s not found.', $daemonConfig->getName(), $daemonConfig->getAcceptsDeployId())); } - return 1; + return 2; } if ($exApp->getVersion() === $appInfo['version']) { @@ -104,9 +113,9 @@ protected function execute(InputInterface $input, OutputInterface $output): int if ($exApp->getEnabled()) { if ($this->service->disableExApp($exApp)) { - $this->logger->info(sprintf('ExApp %s disabled.', $appId)); + $this->logger->info(sprintf('ExApp %s successfully disabled.', $appId)); if ($outputConsole) { - $output->writeln(sprintf('ExApp %s disabled.', $appId)); + $output->writeln(sprintf('ExApp %s successfully disabled.', $appId)); } } } else { @@ -116,6 +125,16 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + if (!empty($appInfo['external-app']['translations_folder'])) { + $result = $this->exAppArchiveFetcher->installTranslations($appId, $appInfo['external-app']['translations_folder']); + if ($result) { + $this->logger->error(sprintf('Failed to install translations for %s. Reason: %s', $appId, $result)); + if ($outputConsole) { + $output->writeln(sprintf('Failed to install translations for %s. Reason: %s', $appId, $result)); + } + } + } + $appInfo['port'] = $exApp->getPort(); $appInfo['secret'] = $exApp->getSecret(); $auth = []; @@ -161,12 +180,15 @@ protected function execute(InputInterface $input, OutputInterface $output): int $auth, ); } else { - $this->logger->error(sprintf('Daemon config %s actions for %s not found.', $daemonConfig->getName(), $daemonConfig->getAcceptsDeployId())); - if ($outputConsole) { - $output->writeln(sprintf('Daemon config %s actions for %s not found.', $daemonConfig->getName(), $daemonConfig->getAcceptsDeployId())); - } - $this->exAppService->setStatusError($exApp, 'Daemon actions not found'); - return 2; + $this->manualActions->deployExApp($exApp, $daemonConfig); + $exAppUrl = $this->manualActions->resolveExAppUrl( + $appId, + $daemonConfig->getProtocol(), + $daemonConfig->getHost(), + $daemonConfig->getDeployConfig(), + (int) $appInfo['port'], + $auth, + ); } if (!$this->service->heartbeatExApp($exAppUrl, $auth)) { diff --git a/lib/Fetcher/ExAppArchiveFetcher.php b/lib/Fetcher/ExAppArchiveFetcher.php index 698f70b5..9d2dd986 100644 --- a/lib/Fetcher/ExAppArchiveFetcher.php +++ b/lib/Fetcher/ExAppArchiveFetcher.php @@ -7,8 +7,11 @@ use Exception; use OC\Archive\TAR; use OCP\Http\Client\IClientService; +use OCP\IConfig; use OCP\ITempManager; use phpseclib\File\X509; +use RecursiveDirectoryIterator; +use RecursiveIteratorIterator; use SimpleXMLElement; /** @@ -17,8 +20,9 @@ class ExAppArchiveFetcher { public function __construct( - private ITempManager $tempManager, - private IClientService $clientService, + private readonly ITempManager $tempManager, + private readonly IClientService $clientService, + private readonly IConfig $config, ) { } @@ -26,7 +30,7 @@ public function __construct( * Based on regular app download algorithm. * Download ExApp release archive, verify signature extract info.xml and return its object */ - public function downloadInfoXml(array $exAppAppstoreData): ?SimpleXMLElement { + public function downloadInfoXml(array $exAppAppstoreData, string &$extractedDir): ?SimpleXMLElement { // 1. Signature check if (!$this->checkExAppSignature($exAppAppstoreData)) { return null; @@ -56,9 +60,7 @@ public function downloadInfoXml(array $exAppAppstoreData): ?SimpleXMLElement { } $allFiles = scandir($extractDir); - $folders = array_diff($allFiles, ['.', '..']); - $folders = array_values($folders); - + $folders = array_values(array_diff($allFiles, ['.', '..'])); if (count($folders) > 1) { return null; } @@ -68,10 +70,58 @@ public function downloadInfoXml(array $exAppAppstoreData): ?SimpleXMLElement { if ((string) $infoXml->id !== $exAppAppstoreData['id']) { return null; } - + $extractedDir = $extractDir . '/' . $folders[0]; return $infoXml; } + public function installTranslations(string $appId, string $dirTranslations): string { + if (!file_exists($dirTranslations)) { + return sprintf('Can not access directory: %s', $dirTranslations); + } + $writableAppPath = $this->getExAppFolder($appId); + if (!$writableAppPath) { + return 'Can not find writable apps path to perform installation.'; + } + + $installL10NPath = $writableAppPath . '/l10n'; + if (file_exists($installL10NPath)) { + $this->rmdirr($installL10NPath); // Remove old l10n folder and files if exists + } + $this->copyr($dirTranslations, $installL10NPath); + return ''; + } + + public function getExAppFolder(string $appId): ?string { + $appsPaths = $this->config->getSystemValue('apps_paths'); + $existingPath = ''; + foreach ($appsPaths as $appPath) { + if ($appPath['writable'] && file_exists($appPath['path'] . '/' . $appId)) { + $existingPath = $appPath['path'] . '/' . $appId; + } + } + if (!empty($existingPath)) { + return $existingPath; + } + foreach ($appsPaths as $appPath) { + if ($appPath['writable']) { + if (mkdir($appPath['path'] . '/' . $appId)) { + return $appPath['path'] . '/' . $appId; + } + } + } + return null; + } + + public function removeExAppFolder(string $appId): void { + foreach ($this->config->getSystemValue('apps_paths') as $appPath) { + if ($appPath['writable']) { + if (file_exists($appPath['path'] . '/' . $appId)) { + $this->rmdirr($appPath['path'] . '/' . $appId); + } + } + } + } + /** * @psalm-suppress UndefinedClass */ @@ -149,4 +199,51 @@ private function splitCerts(string $cert): array { return $matches[0]; } + + public function rmdirr(string $dir, bool $deleteSelf = true): bool { + if (is_dir($dir)) { + $files = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS), + RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($files as $fileInfo) { + if ($fileInfo->isLink()) { + unlink($fileInfo->getPathname()); + } elseif ($fileInfo->isDir()) { + rmdir($fileInfo->getRealPath()); + } else { + unlink($fileInfo->getRealPath()); + } + } + if ($deleteSelf) { + rmdir($dir); + } + } elseif (file_exists($dir)) { + if ($deleteSelf) { + unlink($dir); + } + } + if (!$deleteSelf) { + return true; + } + + return !file_exists($dir); + } + + public function copyr(string $src, string $dest): void { + if (is_dir($src)) { + if (!is_dir($dest)) { + mkdir($dest); + } + $files = scandir($src); + foreach ($files as $file) { + if ($file != "." && $file != "..") { + self::copyr("$src/$file", "$dest/$file"); + } + } + } elseif (file_exists($src)) { + copy($src, $dest); + } + } } diff --git a/lib/Middleware/ExAppUIL10NMiddleware.php b/lib/Middleware/ExAppUIL10NMiddleware.php new file mode 100644 index 00000000..1ad0954c --- /dev/null +++ b/lib/Middleware/ExAppUIL10NMiddleware.php @@ -0,0 +1,68 @@ +request->getRequestUri(); + $loadL10N = false; + foreach (self::routesToLoadL10N as $route) { + $url = str_replace('/index.php', '', $url); + $url = str_replace('/apps', '', $url); + if (str_starts_with($url, $route)) { + $loadL10N = true; + break; + } + } + if (!$loadL10N) { + return $output; + } + /** @var array $exApp */ + foreach ($this->exAppService->getExAppsList() as $exApp) { + $appId = $exApp['id']; + $lang = $this->l10nFactory->findLanguage($appId); + $availableLocales = $this->l10nFactory->findAvailableLanguages($appId); + if (in_array($lang, $availableLocales) && $lang !== 'en') { + $headPos = stripos($output, ''); + try { + $l10nScriptSrc = $this->appManager->getAppWebPath($appId) . '/l10n/' . $lang . '.js'; + $nonce = $this->nonceManager->getNonce(); + $output = substr_replace($output, '', $headPos, 0); + } catch (AppPathNotFoundException) { + $this->logger->debug(sprintf('Can not find translations for %s ExApp.', $appId)); + } + } + } + return $output; + } +} diff --git a/lib/Middleware/ExAppUiMiddleware.php b/lib/Middleware/ExAppUiMiddleware.php index 6b3b2052..5d7f7686 100644 --- a/lib/Middleware/ExAppUiMiddleware.php +++ b/lib/Middleware/ExAppUiMiddleware.php @@ -5,8 +5,8 @@ namespace OCA\AppAPI\Middleware; use OCA\AppAPI\AppInfo\Application; -use OCA\AppAPI\Controller\TopMenuController; +use OCA\AppAPI\Controller\TopMenuController; use OCP\AppFramework\Controller; use OCP\AppFramework\Http\Response; use OCP\AppFramework\Middleware; @@ -16,25 +16,24 @@ class ExAppUiMiddleware extends Middleware { public function __construct( - protected IRequest $request, - private INavigationManager $navigationManager, + protected IRequest $request, + private readonly INavigationManager $navigationManager, ) { } public function beforeOutput(Controller $controller, string $methodName, string $output) { if (($controller instanceof TopMenuController) && ($controller->postprocess)) { - $correctedOutput = preg_replace( + $output = preg_replace( '/(href=")(\/.*?)(\/app_api\/css\/)(proxy\/.*css.*")/', '$1/index.php/apps/app_api/$4', $output); foreach ($controller->jsProxyMap as $key => $value) { - $correctedOutput = preg_replace( + $output = preg_replace( '/(src=")(\/.*?)(\/app_api\/js\/)(proxy_js\/' . $key . '.js)(.*")/', '$1/index.php/apps/app_api/proxy/' . $value . '.js$5', - $correctedOutput, + $output, limit: 1); } - return $correctedOutput; } return $output; } diff --git a/lib/Notifications/ExAppNotifier.php b/lib/Notifications/ExAppNotifier.php index 8dcea20b..11e0266f 100644 --- a/lib/Notifications/ExAppNotifier.php +++ b/lib/Notifications/ExAppNotifier.php @@ -18,6 +18,7 @@ public function __construct( private readonly IFactory $factory, private readonly IURLGenerator $url, private readonly ExAppService $service, + private readonly IFactory $l10nFactory ) { } @@ -40,6 +41,8 @@ public function prepare(INotification $notification, string $languageCode): INot throw new InvalidArgumentException('ExApp is disabled'); } + $l = $this->l10nFactory->get($notification->getApp(), $languageCode); + $parameters = $notification->getSubjectParameters(); if (isset($parameters['link']) && $parameters['link'] !== '') { $notification->setLink($parameters['link']); @@ -47,10 +50,10 @@ public function prepare(INotification $notification, string $languageCode): INot $notification->setIcon($this->url->imagePath(Application::APP_ID, 'app-dark.svg')); if (isset($parameters['rich_subject']) && isset($parameters['rich_subject_params'])) { - $notification->setRichSubject($parameters['rich_subject'], $parameters['rich_subject_params']); + $notification->setRichSubject($l->t($parameters['rich_subject']), $parameters['rich_subject_params']); } if (isset($parameters['rich_message']) && isset($parameters['rich_message_params'])) { - $notification->setRichMessage($parameters['rich_message'], $parameters['rich_message_params']); + $notification->setRichMessage($l->t($parameters['rich_message']), $parameters['rich_message_params']); } return $notification; diff --git a/lib/Service/AppAPIService.php b/lib/Service/AppAPIService.php index 88aca6fe..687d37d6 100644 --- a/lib/Service/AppAPIService.php +++ b/lib/Service/AppAPIService.php @@ -20,6 +20,7 @@ use OCP\ISession; use OCP\IUserManager; use OCP\IUserSession; +use OCP\L10N\IFactory; use OCP\Log\ILogFactory; use OCP\Security\Bruteforce\IThrottler; use Psr\Log\LoggerInterface; @@ -37,6 +38,7 @@ public function __construct( private readonly IUserSession $userSession, private readonly ISession $session, private readonly IUserManager $userManager, + private readonly IFactory $l10nFactory, private readonly ExNotificationsManager $exNotificationsManager, private readonly ExAppService $exAppService, private readonly ExAppUsersService $exAppUsersService, @@ -98,6 +100,10 @@ public function requestToExApp( } else { $options['headers'] = $this->commonService->buildAppAPIAuthHeaders($request, $userId, $exApp->getAppid(), $exApp->getVersion(), $exApp->getSecret()); } + $lang = $this->l10nFactory->findLanguage($exApp->getAppid()); + if (!isset($options['headers']['Accept-Language'])) { + $options['headers']['Accept-Language'] = $lang; + } $options['nextcloud'] = [ 'allow_local_address' => true, // it's required as we are using ExApp appid as hostname (usually local) ]; diff --git a/lib/Service/ExAppService.php b/lib/Service/ExAppService.php index 89703d52..871e1035 100644 --- a/lib/Service/ExAppService.php +++ b/lib/Service/ExAppService.php @@ -118,6 +118,7 @@ public function unregisterExApp(string $appId): bool { $this->textProcessingService->unregisterExAppTextProcessingProviders($appId); $this->translationService->unregisterExAppTranslationProviders($appId); $this->settingsService->unregisterExAppForms($appId); + $this->exAppArchiveFetcher->removeExAppFolder($appId); if ($this->exAppMapper->deleteExApp($appId) === 1) { $this->cache->remove('/exApp_' . $appId); return true; @@ -245,26 +246,7 @@ public function updateExApp(ExApp $exApp, array $fields = ['version', 'name', 'p return false; } - /** - * Get info from App Store releases for specific ExApp and its current version - */ - public function getExAppInfoFromAppstore(ExApp $exApp): ?SimpleXMLElement { - $exApps = $this->exAppFetcher->get(); - $exAppAppstoreData = array_filter($exApps, function (array $exAppItem) use ($exApp) { - return $exAppItem['id'] === $exApp->getAppid() && count(array_filter($exAppItem['releases'], function (array $release) use ($exApp) { - return $release['version'] === $exApp->getVersion(); - })) === 1; - }); - if (count($exAppAppstoreData) === 1) { - return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData); - } - return null; - } - - /** - * Get latest ExApp release info by ExApp appid (in case of first installation or update) - */ - public function getLatestExAppInfoFromAppstore(string $appId): ?SimpleXMLElement { + public function getLatestExAppInfoFromAppstore(string $appId, string &$extractedDir): ?SimpleXMLElement { $exApps = $this->exAppFetcher->get(); $exAppAppstoreData = array_filter($exApps, function (array $exAppItem) use ($appId) { return $exAppItem['id'] === $appId && count($exAppItem['releases']) > 0; @@ -272,7 +254,7 @@ public function getLatestExAppInfoFromAppstore(string $appId): ?SimpleXMLElement $exAppAppstoreData = end($exAppAppstoreData); $exAppReleaseInfo = end($exAppAppstoreData['releases']); if ($exAppReleaseInfo !== false) { - return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData); + return $this->exAppArchiveFetcher->downloadInfoXml($exAppAppstoreData, $extractedDir); } return null; } @@ -287,12 +269,13 @@ private function resetCaches(): void { } public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo): array { + $extractedDir = ''; if ($jsonInfo !== null) { $appInfo = json_decode($jsonInfo, true); # fill 'id' if it is missing(this field was called `appid` in previous versions in json) $appInfo['id'] = $appInfo['id'] ?? $appId; # during manual install JSON can have all values at root level - foreach (['docker-install', 'scopes', 'system'] as $key) { + foreach (['docker-install', 'scopes', 'system', 'translations_folder'] as $key) { if (isset($appInfo[$key])) { $appInfo['external-app'][$key] = $appInfo[$key]; unset($appInfo[$key]); @@ -310,12 +293,19 @@ public function getAppInfo(string $appId, ?string $infoXml, ?string $jsonInfo): return ['error' => sprintf('Failed to load info.xml from %s', $infoXml)]; } } else { - $xmlAppInfo = $this->getLatestExAppInfoFromAppstore($appId); + $xmlAppInfo = $this->getLatestExAppInfoFromAppstore($appId, $extractedDir); } $appInfo = json_decode(json_encode((array)$xmlAppInfo), true); if (isset($appInfo['external-app']['scopes']['value'])) { $appInfo['external-app']['scopes'] = $appInfo['external-app']['scopes']['value']; } + if ($extractedDir) { + if (file_exists($extractedDir . '/l10n')) { + $appInfo['translations_folder'] = $extractedDir . '/l10n'; + } else { + $this->logger->info(sprintf('Application %s does not support translations', $appId)); + } + } # TO-DO: remove this in AppAPI 2.3.0 if (isset($appInfo['external-app']['scopes']['required']['value'])) { $appInfo['external-app']['scopes'] = $appInfo['external-app']['scopes']['required']['value']; diff --git a/lib/Service/UI/TopMenuService.php b/lib/Service/UI/TopMenuService.php index c51d18b1..34405776 100644 --- a/lib/Service/UI/TopMenuService.php +++ b/lib/Service/UI/TopMenuService.php @@ -18,6 +18,7 @@ use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserSession; +use OCP\L10N\IFactory; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Container\NotFoundExceptionInterface; @@ -55,6 +56,8 @@ public function registerMenuEntries(ContainerInterface $container): void { } $container->get(INavigationManager::class)->add(function () use ($container, $menuEntry) { $urlGenerator = $container->get(IURLGenerator::class); + /** @var IFactory $l10nFactory */ + $l10nFactory = $container->get(IFactory::class); $appId = $menuEntry->getAppid(); $entryName = $menuEntry->getName(); $icon = $menuEntry->getIcon(); @@ -70,7 +73,7 @@ public function registerMenuEntries(ContainerInterface $container): void { $urlGenerator->linkToRoute( 'app_api.ExAppProxy.ExAppGet', ['appId' => $appId, 'other' => $icon] ), - 'name' => $menuEntry->getDisplayName(), + 'name' => $l10nFactory->get($appId)->t($menuEntry->getDisplayName()), ]; }); } diff --git a/src/components/Apps/AppDetails.vue b/src/components/Apps/AppDetails.vue index dc6f3900..7e3be1ee 100644 --- a/src/components/Apps/AppDetails.vue +++ b/src/components/Apps/AppDetails.vue @@ -94,7 +94,7 @@ diff --git a/src/components/Apps/AppList.vue b/src/components/Apps/AppList.vue index 2655e47f..00cb669d 100644 --- a/src/components/Apps/AppList.vue +++ b/src/components/Apps/AppList.vue @@ -233,17 +233,4 @@ export default { display: flex; flex-wrap: wrap; } - -.toolbar { - height: 60px; - padding: 8px; - padding-left: 60px; - width: 100%; - background-color: var(--color-main-background); - position: sticky; - top: 0; - z-index: 1; - display: flex; - align-items: center; -} diff --git a/src/components/Apps/ScopesDetails.vue b/src/components/Apps/ScopesDetails.vue index 0263c777..e7b623ae 100644 --- a/src/components/Apps/ScopesDetails.vue +++ b/src/components/Apps/ScopesDetails.vue @@ -1,7 +1,7 @@