Skip to content
Open
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
22 changes: 21 additions & 1 deletion src/components/TranslationModal.vue
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,26 @@ import logger from '../logger.js'
import { translateText } from '../service/translationService.js'
import useMainStore from '../store/mainStore.js'

const normalize = (code) => code?.toLowerCase().replace(/_/g, '-')
const primarySubtag = (code) => normalize(code)?.split('-')[0]

/**
* Find the option closest to a language code.
*
* The interface reports `pt-BR` while providers list `pt_BR`, and either side
* may omit the region the other one has, so neither separators nor regions can
* be compared verbatim.
*
* @param {Array} languages options offered by the provider
* @param {string|null} code language code to look for
* @return {object|null} the matching option, or null when the provider has none
*/
function findLanguage(languages, code) {
return languages.find((language) => normalize(language.value) === normalize(code))
?? languages.find((language) => primarySubtag(language.value) === primarySubtag(code))
?? null
}

export default {
name: 'TranslationModal',

Expand Down Expand Up @@ -152,7 +172,7 @@ export default {
},

async mounted() {
this.selectedTo = this.availableOutputLanguages.find((language) => language.value === this.userLanguage) || null
this.selectedTo = findLanguage(this.availableOutputLanguages, this.userLanguage)
this.selectedFrom = this.availableInputLanguages.find((language) => language.value === 'detect_language')
this.$nextTick(() => {
// FIXME trick to avoid focusTrap() from activating on NcSelect
Expand Down
113 changes: 113 additions & 0 deletions src/tests/unit/components/TranslationModal.vue.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { createLocalVue, shallowMount } from '@vue/test-utils'
import { createPinia, setActivePinia } from 'pinia'
import TranslationModal from '../../../components/TranslationModal.vue'
import Nextcloud from '../../../mixins/Nextcloud.js'
import useMainStore from '../../../store/mainStore.js'

const { getLanguage } = vi.hoisted(() => ({ getLanguage: vi.fn() }))

vi.mock('@nextcloud/l10n', async () => ({
...await vi.importActual('@nextcloud/l10n'),
getLanguage,
}))

const localVue = createLocalVue()

localVue.mixin(Nextcloud)

describe('TranslationModal', () => {
const language = (value) => ({ id: value, value, name: value })

const mountModal = ({ input = [], output = [], detectedForeignLanguage = null } = {}) => {
const store = useMainStore()
store.translationInputLanguages = input.map(language)
store.translationOutputLanguages = output.map(language)

return shallowMount(TranslationModal, {
propsData: {
message: 'Bonjour tout le monde',
richParameters: {},
detectedForeignLanguage,
},
localVue,
})
}

beforeEach(() => {
setActivePinia(createPinia())
getLanguage.mockReturnValue('en')
})

describe('target language', () => {
it('picks the interface language', () => {
getLanguage.mockReturnValue('de')

const view = mountModal({ output: ['en', 'de'] })

expect(view.vm.selectedTo?.value).toBe('de')
})

it('matches the interface hyphen against the provider underscore', () => {
getLanguage.mockReturnValue('pt-BR')

const view = mountModal({ output: ['pt_PT', 'pt_BR'] })

expect(view.vm.selectedTo?.value).toBe('pt_BR')
})

it('settles for the primary language when the provider offers no regional variant', () => {
getLanguage.mockReturnValue('pt_BR')

const view = mountModal({ output: ['en', 'pt'] })

expect(view.vm.selectedTo?.value).toBe('pt')
})

it('prefers the exact match over the primary language', () => {
getLanguage.mockReturnValue('pt-BR')

const view = mountModal({ output: ['pt', 'pt_BR'] })

expect(view.vm.selectedTo?.value).toBe('pt_BR')
})

it('selects nothing when the provider does not offer the language at all', () => {
getLanguage.mockReturnValue('de')

const view = mountModal({ output: ['en', 'fr'] })

expect(view.vm.selectedTo).toBeNull()
})
})

describe('source language', () => {
it('lets the provider detect the language when it can', () => {
const view = mountModal({ input: ['detect_language', 'fr'], detectedForeignLanguage: 'fr' })

expect(view.vm.selectedFrom?.value).toBe('detect_language')
})

it('falls back to the detected language', () => {
const view = mountModal({ input: ['en', 'fr'], detectedForeignLanguage: 'fr' })

expect(view.vm.selectedFrom?.value).toBe('fr')

Check failure on line 98 in src/tests/unit/components/TranslationModal.vue.spec.js

View workflow job for this annotation

GitHub Actions / Front-end unit tests

src/tests/unit/components/TranslationModal.vue.spec.js > TranslationModal > source language > falls back to the detected language

AssertionError: expected undefined to be 'fr' // Object.is equality - Expected: "fr" + Received: undefined ❯ src/tests/unit/components/TranslationModal.vue.spec.js:98:40
})

it('matches a regional variant of the detected language', () => {
const view = mountModal({ input: ['en', 'pt_BR'], detectedForeignLanguage: 'pt' })

expect(view.vm.selectedFrom?.value).toBe('pt_BR')

Check failure on line 104 in src/tests/unit/components/TranslationModal.vue.spec.js

View workflow job for this annotation

GitHub Actions / Front-end unit tests

src/tests/unit/components/TranslationModal.vue.spec.js > TranslationModal > source language > matches a regional variant of the detected language

AssertionError: expected undefined to be 'pt_BR' // Object.is equality - Expected: "pt_BR" + Received: undefined ❯ src/tests/unit/components/TranslationModal.vue.spec.js:104:40
})

it('selects nothing when there is no detected language', () => {
const view = mountModal({ input: ['en', 'fr'] })

expect(view.vm.selectedFrom).toBeNull()

Check failure on line 110 in src/tests/unit/components/TranslationModal.vue.spec.js

View workflow job for this annotation

GitHub Actions / Front-end unit tests

src/tests/unit/components/TranslationModal.vue.spec.js > TranslationModal > source language > selects nothing when there is no detected language

AssertionError: expected undefined to be null - Expected: null + Received: undefined ❯ src/tests/unit/components/TranslationModal.vue.spec.js:110:33
})
})
})
Loading