diff --git a/README.md b/README.md index e59ee2360..2fe290a99 100644 --- a/README.md +++ b/README.md @@ -75,6 +75,32 @@ Must be set to `true` if a `catalogUrl` is not given as otherwise you won't be a You can list additional domains (e.g. `example.com`) that private data is sent to, e.g. authentication data. +### 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). +The language given here must be present in `supportedLocales`. + +### fallbackLocale + +The language to use if individual phrases are not available in the default language, defaults to `en` (English). +The language given here must be present in `supportedLocales`. + +### supportedLocales + +A list of languages to show in the STAC Browser UI. +The languages given here must have a corresponding JS and JSON file in the `src/locales` folder, +e.g. provide `en` (English) for the files in `src/locales/en`. + +In CLI, please provide the languages separated by a space, e.g. `--supportedLocales en de fr it` + +Please note that only left-to-right languages have been tested. +I'd need help to test support for right-to-left languages. + ### stacLint ***experimental*** @@ -230,6 +256,7 @@ There are four options you can set in the `authConfig` object: * `key` (string): The query string parameter name or the HTTP header name respecively. * `formatter` (function|null): You can optionally specify a formatter for the query string value or HTTP header value respectively. If not given, the token is provided as provided by the user. * `description` (string|null): Optionally a description that is shown to the user. This should explain how the token can be obtained for example. CommonMark is allowed. + **Note:** You can leave the description empty in the config file and instead provide a localized string with the key `authConfig` -> `description` in the file for custom phrases (`src/locales/custom.js`). Please note that this option can only be provided through a config file and is not available via CLI. diff --git a/config.js b/config.js index 4717c27d5..e675a9b60 100644 --- a/config.js +++ b/config.js @@ -3,6 +3,16 @@ module.exports = { catalogTitle: "STAC Browser", allowExternalAccess: true, // Must be true if catalogUrl is not given allowedDomains: [], + detectLocaleFromBrowser: true, + locale: "en", + fallbackLocale: "en", + supportedLocales: [ + "en", + "en-US", + "de", + "fr-CA", + "fr-FR" + ], useTileLayerAsFallback: true, tileSourceTemplate: null, displayGeoTiffByDefault: false, diff --git a/helpers/fields_locales.js b/helpers/fields_locales.js new file mode 100644 index 000000000..f92ab0ca4 --- /dev/null +++ b/helpers/fields_locales.js @@ -0,0 +1,73 @@ +const Fields = require('@radiantearth/stac-fields/fields-normalized.json'); +const HardCodedFields = require("./fields_locales.json"); +const fs = require('fs'); + +const translatable = ["label", "explain", "unit"]; +const iterable = ["items", "properties"]; +const dest_file = "src/locales/en/fields.json"; + +function ignore(text, path, otherPath) { + if (path.endsWith(".unit")) { + return true; + } + if (path.includes(".id.") || path.includes(".name.") || path.includes(".title.") || path.includes(".description.")) { + return true; + } + if (path.includes(".href") || path.includes(".roles.")) { + return true; + } + if (path.includes(".average.") || path.includes(".minimum.") || path.includes(".maximum.") || path.includes(".stddev.")) { + return true; + } + if (path.includes("cube:") && otherPath.includes("cube:")) { + return true; + } + if (path.includes("links.type.") && otherPath.includes("assets.type.")) { + return true; + } + if (path.includes(".mgrs.")) { + return true; + } + return false; +} + +function addText(locales, text, path) { + if (locales[text] && !ignore(text, path, locales[text])) { + console.warn(`Potential conflict between '${path}' and '${locales[text]}'`); + } + locales[text] = path; +} + +function findTexts(locales, fields, path) { + for(let name in fields) { + let field = fields[name]; + if (field.alias) { + continue; + } + + translatable.forEach(key => field[key] && addText(locales, field[key], `${path}.${name}.${key}`)); + iterable.forEach(key => field[key] && findTexts(locales, field[key], `${path}.${name}.${key}`)); + if (field.mapping) { + Object.entries(field.mapping).forEach(([key, text]) => addText(locales, text, `${path}.${name}.mapping.${key}`)); + } + } +} + +function writeToFile(file, locales) { + const data = {}; + Object.keys(locales).sort().forEach(key => data[key] = key); + const json = JSON.stringify(data, null, 2); + fs.writeFileSync(file, json); +} + +function generateLocales() { + const locales = {}; + HardCodedFields.forEach(text => addText(locales, text, "hardcoded")); + const types = ["assets", "extensions", "links", "metadata"]; + types.forEach(type => findTexts(locales, Fields[type], type)); + writeToFile(dest_file, locales); +} + +console.log(`Generating fields locale file`); +generateLocales(); +console.log(`Saved fields locale file to ${dest_file}`); \ No newline at end of file diff --git a/helpers/fields_locales.json b/helpers/fields_locales.json new file mode 100644 index 000000000..0351c2f5a --- /dev/null +++ b/helpers/fields_locales.json @@ -0,0 +1,87 @@ +[ + "n/a", + "none", + + "Hashing algorithm:", + + "Until {0}", + "{0} until present", + + "8-bit integer", + "16-bit integer", + "32-bit integer", + "64-bit integer", + "unsigned 8-bit integer", + "unsigned 16-bit integer", + "unsigned 32-bit integer", + "unsigned 64-bit integer", + "16-bit float", + "32-bit float", + "64-bit float", + "16-bit complex integer", + "32-bit complex integer", + "32-bit complex float", + "64-bit complex float", + "non-standard", + + "MGRS", + "Military Grid Reference System", + "UTM Zone", + "Latitude Band", + "Square Identifier", + "Easting", + "Northing", + "MODIS Sinusoidal Tile Grid", + "Horizontal", + "Vertical", + "WRS-1", + "Worldwide Reference System 1", + "WRS-2", + "Worldwide Reference System 2", + "Path", + "Row", + "DOQ", + "Digital Orthophoto Quadrangle", + "Quadrangle", + "DOQQ", + "Digital Orthophoto Quarter Quadrangle", + "North", + "East", + "South", + "West", + "Quarter", + "Maxar ARD Tile Grid", + "Quadkey", + "EASE-DGGS", + "Level", + "Level 0 row cell", + "Level 0 column cell", + "Fraction of level {i} row cell", + "Fraction of level {i} column cell", + + "Cloud-Optimized GeoTIFF image", + "GeoTIFF image", + "TIFF image", + "JPEG 2000 image", + "PNG image", + "GIF image", + "JPEG image", + "WebP image", + "Bitmap", + "Bitmap image", + "SVG vector image", + "Comma-separated values (CSV)", + "Newline Delimited JSON", + "HTML (Website)", + "Text", + "Text document", + "Markdown document", + "PDF document", + "ZIP archive", + "GZIP archive", + "Meta Raster Format", + "Binary", + "Binary file", + "Cloud-Optimized Point Cloud (LASzip)", + "Font" +] \ No newline at end of file diff --git a/package.json b/package.json index 172e39b40..6f130febf 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "start": "vue-cli-service serve", "build": "vue-cli-service build --report", - "lint": "vue-cli-service lint" + "lint": "vue-cli-service lint", + "i18n:report": "vue-cli-service i18n:report --src \"./src/**/*.?(js|vue)\" --locales \"./src/locales/**/*.(js|json)\"", + "i18n:fields": "node helpers/fields_locales.js" }, "bugs": { "url": "https://github.com/radiantearth/stac-browser/issues" @@ -30,6 +32,7 @@ "license": "ISC", "dependencies": { "@apidevtools/json-schema-ref-parser": "^9.0.9", + "@musement/iso-duration": "^1.0.0", "@radiantearth/stac-fields": "1.0.0-beta.25", "@radiantearth/stac-migrate": "~1.2.0", "axios": "^1.2.0", @@ -38,12 +41,14 @@ "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", "urijs": "^1.19.11", "v-clipboard": "^2.2.3", "vue": "^2.6.12", + "vue-i18n": "^8.28.2", "vue-multiselect": "^2.1.6", "vue-read-more-smooth": "^0.1.8", "vue-router": "^3.2.0", @@ -64,6 +69,7 @@ "eslint-plugin-vue": "^8.7.1", "sass": "^1.26.5", "sass-loader": "^13.2.0", + "vue-cli-plugin-i18n": "~2.3.1", "vue-template-compiler": "^2.6.12" }, "browserslist": [ diff --git a/src/StacBrowser.vue b/src/StacBrowser.vue index 4633da426..0ce970211 100644 --- a/src/StacBrowser.vue +++ b/src/StacBrowser.vue @@ -1,12 +1,12 @@ @@ -25,7 +27,8 @@ \ No newline at end of file diff --git a/src/components/Description.vue b/src/components/Description.vue index 46387e7be..9411f90ae 100644 --- a/src/components/Description.vue +++ b/src/components/Description.vue @@ -1,5 +1,5 @@ + + \ No newline at end of file diff --git a/src/components/Providers.vue b/src/components/Providers.vue index 58b1edd3c..86d1633b5 100644 --- a/src/components/Providers.vue +++ b/src/components/Providers.vue @@ -1,6 +1,6 @@ diff --git a/src/components/SortButtons.vue b/src/components/SortButtons.vue index 136983948..743d284b8 100644 --- a/src/components/SortButtons.vue +++ b/src/components/SortButtons.vue @@ -1,10 +1,10 @@ diff --git a/src/components/Source.vue b/src/components/Source.vue new file mode 100644 index 000000000..49d848497 --- /dev/null +++ b/src/components/Source.vue @@ -0,0 +1,215 @@ + + + + + \ No newline at end of file diff --git a/src/components/StacFieldsMixin.js b/src/components/StacFieldsMixin.js new file mode 100644 index 000000000..91087cdb9 --- /dev/null +++ b/src/components/StacFieldsMixin.js @@ -0,0 +1,20 @@ +import { mapState } from 'vuex'; + +export default functions => { + let mixin = { + computed: { + ...mapState(['uiLanguage']) + }, + methods: {} + }; + for(let name in functions) { + let fn = functions[name]; + mixin.methods[name] = function() { + // We call uiLanguage once so that it's a dependency for the computed property + // which makes the computed property to re-render when uiLanguage changes. + this.uiLanguage; + return fn(...arguments); + }; + } + return mixin; +}; \ No newline at end of file diff --git a/src/components/StacHeader.vue b/src/components/StacHeader.vue index 3559307de..4781dcd2f 100644 --- a/src/components/StacHeader.vue +++ b/src/components/StacHeader.vue @@ -9,26 +9,28 @@ {{ title }}

- in + + + - - Go to Parent + + {{ $t('goToParent.label') }} - - Go to Collection + + {{ $t('goToCollection.label') }} - - Browse + + {{ $t('browse') }} - - Search + + {{ $t('search.title') }} - + @@ -54,11 +56,27 @@ export default { BIconLock, BIconUnlock, StacLink, - Share: () => import('../components/Share.vue') + Share: () => import('./Source.vue') }, computed: { ...mapState(['allowSelectCatalog', 'authConfig', 'authData', 'catalogUrl', 'data', 'url', 'title']), ...mapGetters(['root', 'parentLink', 'collectionLink', 'stacVersion', 'toBrowserPath']), + collectionLinkTitle() { + if (Utils.hasText(this.collectionLink.title)) { + return this.$t('goToCollection.descriptionWithTitle', this.collectionLink); + } + else { + return this.$t('goToCollection.description'); + } + }, + parentLinkTitle() { + if (Utils.hasText(this.parentLink.title)) { + return this.$t('goToParent.descriptionWithTitle', this.parentLink); + } + else { + return this.$t('goToParent.description'); + } + }, icon() { if (this.data instanceof STAC) { let icons = this.data.getIcons(); diff --git a/src/components/Tree.vue b/src/components/Tree.vue index 7a1e87c89..a28fb7e61 100644 --- a/src/components/Tree.vue +++ b/src/components/Tree.vue @@ -20,18 +20,18 @@ @@ -138,7 +138,7 @@ export default { }, title() { if (this.pagination) { - return 'more pages available for Collection'; + return this.$t('tree.moreCollectionPagesAvailable'); } return STAC.getDisplayTitle([this.item, this.stac]); }, diff --git a/src/components/Url.vue b/src/components/Url.vue index c392dd6c5..7e5d8eb7c 100644 --- a/src/components/Url.vue +++ b/src/components/Url.vue @@ -4,7 +4,7 @@ - + diff --git a/src/components/ViewButtons.vue b/src/components/ViewButtons.vue index 8abe42d8b..d2291a364 100644 --- a/src/components/ViewButtons.vue +++ b/src/components/ViewButtons.vue @@ -1,10 +1,10 @@ diff --git a/src/components/metadata/MetadataEntry.vue b/src/components/metadata/MetadataEntry.vue index b6816ccbe..9670b5df1 100644 --- a/src/components/metadata/MetadataEntry.vue +++ b/src/components/metadata/MetadataEntry.vue @@ -1,6 +1,6 @@