Skip to content

fix: preserve language in password protected folders #12206

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Bugfix: Preserve current language in password protected folder view

We've added the new `lang` URL query param to the URL of password protected folder iframe to preserve the language settings. The value is set to the currently used language.

https://github.com/owncloud/web/pull/12206
https://github.com/owncloud/web/issues/12186
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Enhancement: Add `lang` URL query parameter

We've added a new `lang` URL query parameter that allows specifying the default language in which the UI should get displayed when opened. This value is ignored when it does not match supported language codes or when a user is logged in.

https://github.com/owncloud/web/pull/12206
https://github.com/owncloud/web/issues/12186
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { ref } from 'vue'
import { Modal, useThemeStore } from '@ownclouders/web-pkg/src/composables'
import AppLoadingSpinner from '@ownclouders/web-pkg/src/components/AppLoadingSpinner.vue'
import { unref } from 'vue'
import { useGettext } from 'vue3-gettext'

const props = defineProps<{
modal: Modal
Expand All @@ -28,13 +29,15 @@ const props = defineProps<{
const iframeRef = ref<HTMLIFrameElement>()
const isLoading = ref(true)
const themeStore = useThemeStore()
const { current } = useGettext()

const iframeTitle = themeStore.currentTheme.common?.name
const iframeUrl = new URL(props.publicLink)
iframeUrl.searchParams.append('hide-logo', 'true')
iframeUrl.searchParams.append('hide-app-switcher', 'true')
iframeUrl.searchParams.append('hide-account-menu', 'true')
iframeUrl.searchParams.append('hide-navigation', 'true')
iframeUrl.searchParams.append('lang', current)

const onLoad = () => {
isLoading.value = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ describe('FolderViewModal', () => {
const iframe = wrapper.find(SELECTORS.iframe)

expect(iframe.attributes('src')).toEqual(
'https://example.org/public-link?hide-logo=true&hide-app-switcher=true&hide-account-menu=true&hide-navigation=true'
'https://example.org/public-link?hide-logo=true&hide-app-switcher=true&hide-account-menu=true&hide-navigation=true&lang=en'
)
})
})
Expand Down
3 changes: 2 additions & 1 deletion packages/web-pkg/src/composables/piniaStores/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ const OptionsConfigSchema = z.object({
hideLogo: z.boolean().optional(),
hideAppSwitcher: z.boolean().optional(),
hideAccountMenu: z.boolean().optional(),
hideNavigation: z.boolean().optional()
hideNavigation: z.boolean().optional(),
defaultLanguage: z.string().optional()
})

export type OptionsConfig = z.infer<typeof OptionsConfigSchema>
Expand Down
12 changes: 9 additions & 3 deletions packages/web-runtime/src/container/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import {
} from './sse'
import { loadAppTranslations } from '../helpers/language'
import { urlJoin } from '@ownclouders/web-client'
import { supportedLanguages } from '../defaults'

const getEmbedConfigFromQuery = (
doesEmbedEnabledOptionExists: boolean
Expand Down Expand Up @@ -159,16 +160,22 @@ export const announceConfiguration = async ({
Object.prototype.hasOwnProperty.call(rawConfig.options.embed, 'enabled')
)

const langQuery = getQueryParam('lang')
const defaultLanguage =
langQuery && supportedLanguages[langQuery] ? langQuery : navigator.language.substring(0, 2)

rawConfig.options = {
...rawConfig.options,
embed: { ...rawConfig.options?.embed, ...embedConfigFromQuery },
hideLogo: getQueryParam('hide-logo') === 'true',
hideAppSwitcher: getQueryParam('hide-app-switcher') === 'true',
hideAccountMenu: getQueryParam('hide-account-menu') === 'true',
hideNavigation: getQueryParam('hide-navigation') === 'true'
hideNavigation: getQueryParam('hide-navigation') === 'true',
defaultLanguage
}

configStore.loadConfig(rawConfig)
return rawConfig
}

/**
Expand Down Expand Up @@ -379,9 +386,8 @@ export const announceGettext = ({
...options
}: {
app: App
} & Partial<GetTextOptions>) => {
} & Partial<GetTextOptions> & { defaultLanguage: string }) => {
const gettext = createGettext({
defaultLanguage: navigator.language.substring(0, 2),
silent: true,
...options
})
Expand Down
14 changes: 11 additions & 3 deletions packages/web-runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,15 @@ export const bootstrapApp = async (configurationPath: string, appsReadyCallback:

app.provide('$router', router)

await announceConfiguration({ path: configurationPath, configStore })
const config = await announceConfiguration({ path: configurationPath, configStore })

app.use(abilitiesPlugin, createMongoAbility([]), { useGlobalProperties: true })

const gettext = announceGettext({ app, availableLanguages: supportedLanguages })
const gettext = announceGettext({
app,
availableLanguages: supportedLanguages,
defaultLanguage: config.options.defaultLanguage
})

const clientService = announceClientService({ app, configStore, authStore })
announceAuthService({
Expand Down Expand Up @@ -292,7 +296,11 @@ export const bootstrapErrorApp = async (err: Error): Promise<void> => {
await announceTheme({ app, designSystem, configStore })
console.error(err)
const translations = await loadTranslations()
const gettext = announceGettext({ app, availableLanguages: supportedLanguages })
const gettext = announceGettext({
app,
availableLanguages: supportedLanguages,
defaultLanguage: window.navigator.language.slice(0, 3)
})
announceTranslations({ gettext, coreTranslations: translations })
app.mount('#owncloud')
}
Expand Down
41 changes: 41 additions & 0 deletions packages/web-runtime/tests/unit/container/bootstrap.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,4 +201,45 @@ describe('announceConfiguration', () => {
await announceConfiguration({ path: '/config.json', configStore })
expect(configStore.options.embed.enabled).toStrictEqual(false)
})

it('should set default language to value set in URL query when it matches supported languages', async () => {
Object.defineProperty(window, 'location', {
value: {
search: '?lang=de'
},
writable: true
})
vi.spyOn(global, 'fetch').mockResolvedValue(
mock<Response>({
status: 200,
json: () => Promise.resolve({ theme: '', server: '', options: {} })
})
)
const configStore = useConfigStore()
await announceConfiguration({ path: '/config.json', configStore })
expect(configStore.options.defaultLanguage).toStrictEqual('de')
})

it('should fallback to navigator language as default language when lang in URL query does not match supported languages', async () => {
Object.defineProperty(window, 'location', {
value: {
search: '?lang=la'
},
writable: true
})
Object.defineProperty(navigator, 'language', {
value: 'de',
writable: true
})

vi.spyOn(global, 'fetch').mockResolvedValue(
mock<Response>({
status: 200,
json: () => Promise.resolve({ theme: '', server: '', options: {} })
})
)
const configStore = useConfigStore()
await announceConfiguration({ path: '/config.json', configStore })
expect(configStore.options.defaultLanguage).toStrictEqual('de')
})
})