diff --git a/kolibri/plugins/learn/assets/src/composables/useContentLink.js b/kolibri/plugins/learn/assets/src/composables/useContentLink.js
index d5bce334591..00447fa62f6 100644
--- a/kolibri/plugins/learn/assets/src/composables/useContentLink.js
+++ b/kolibri/plugins/learn/assets/src/composables/useContentLink.js
@@ -2,6 +2,7 @@ import { get } from '@vueuse/core';
import isEmpty from 'lodash/isEmpty';
import pick from 'lodash/pick';
import { computed, getCurrentInstance } from 'vue';
+import { primaryLanguageKey } from 'kolibri-common/composables/useBaseSearch';
import { ExternalPagePaths, PageNames } from '../constants';
function _decodeBackLinkQuery(query) {
@@ -17,6 +18,10 @@ export default function useContentLink(store) {
function _makeNodeLink(id, isResource, query, deviceId) {
const params = get(route).params;
+ const oldQuery = get(route).query || {};
+ if (!isResource && oldQuery[primaryLanguageKey]) {
+ query[primaryLanguageKey] = oldQuery[primaryLanguageKey];
+ }
return {
name: isResource ? PageNames.TOPICS_CONTENT : PageNames.TOPICS_TOPIC,
params: pick({ id, deviceId: deviceId || params.deviceId }, ['id', 'deviceId']),
diff --git a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue
index b0499ab0f87..4a9e997c1b8 100644
--- a/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue
+++ b/kolibri/plugins/learn/assets/src/views/LibraryPage/index.vue
@@ -49,9 +49,16 @@
v-else-if="!displayingSearchResults && !rootNodesLoading"
data-test="channels"
>
-
- handleChange('languages', val)"
/>
handleChange('grade_levels', val)"
+ @change="val => handleChange('grade_levels', val && val.value)"
/>
handleChange('accessibility_labels', val)"
+ @change="val => handleChange('accessibility_labels', val && val.value)"
/>
@@ -47,21 +40,20 @@
import { ContentLevels, AccessibilityCategories } from 'kolibri/constants';
import commonCoreStrings from 'kolibri/uiText/commonCoreStrings';
import { injectBaseSearch } from 'kolibri-common/composables/useBaseSearch';
+ import LanguageSelector from './LanguageSelector';
export default {
name: 'SelectGroup',
+ components: {
+ LanguageSelector,
+ },
mixins: [commonCoreStrings],
setup() {
- const {
- availableGradeLevels,
- availableAccessibilityOptions,
- availableLanguages,
- searchableLabels,
- } = injectBaseSearch();
+ const { availableGradeLevels, availableAccessibilityOptions, searchableLabels } =
+ injectBaseSearch();
return {
availableGradeLevels,
availableAccessibilityOptions,
- availableLanguages,
searchableLabels,
};
},
@@ -74,6 +66,10 @@
return inputKeys.every(k => Object.prototype.hasOwnProperty.call(value, k));
},
},
+ showLanguages: {
+ type: Boolean,
+ default: true,
+ },
},
computed: {
selectorStyle() {
@@ -83,19 +79,6 @@
borderRadius: '2px',
};
},
- languageOptionsList() {
- return this.availableLanguages.map(language => {
- return {
- value: language.id,
- disabled:
- this.searchableLabels && !this.searchableLabels.languages.includes(language.id),
- label: language.lang_name,
- };
- });
- },
- enabledLanguageOptions() {
- return this.languageOptionsList.filter(l => !l.disabled);
- },
accessibilityOptionsList() {
return this.availableAccessibilityOptions.map(key => {
const value = AccessibilityCategories[key];
@@ -133,15 +116,6 @@
enabledContentLevels() {
return this.contentLevelsList.filter(c => !c.disabled);
},
- langId() {
- return Object.keys(this.value.languages)[0];
- },
- selectedLanguage() {
- if (!this.langId && this.enabledLanguageOptions.length === 1) {
- return this.enabledLanguageOptions[0];
- }
- return this.languageOptionsList.find(o => o.value === this.langId) || {};
- },
accessId() {
return Object.keys(this.value.accessibility_labels)[0];
},
diff --git a/packages/kolibri-common/components/SearchFiltersPanel/index.vue b/packages/kolibri-common/components/SearchFiltersPanel/index.vue
index ba2eb32634f..d98e5244be1 100644
--- a/packages/kolibri-common/components/SearchFiltersPanel/index.vue
+++ b/packages/kolibri-common/components/SearchFiltersPanel/index.vue
@@ -85,7 +85,7 @@
{});
+ },
+ });
+
const displayingSearchResults = computed(() =>
// Happily this works even for keywords, because calling Object.keys
// on a string value will give an array of the indexes of a string
@@ -230,43 +254,66 @@ export default function useBaseSearch({
}
}
- function search() {
- const currentBaseUrl = get(baseurl);
+ function createBaseSearchGetParams(setDescendant = true) {
const getParams = {
include_coach_content: get(isAdmin) || get(isCoach) || get(isSuperuser),
- baseurl: currentBaseUrl,
+ baseurl: get(baseurl),
};
- const descValue = descendant ? get(descendant) : null;
- if (descValue) {
- getParams.tree_id = descValue.tree_id;
- getParams.lft__gt = descValue.lft;
- getParams.rght__lt = descValue.rght;
+ if (setDescendant && descendant) {
+ const descValue = get(descendant);
+ if (descValue) {
+ getParams.tree_id = descValue.tree_id;
+ getParams.lft__gt = descValue.lft;
+ getParams.rght__lt = descValue.rght;
+ }
}
- if (get(displayingSearchResults)) {
- getParams.max_results = 25;
- const terms = get(searchTerms);
- set(searchResultsLoading, true);
- for (const key of searchKeys) {
- if (key === 'categories') {
- if (terms[key][AllCategories]) {
- getParams['categories__isnull'] = false;
- continue;
- } else if (terms[key][NoCategories]) {
- getParams['categories__isnull'] = true;
- continue;
- }
- }
- if (key === 'channels' && descValue) {
+ const primaryLang = get(primaryLanguage);
+ if (primaryLang && primaryLang !== allLanguagesValue) {
+ const langCode = primaryLang.split('-')[0];
+ getParams.languages = get(languagesList)
+ .filter(lang => lang.id.startsWith(langCode))
+ .map(lang => lang.id);
+ }
+ return getParams;
+ }
+
+ function createSearchGetParams() {
+ const getParams = createBaseSearchGetParams();
+ const terms = get(searchTerms);
+ for (const key of searchKeys) {
+ if (key === 'categories') {
+ if (terms[key][AllCategories]) {
+ getParams['categories__isnull'] = false;
+ continue;
+ } else if (terms[key][NoCategories]) {
+ getParams['categories__isnull'] = true;
continue;
- }
- const keys = Object.keys(terms[key]);
- if (keys.length) {
- getParams[key] = keys;
}
}
- if (terms.keywords) {
- getParams.keywords = terms.keywords;
+ if (key === 'channels' && descendant ? get(descendant) : null) {
+ continue;
}
+ if (key === 'keywords') {
+ getParams.keywords = terms[key];
+ continue;
+ }
+ const keys = Object.keys(terms[key]);
+ if (keys.length) {
+ getParams[key] = keys;
+ }
+ }
+ return getParams;
+ }
+
+ async function search() {
+ await until(globalLabelsLoading).toBe(false);
+ const desc = descendant ? get(descendant) : null;
+ if (get(displayingSearchResults)) {
+ // If we're actually displaying search results
+ // then we need to load all the search results to display
+ set(searchResultsLoading, true);
+ const getParams = createSearchGetParams();
+ getParams.max_results = 25;
if (get(isUserLoggedIn)) {
fetchContentNodeProgress?.(getParams);
}
@@ -276,7 +323,8 @@ export default function useBaseSearch({
_setAvailableLabels(data.labels);
set(searchResultsLoading, false);
});
- } else if (descValue) {
+ } else if (desc || get(primaryLanguage)) {
+ const getParams = createBaseSearchGetParams();
getParams.max_results = 1;
ContentNodeResource.fetchCollection({ getParams }).then(data => {
_setAvailableLabels(data.labels);
@@ -305,7 +353,7 @@ export default function useBaseSearch({
}
}
- function removeFilterTag({ value, key }) {
+ function removeSearchTerm({ value, key }) {
if (key === 'keywords') {
set(searchTerms, {
...get(searchTerms),
@@ -335,6 +383,8 @@ export default function useBaseSearch({
});
}
+ watch(primaryLanguage, search);
+
// Helper to get the route information in a setup() function
function currentRoute() {
return get(route);
@@ -409,9 +459,61 @@ export default function useBaseSearch({
return _getGlobalLabels('accessibilityOptionsList', []);
});
const languagesList = computed(() => {
- return _getGlobalLabels('languagesList', []);
+ return orderBy(
+ _getGlobalLabels('languagesList', []),
+ [getContentLangActive, 'id'],
+ ['desc', 'asc'],
+ );
});
+ const languageOptionsList = computed(() => {
+ const searchableLabels = get(labels);
+ return get(languagesList).map(language => {
+ return {
+ value: language.id,
+ disabled: searchableLabels && !searchableLabels.languages.includes(language.id),
+ label: language.lang_name,
+ };
+ });
+ });
+
+ const enabledLanguageOptions = computed(() => {
+ return get(languageOptionsList).filter(l => !l.disabled);
+ });
+
+ const currentLanguageId = computed({
+ get() {
+ return get(searchTerms).languages ? Object.keys(get(searchTerms).languages)[0] : null;
+ },
+ set(value) {
+ set(searchTerms, {
+ ...get(searchTerms),
+ languages: value ? { [value]: true } : {},
+ });
+ },
+ });
+
+ async function ensurePrimaryLanguage() {
+ await until(globalLabelsLoading).toBe(false);
+ if (!get(route).query[primaryLanguageKey]) {
+ // If we have no currently selected primary language key
+ // compute the closest match to the currently active interface language
+ const closestMatchChannelLanguage = get(languagesList)[0];
+ const language =
+ closestMatchChannelLanguage && getContentLangActive(closestMatchChannelLanguage) > 1
+ ? closestMatchChannelLanguage.id
+ : allLanguagesValue;
+ router.replace({
+ query: {
+ ...currentRoute().query,
+ [primaryLanguageKey]: language,
+ },
+ });
+ return false;
+ }
+ return true;
+ }
+
provide('availableLearningActivities', learningActivitiesShown);
provide('availableLibraryCategories', libraryCategories);
provide('availableResourcesNeeded', resourcesNeeded);
@@ -419,6 +521,11 @@ export default function useBaseSearch({
provide('availableAccessibilityOptions', accessibilityOptionsList);
provide('availableLanguages', languagesList);
+ provide('currentLanguageId', currentLanguageId);
+ provide('currentPrimaryLanguageId', primaryLanguage);
+ provide('languageOptions', languageOptionsList);
+ provide('enabledLanguageOptions', enabledLanguageOptions);
+
// Provide an object of searchable labels
// This is a manifest of all the labels that could still be selected and produce search results
// given the currently applied search filters.
@@ -427,9 +534,12 @@ export default function useBaseSearch({
// Currently selected search terms
provide('activeSearchTerms', searchTerms);
+ provide('removeSearchTerm', removeSearchTerm);
+
return {
currentRoute,
searchTerms,
+ createBaseSearchGetParams,
displayingSearchResults,
searchLoading,
moreLoading,
@@ -438,8 +548,9 @@ export default function useBaseSearch({
labels,
search,
searchMore,
- removeFilterTag,
clearSearch,
+ primaryLanguage,
+ ensurePrimaryLanguage,
};
}
@@ -456,6 +567,14 @@ export function injectBaseSearch() {
const availableLanguages = inject('availableLanguages');
const searchableLabels = inject('searchableLabels');
const activeSearchTerms = inject('activeSearchTerms');
+
+ const currentLanguageId = inject('currentLanguageId');
+ const currentPrimaryLanguageId = inject('currentPrimaryLanguageId');
+ const languageOptions = inject('languageOptions');
+ const enabledLanguageOptions = inject('enabledLanguageOptions');
+
+ const removeSearchTerm = inject('removeSearchTerm');
+
return {
availableLearningActivities,
availableLibraryCategories,
@@ -465,5 +584,10 @@ export function injectBaseSearch() {
availableLanguages,
searchableLabels,
activeSearchTerms,
+ currentLanguageId,
+ currentPrimaryLanguageId,
+ languageOptions,
+ enabledLanguageOptions,
+ removeSearchTerm,
};
}
diff --git a/packages/kolibri/utils/i18n.js b/packages/kolibri/utils/i18n.js
index 769cb5f368c..cc9ce41e07b 100644
--- a/packages/kolibri/utils/i18n.js
+++ b/packages/kolibri/utils/i18n.js
@@ -1,4 +1,5 @@
import has from 'lodash/has';
+import isString from 'lodash/isString';
import Vue from 'vue';
import logger from 'kolibri-logging';
import plugin_data from 'kolibri-plugin-data';
@@ -32,15 +33,30 @@ const contentLanguageCodes = {
yo: ['yor'],
};
-export const getContentLangActive = language => {
- const langCode = languageIdToCode(currentLanguage);
+export const getContentLangActive = (language, comparisonLanguage = currentLanguage) => {
+ if (isString(language)) {
+ // If the language is a string, assume it's a language code
+ // convert to object format
+ language = {
+ id: language,
+ lang_code: languageIdToCode(language),
+ };
+ }
+ const langCode = languageIdToCode(comparisonLanguage);
const additionalCodes = contentLanguageCodes[langCode] || [];
- if (language.id.toLowerCase() === currentLanguage.toLowerCase()) {
- // Best possible match, return a 2 to have it still be truthy, but distinguishable
- // from a 1 which is a lang_code match
+ if (language.id.toLowerCase() === comparisonLanguage.toLowerCase()) {
+ // Best possible match, return a 3 to sort first
+ // Exact match between content and the language the user is using
+ return 3;
+ }
+ if (language.id === langCode || additionalCodes.includes(language.id)) {
+ // Here the language for the content has no region code, and we have an exact
+ // match with the language we are using.
return 2;
}
if (language.lang_code === langCode || additionalCodes.includes(language.lang_code)) {
+ // Here the language for the content has a region code, but the language itself
+ // matches, even if the region is different.
return 1;
}
return 0;