Skip to content

Commit

Permalink
Implement language switching for static catalogs, send Accept-Header,…
Browse files Browse the repository at this point in the history
… fix invalid references to phrase contactProvider
  • Loading branch information
m-mohr committed Jan 10, 2023
1 parent 54d1f26 commit 4502659
Show file tree
Hide file tree
Showing 11 changed files with 148 additions and 32 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ This allows or disallows loading and browsing external STAC data.
External STAC data is any data that is not a children of the given `catalogUrl`.
Must be set to `true` if a `catalogUrl` is not given as otherwise you won't be able to browse anything.

### detectLocaleFromBrowser

If set to `true`, tries to detect the preferred language of the user from the Browser.
Otherwise, defaults to the language set for `locale`.

### locale

The default language to use for STAC Browser, defaults to `en` (English).
Expand Down
1 change: 1 addition & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module.exports = {
catalogUrl: null, // Must have a slash at the end for folders/APIs
catalogTitle: "STAC Browser",
allowExternalAccess: true, // Must be true if catalogUrl is not given
detectLocaleFromBrowser: true,
locale: "en",
fallbackLocale: "en",
supportedLocales: [
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"commonmark": "^0.29.3",
"core-js": "^3.6.5",
"leaflet": "^1.8.0",
"locale-id": "^1.1.2",
"node-polyfill-webpack-plugin": "^2.0.0",
"remove-markdown": "^0.5.0",
"stac-layer": "^0.15.0",
Expand Down
42 changes: 32 additions & 10 deletions src/StacBrowser.vue
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import ErrorAlert from './components/ErrorAlert.vue';
import Sidebar from './components/Sidebar.vue';
import StacHeader from './components/StacHeader.vue';
import STAC from './models/stac';
import Utils from './utils';
import URI from 'urijs';
Expand All @@ -64,10 +65,6 @@ Vue.directive('b-toggle', VBToggle);
// Used to detect when a catalog/item becomes visible so that further data can be loaded
Vue.directive('b-visible', VBVisible);
// Setup store
Vue.use(Vuex);
const store = getStore(CONFIG);
// Setup router
Vue.use(VueRouter);
const router = new VueRouter({
Expand All @@ -76,6 +73,10 @@ const router = new VueRouter({
routes: getRoutes(CONFIG)
});
// Setup store
Vue.use(Vuex);
const store = getStore(CONFIG, router);
// Pass Config through from props to vuex
let Props = {};
let Watchers = {};
Expand Down Expand Up @@ -112,9 +113,14 @@ export default {
};
},
computed: {
...mapState(['doAuth', 'globalError', 'stateQueryParameters', 'title']),
...mapState({catalogUrlFromVueX: 'catalogUrl', localeFromVueX: 'locale'}),
...mapGetters(['displayCatalogTitle']),
...mapState(['data', 'doAuth', 'globalError', 'stateQueryParameters', 'title']),
...mapState({
catalogUrlFromVueX: 'catalogUrl',
localeFromVueX: 'locale',
detectLocaleFromBrowserFromVueX: 'detectLocaleFromBrowser',
fallbackLocaleFromVueX: 'fallbackLocale'
}),
...mapGetters(['displayCatalogTitle', 'toBrowserPath']),
browserVersion() {
if (typeof STAC_BROWSER_VERSION !== 'undefined') {
return STAC_BROWSER_VERSION;
Expand All @@ -129,9 +135,21 @@ export default {
title(title) {
document.title = title;
},
localeFromVueX(locale) {
this.$root.$i18n.locale = locale;
require(`./locales/${locale}/datepicker.js`);
localeFromVueX: {
immediate: true,
async handler (locale) {
this.$root.$i18n.locale = locale;
require(`./locales/${locale}/datepicker.js`);
if (this.data instanceof STAC) {
let link = this.data.getLocaleLink(locale, this.fallbackLocaleFromVueX);
if (link) {
let state = Object.assign({}, this.stateQueryParameters);
this.$router.push(this.toBrowserPath(link.href));
this.$store.commit('state', state);
}
}
}
},
catalogUrlFromVueX(url) {
if (url) {
Expand Down Expand Up @@ -181,6 +199,10 @@ export default {
this.$store.commit('resetPage');
this.parseQuery(to);
});
if (this.detectLocaleFromBrowserFromVueX && Array.isArray(navigator.languages)) {
this.$store.dispatch('switchLocale', navigator.languages);
}
},
mounted() {
this.$root.$on('error', this.showError);
Expand Down
6 changes: 3 additions & 3 deletions src/locales/de/texts.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
"loadApiItemsFailed": "Die Elemente des Dienstanbieters konnten nicht geladen werden.",
"loadFilteredItems": "Das Laden einer gefilterten Liste von Elementen ist leider fehlgeschlagen.",
"loadItems": "Das Laden der Liste der Elemente ist leider fehlgeschlagen.",
"networkError": "Dieses Problem kann auftreten, wenn Server den externen Zugriff über Webbrowser nicht zulassen (bspw. wenn keine CORS-Kopfzeilen vorhanden sind). @:browse.contactProvider",
"networkError": "Dieses Problem kann auftreten, wenn Server den externen Zugriff über Webbrowser nicht zulassen (bspw. wenn keine CORS-Kopfzeilen vorhanden sind). @:errors.contactProvider",
"noExternalAccess": "Der Zugriff auf externe Kataloge ist nicht erlaubt!",
"notFound": "Die angeforderte Seite existiert nicht. @:browse.contactProvider",
"serverError": "Auf dem Server ist ein Problem aufgetreten. @:browse.contactProvider",
"notFound": "Die angeforderte Seite existiert nicht. @:errors.contactProvider",
"serverError": "Auf dem Server ist ein Problem aufgetreten. @:errors.contactProvider",
"unauthorized": "Der Anfrage fehlen die Anmeldedaten, bspw. ein API-Schlüssel. Bitte geben Sie Ihre Anmeldedaten an und versuchen Sie es erneut."
},
"featureExperimental": "Diese Funktion ist noch experimentell und kann unerwartete Ergebnisse verursachen!",
Expand Down
6 changes: 3 additions & 3 deletions src/locales/en/texts.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
"loadApiItemsFailed": "The API Items could not be loaded.",
"loadFilteredItems": "Sorry, loading a filtered list of Items failed.",
"loadItems": "Sorry, loading the list of STAC Items failed.",
"networkError": "This issue may occur when servers don't allow external access via web browsers (e.g., when CORS headers are not present). @:browse.contactProvider",
"networkError": "This issue may occur when servers don't allow external access via web browsers (e.g., when CORS headers are not present). @:errors.contactProvider",
"noExternalAccess": "Accessing external catalogs is not allowed!",
"notFound": "The requested resource does not exist. @:browse.contactProvider",
"serverError": "The server encountered an issue. @:browse.contactProvider",
"notFound": "The requested resource does not exist. @:errors.contactProvider",
"serverError": "The server encountered an issue. @:errors.contactProvider",
"unauthorized": "The request lacks credentials, e.g. an API token. Please provide your credentials and try again."
},
"featureExperimental": "This feature is still experimental and may give unexpected results!",
Expand Down
6 changes: 3 additions & 3 deletions src/locales/fr/texts.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@
"loadApiItemsFailed": "Les éléments de l'API n'ont pas pu être chargés.",
"loadFilteredItems": "Désolé, le chargement d'une liste d'éléments filtrés a échoué.",
"loadItems": "Désolé, le chargement de la liste des éléments STAC a échoué.",
"networkError": "Ce problème peut se produire lorsque les serveurs n'autorisent pas l'accès externe via les navigateurs Web (par exemple, lorsque les en-têtes CORS ne sont pas présents). @:browse.contactProvider",
"networkError": "Ce problème peut se produire lorsque les serveurs n'autorisent pas l'accès externe via les navigateurs Web (par exemple, lorsque les en-têtes CORS ne sont pas présents). @:errors.contactProvider",
"noExternalAccess": "L'accès aux catalogues externes n'est pas autorisé!",
"notFound": "La ressource demandée n'existe pas. @:browse.contactProvider",
"serverError": "Le serveur a rencontré un problème. @:browse.contactProvider",
"notFound": "La ressource demandée n'existe pas. @:errors.contactProvider",
"serverError": "Le serveur a rencontré un problème. @:errors.contactProvider",
"unauthorized": "La requête requiert des informations d'authentification, comme par exemple un jeton API. Veuillez fournir vos informations d'authentification et réessayer"
},
"featureExperimental": "Cette fonctionnalité est encore expérimentale et peut donner des résultats inattendus!",
Expand Down
19 changes: 18 additions & 1 deletion src/models/stac.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Utils from "../utils";
import Migrate from '@radiantearth/stac-migrate';
import { getBest } from 'locale-id';

let stacObjCounter = 0;

Expand Down Expand Up @@ -141,13 +142,29 @@ class STAC {
return this._url;
}

getLocaleLink(locale, fallbackLocale = null) {
let links = this.getStacLinksWithRel('alternate')
.filter(link => Utils.hasText(link.hreflang));

let available;
if (Array.isArray(this.languages)) {
available = this.languages;
}
else {
available = links.map(link => link.hreflang);
}

let best = getBest(available, locale, fallbackLocale, true);
return links.find(link => link.hreflang === best) || null;
}

getStacLinksWithRel(rel, allowEmpty = true) {
return Utils.getLinksWithRels(this.links, [rel])
.filter(link => Utils.isStacMediaType(link.type, allowEmpty));
}

getStacLinkWithRel(rel, allowEmpty = true) {
let links = this.getStacLinksWithRel(rel, allowEmpty);
const links = this.getStacLinksWithRel(rel, allowEmpty);
if (links.length > 0) {
return links[0];
}
Expand Down
3 changes: 2 additions & 1 deletion src/rels.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ export const stacBrowserNavigatesTo = [
'latest-version', // version extension v
'predecessor-version',
'successor-version',
'source', // label extension
'source', // label extension,
'alternate' // language extension
].concat(stacHierarchy).concat(stacPagination);

// Rels that are handled in a special way and should not be shown in the link list
Expand Down
80 changes: 71 additions & 9 deletions src/store/index.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
import Vue from "vue";
import Vuex from "vuex";

import axios from "axios";
import Utils, { schemaMediaType } from '../utils';
import STAC from '../models/stac';
import bs58 from 'bs58';
import { ogcQueryables, stacBrowserSpecialHandling, stacPagination } from "../rels";
import { addQueryIfNotExists, isAuthenticationError, Loading, processSTAC, stacRequest } from './utils';
import { BrowserError } from '../utils';
import { getBest, prepareSupported } from 'locale-id';
import URI from "urijs";
import Queryable from '../models/queryable';

import i18n from '../i18n';
import { ogcQueryables, stacBrowserSpecialHandling, stacPagination } from "../rels";
import Utils, { schemaMediaType, BrowserError } from '../utils';
import STAC from '../models/stac';
import Queryable from '../models/queryable';

// TODO: I18N
function getStore(config) {
import { addQueryIfNotExists, isAuthenticationError, Loading, processSTAC, stacRequest } from './utils';

function getStore(config, router) {
// Local settings (e.g. for currently loaded STAC entity)
const localDefaults = () => ({
url: '',
Expand Down Expand Up @@ -347,6 +349,36 @@ function getStore(config) {
}
// If we are proxying a STAC Catalog, replace any URI with the proxied address.
return absoluteUrl.toString();
},

acceptedLanguages: state => {
const languages = {};
// Implement in ascending order:
languages['en'] = 0.1;
if (Array.isArray(state.supportedLocales)) {
state.supportedLocales.forEach(locale => languages[locale] = 0.2);
}
if (Utils.hasText(state.fallbackLocale)) {
languages[state.fallbackLocale] = 0.5;
}
if (Array.isArray(navigator.languages)) {
navigator.languages.forEach(locale => languages[locale] = 0.7);
}
if (Utils.hasText(state.locale)) {
languages[state.locale] = 1;
}
return Object.entries(languages)
.sort((a,b) => {
if (a[1] > b[1]) {
return -1;
}
else if (a[1] < b[1]) {
return 1;
}
return 0;
})
.map(([l, q]) => q >= 1 ? l : `${l};q=${q}`)
.join(',');
}
},
mutations: {
Expand Down Expand Up @@ -414,6 +446,9 @@ function getStore(config) {
state.stateQueryParameters[type].push(uid);
}
},
state(state, newState) {
state.stateQueryParameters = newState;
},
closeCollapsible(state, { type, uid }) {
const idx = state.stateQueryParameters[type].indexOf(uid);
if (idx > -1) {
Expand Down Expand Up @@ -576,9 +611,27 @@ function getStore(config) {
},
actions: {
async switchLocale(cx, locale) {
if (Array.isArray(locale)) {
const supported = prepareSupported(cx.state.supportedLocales);
for(let l of locale) {
const best = getBest(supported, l, null, true);
if (best) {
locale = best;
break;
}
}
}

if (Array.isArray(locale)) {
console.error(`None of the given languages is supported.`);
locale = cx.state.fallbackLocale;
}
if (!cx.state.supportedLocales.includes(locale)) {
console.error(`Language '${locale}' is not supported.`);
return;
if (locale === cx.state.fallbackLocale) {
return;
}
locale = cx.state.fallbackLocale;
}

// No messages in cache, load them
Expand Down Expand Up @@ -688,6 +741,15 @@ function getStore(config) {
throw new BrowserError(i18n.t('errors.invalidJsonObject'));
}
data = new STAC(response.data, url, path);
if (show) {
// If we prefer another language abort redirect to the new language
let localeLink = data.getLocaleLink(cx.state.locale);
if (localeLink) {
router.replace(cx.getters.toBrowserPath(localeLink.href));
return;
}
}

cx.commit('loaded', { url, data });

if (!cx.getters.root) {
Expand Down
11 changes: 9 additions & 2 deletions src/store/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,19 @@ export class Loading {

export async function stacRequest(cx, link) {
let opts;
let headers = {
'Accept-Language': cx.acceptedLanguages
};
Object.assign(headers, cx.state.requestHeaders);
if (Utils.isObject(link)) {
let method = typeof link.method === 'string' ? link.method.toLowerCase() : 'get';
if (Utils.isObject(link.headers)) {
Object.assign(headers, link.headers);
}
opts = {
method,
url: cx.getters.getRequestUrl(link.href),
headers: Object.assign({}, cx.state.requestHeaders, link.headers),
headers,
data: link.body
// ToDo: Support for merge property from STAC API
};
Expand All @@ -26,7 +33,7 @@ export async function stacRequest(cx, link) {
opts = {
method: 'get',
url: cx.getters.getRequestUrl(link),
headers: cx.state.requestHeaders
headers
};
}
else {
Expand Down

0 comments on commit 4502659

Please sign in to comment.