Skip to content

Commit 664a286

Browse files
authored
Merge pull request #6050 from nextcloud/backport/6039/stable32
[stable32] refactor(l10n): move language tag logic to backend
2 parents 1917720 + 5abcaad commit 664a286

9 files changed

Lines changed: 136 additions & 69 deletions

File tree

lib/Conversion/ConversionProvider.php

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
namespace OCA\Richdocuments\Conversion;
1111

12+
use OCA\Richdocuments\Service\LanguageService;
1213
use OCA\Richdocuments\Service\RemoteService;
1314
use OCP\Files\Conversion\ConversionMimeProvider;
1415
use OCP\Files\Conversion\IConversionProvider;
@@ -53,6 +54,7 @@ public function __construct(
5354
private RemoteService $remoteService,
5455
private LoggerInterface $logger,
5556
IFactory $l10nFactory,
57+
private LanguageService $languageService,
5658
) {
5759
$this->l10n = $l10nFactory->get('richdocuments');
5860
}
@@ -145,17 +147,10 @@ public function convertFile(File $file, string $targetMimeType): mixed {
145147
return $this->remoteService->convertFileTo(
146148
$file,
147149
$targetFileExtension,
148-
conversionOptions: ['lang' => $this->getConversionLanguage()]
150+
conversionOptions: ['lang' => $this->languageService->getBCP47LanguageTag()]
149151
);
150152
}
151153

152-
private function getConversionLanguage(): string {
153-
$locale = $this->l10n->getLocaleCode();
154-
$language = $locale !== '' ? $locale : $this->l10n->getLanguageCode();
155-
156-
return str_replace('_', '-', $language);
157-
}
158-
159154
private function getMimeProvidersFor(array $inputMimeTypes, string $outputMimeType): array {
160155
$outputMimeInfo = $this->getMimeInfoFor($outputMimeType);
161156
if ($outputMimeInfo === null) {

lib/Service/InitialStateService.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function __construct(
3030
private IURLGenerator $urlGenerator,
3131
private Defaults $themingDefaults,
3232
private IConfig $config,
33+
private LanguageService $languageService,
3334
private ?string $userId,
3435
) {
3536
}
@@ -44,6 +45,7 @@ public function provideCapabilities(): void {
4445
$this->initialState->provideInitialState('hasNextcloudBranding', $this->capabilitiesService->hasNextcloudBranding());
4546
$this->initialState->provideInitialState('instanceId', $this->config->getSystemValue('instanceid'));
4647
$this->initialState->provideInitialState('wopi_callback_url', $this->appConfig->getNextcloudUrl());
48+
$this->initialState->provideInitialState('bcp47Language', $this->languageService->getBCP47LanguageTag());
4749

4850
$this->provideOptions();
4951

lib/Service/LanguageService.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Richdocuments\Service;
11+
12+
use OCA\Richdocuments\AppInfo\Application;
13+
use OCP\L10N\IFactory;
14+
15+
class LanguageService {
16+
private const LOCALE_OVERRIDES = [
17+
'de' => [
18+
'de_CH' => 'de-CH',
19+
'gsw' => 'de-CH',
20+
'gsw_CH' => 'de-CH',
21+
],
22+
'fr' => [
23+
'fr_CH' => 'fr-CH',
24+
],
25+
'it' => [
26+
'it_CH' => 'it-CH',
27+
],
28+
];
29+
30+
public function __construct(
31+
private IFactory $l10nFactory,
32+
) {
33+
}
34+
35+
/**
36+
* Converts the current user's Nextcloud language/locale settings into the
37+
* BCP 47 language tag Collabora Online expects.
38+
*/
39+
public function getBCP47LanguageTag(): string {
40+
$l10n = $this->l10nFactory->get(Application::APPNAME);
41+
42+
// getLanguageCode()/getLocaleCode() mirror @nextcloud/l10n's getLanguage()/getLocale()
43+
$language = str_replace('_', '-', $l10n->getLanguageCode());
44+
$locale = $l10n->getLocaleCode();
45+
46+
$language = match ($language) {
47+
'de-DE' => 'de', // German formal should just be treated as 'de'
48+
'es-419' => 'es-MX', // not a valid locale string in COOL
49+
default => $language,
50+
};
51+
52+
if ($language === 'en-GB' && $locale === 'en_AU') {
53+
$language = 'en-AU';
54+
}
55+
56+
return self::LOCALE_OVERRIDES[$language][$locale] ?? $language;
57+
}
58+
}

src/components/CoolFrame.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
<script>
2929
3030
import { generateCSSVarTokens, getCollaboraTheme, getUITheme } from '../helpers/coolParameters.js'
31-
import { languageToBCP47 } from '../helpers/index.js'
31+
import { loadState } from '@nextcloud/initial-state'
3232
import PostMessageService from '../services/postMessage.tsx'
3333
3434
export default {
@@ -74,7 +74,7 @@ export default {
7474
window.addEventListener('message', this.handlePostMessage)
7575
7676
if (this.iframeUrl.length > 0) {
77-
this.formAction = this.iframeUrl + '?lang=' + languageToBCP47()
77+
this.formAction = this.iframeUrl + '?lang=' + loadState('richdocuments', 'bcp47Language', '')
7878
this.isIframeLoaded = true
7979
} else {
8080
return

src/helpers/index.js

Lines changed: 0 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,52 +3,6 @@
33
* SPDX-License-Identifier: AGPL-3.0-or-later
44
*/
55

6-
import { getLanguage, getLocale } from '@nextcloud/l10n'
7-
8-
const languageToBCP47 = () => {
9-
let language = getLanguage().replace(/_/g, '-')
10-
const locale = getLocale()
11-
12-
// German formal should just be treated as 'de'
13-
if (language === 'de-DE') {
14-
language = 'de'
15-
}
16-
17-
// es-419 should be mapped as this is not considered a valid locale string in COOL
18-
if (language === 'es-419') {
19-
language = 'es-MX'
20-
}
21-
22-
// Australia
23-
if (language === 'en-GB' && locale === 'en_AU') {
24-
language = 'en-AU'
25-
}
26-
27-
// special case where setting the bc47 region depending on the locale setting makes sense
28-
const whitelist = {
29-
de: {
30-
de_CH: 'de-CH',
31-
gsw: 'de-CH',
32-
gsw_CH: 'de-CH',
33-
},
34-
fr: {
35-
fr_CH: 'fr-CH',
36-
},
37-
it: {
38-
it_CH: 'it-CH',
39-
},
40-
}
41-
const matchingWhitelist = whitelist[language]
42-
if (typeof matchingWhitelist !== 'undefined' && typeof matchingWhitelist[locale] !== 'undefined') {
43-
return matchingWhitelist[locale]
44-
}
45-
46-
// Collabora Online expects BCP47 language tag syntax.
47-
// When the Nextcloud language consists of two parts, we send both,
48-
// as the region is then provided by the language setting.
49-
return language
50-
}
51-
526
const getNextcloudVersion = () => {
537
return parseInt(OC.config.version.split('.')[0])
548
}
@@ -68,7 +22,6 @@ const getRandomId = (length = 5) => {
6822
}
6923

7024
export {
71-
languageToBCP47,
7225
getNextcloudVersion,
7326
splitPath,
7427
getRandomId,

src/helpers/url.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
import { getRootUrl, generateUrl } from '@nextcloud/router'
77
import { getSharingToken } from '@nextcloud/sharing/public'
8-
import { languageToBCP47 } from './index.js'
98
import Config from './../services/config.tsx'
109

1110
const getSearchParam = (name) => {
@@ -39,7 +38,7 @@ const getWopiUrl = ({ fileId, readOnly, closeButton, revisionHistory, target = u
3938
// https://<loolwsd-server>:9980/hosting/discovery
4039
return Config.get('urlsrc')
4140
+ 'WOPISrc=' + encodeURIComponent(getWopiSrc(fileId))
42-
+ '&lang=' + languageToBCP47()
41+
+ '&lang=' + Config.get('bcp47Language')
4342
+ (closeButton ? '&closebutton=1' : '')
4443
+ (revisionHistory ? '&revisionhistory=1' : '')
4544
+ (readOnly ? '&permission=readonly' : '')

src/services/config.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class ConfigService {
1111
constructor() {
1212
this.values = {
1313
wopi_callback_url: loadState('richdocuments', 'wopi_callback_url', ''),
14+
bcp47Language: loadState('richdocuments', 'bcp47Language', ''),
1415
...loadState('richdocuments', 'document', {}),
1516
}
1617
}

tests/lib/Conversion/ConversionProviderTest.php

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
namespace Tests\Richdocuments\Conversion;
1111

1212
use OCA\Richdocuments\Conversion\ConversionProvider;
13+
use OCA\Richdocuments\Service\LanguageService;
1314
use OCA\Richdocuments\Service\RemoteOptionsService;
1415
use OCA\Richdocuments\Service\RemoteService;
15-
use OCA\Richdocuments\Service\SecureViewService;
1616
use OCP\Files\File;
1717
use OCP\IL10N;
1818
use OCP\L10N\IFactory;
@@ -25,7 +25,7 @@ class ConversionProviderTest extends TestCase {
2525
private LoggerInterface&MockObject $logger;
2626
private IFactory&MockObject $l10nFactory;
2727
private IL10N&MockObject $l10n;
28-
private SecureViewService&MockObject $secureViewService;
28+
private LanguageService&MockObject $languageService;
2929
private ConversionProvider $provider;
3030

3131
protected function setUp(): void {
@@ -35,7 +35,7 @@ protected function setUp(): void {
3535
$this->logger = $this->createMock(LoggerInterface::class);
3636
$this->l10nFactory = $this->createMock(IFactory::class);
3737
$this->l10n = $this->createMock(IL10N::class);
38-
$this->secureViewService = $this->createMock(SecureViewService::class);
38+
$this->languageService = $this->createMock(LanguageService::class);
3939

4040
$this->l10n->method('t')->willReturnCallback(static fn (string $text): string => $text);
4141
$this->l10nFactory->method('get')
@@ -46,21 +46,19 @@ protected function setUp(): void {
4646
$this->remoteService,
4747
$this->logger,
4848
$this->l10nFactory,
49-
$this->secureViewService,
49+
$this->languageService,
5050
);
5151
}
5252

5353
public function testConvertFilePassesCurrentLocaleToCollabora(): void {
5454
$file = $this->createMock(File::class);
5555

56-
$this->l10n->expects($this->once())
57-
->method('getLocaleCode')
58-
->willReturn('de_DE');
59-
$this->secureViewService->method('isEnabled')
60-
->willReturn(false);
56+
$this->languageService->expects($this->once())
57+
->method('getBCP47LanguageTag')
58+
->willReturn('de');
6159
$this->remoteService->expects($this->once())
6260
->method('convertFileTo')
63-
->with($file, 'pdf', RemoteOptionsService::REMOTE_TIMEOUT_DEFAULT, ['lang' => 'de-DE'])
61+
->with($file, 'pdf', RemoteOptionsService::REMOTE_TIMEOUT_DEFAULT, ['lang' => 'de'])
6462
->willReturn('pdf-content');
6563

6664
$result = $this->provider->convertFile($file, 'application/pdf');
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace Tests\Richdocuments\Service;
11+
12+
use OCA\Richdocuments\AppInfo\Application;
13+
use OCA\Richdocuments\Service\LanguageService;
14+
use OCP\IL10N;
15+
use OCP\L10N\IFactory;
16+
use PHPUnit\Framework\MockObject\MockObject;
17+
use PHPUnit\Framework\TestCase;
18+
19+
class LanguageServiceTest extends TestCase {
20+
private IFactory&MockObject $l10nFactory;
21+
private IL10N&MockObject $l10n;
22+
private LanguageService $service;
23+
24+
protected function setUp(): void {
25+
parent::setUp();
26+
27+
$this->l10nFactory = $this->createMock(IFactory::class);
28+
$this->l10n = $this->createMock(IL10N::class);
29+
$this->l10nFactory->method('get')
30+
->with(Application::APPNAME)
31+
->willReturn($this->l10n);
32+
33+
$this->service = new LanguageService($this->l10nFactory);
34+
}
35+
36+
public static function languageProvider(): array {
37+
return [
38+
'plain pass-through' => ['en', 'en_US', 'en'],
39+
'German formal is treated as plain German' => ['de_DE', 'de_DE', 'de'],
40+
'es-419 is mapped to es-MX for COOL' => ['es_419', 'es_419', 'es-MX'],
41+
'Australian English' => ['en_GB', 'en_AU', 'en-AU'],
42+
'British English without Australian locale stays en-GB' => ['en_GB', 'en_GB', 'en-GB'],
43+
'Swiss German locale' => ['de', 'de_CH', 'de-CH'],
44+
'Swiss German (gsw locale)' => ['de', 'gsw', 'de-CH'],
45+
'Swiss German (gsw_CH locale)' => ['de', 'gsw_CH', 'de-CH'],
46+
'Swiss French locale' => ['fr', 'fr_CH', 'fr-CH'],
47+
'Swiss Italian locale' => ['it', 'it_CH', 'it-CH'],
48+
'German without Swiss locale stays de' => ['de', 'de_DE', 'de'],
49+
];
50+
}
51+
52+
/**
53+
* @dataProvider languageProvider
54+
*/
55+
public function testGetBCP47LanguageTag(string $languageCode, string $localeCode, string $expected): void {
56+
$this->l10n->method('getLanguageCode')->willReturn($languageCode);
57+
$this->l10n->method('getLocaleCode')->willReturn($localeCode);
58+
59+
$this->assertSame($expected, $this->service->getBCP47LanguageTag());
60+
}
61+
}

0 commit comments

Comments
 (0)