🔀 Merge pull request #223 from Lissy93/REFACTOR/improved-language-detection

[REFACTOR] Improved Language Detection
This commit is contained in:
Alicia Sykes 2021-09-11 02:14:54 +01:00 committed by GitHub
commit 51e21296b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 163 additions and 83 deletions

View File

@ -1,5 +1,9 @@
# Changelog
## 🎨 1.7.5 - Improved Language Detection & UI [PR #223](https://github.com/Lissy93/dashy/pull/223)
- Makes the auto language detection algo smarter
- Improves responsiveness for the language selector form
## 🌐 1.7.4 - Adds Spanish Translations [PR #222](https://github.com/Lissy93/dashy/pull/222)
- Adds Spanish language file, contributed by @lu4t

View File

@ -2,6 +2,31 @@
All app configuration is specified in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml) which is in [YAML Format](https://yaml.org/) format.
---
#### Contents
- [**`pageInfo`**](#pageinfo) - Header text, footer, title, navigation, etc
- [`navLinks`](#pageinfonavlinks-optional) - Navigation bar items and links
- [**`appConfig`**](#appconfig-optional) - Main application settings
- [`webSearch`](#appconfigwebsearch-optional) - Configure web search engine options
- [`hideComponents`](#appconfighidecomponents-optional) - Show/ hide page components
- [`auth`](#appconfigauth-optional) - Built-in authentication setup
- [`users`](#appconfigauthusers-optional) - Setup for simple auth
- [`keycloak`](#appconfigauthkeycloak-optional) - Auth using Keycloak
- [**`sections`**](#section) - List of sections
- [`displayData`](#sectiondisplaydata-optional) - Section display settings
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for a section
- [`items`](#sectionitem) - List of items
- [`icon`](#sectionicon-and-sectionitemicon) - Icon for an item
- [**Notes**](#notes)
- [About YAML](#about-yaml)
- [Config Saving Methods](#config-saving-methods)
- [Preventing Changes](#preventing-changes-being-written-to-disk)
- [Example](#example)
---
Tips:
- You may find it helpful to look at some sample config files to get you started, a collection of which can be found [here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
- You can check that your config file fits the schema, by running `yarn validate-config`
@ -10,16 +35,7 @@ Tips:
- The config can also be modified directly through the UI, validated and written to the conf.yml file.
- All fields are optional, unless otherwise stated.
### About YAML
If you're new to YAML, it's pretty straight-forward. The format is exactly the same as that of JSON, but instead of using curly braces, structure is denoted using whitespace. This [quick guide](https://linuxhandbook.com/yaml-basics/) should get you up to speed in a few minutes, for more advanced topics take a look at this [Wikipedia article](https://en.wikipedia.org/wiki/YAML).
### Config Saving Methods
When updating the config through the JSON editor in the UI, you have two save options: **Local** or **Write to Disk**.
- Changes saved locally will only be applied to the current user through the browser, and will not apply to other instances - you either need to use the cloud sync feature, or manually update the conf.yml file.
- On the other-hand, if you choose to write changes to disk, then your main `conf.yml` file will be updated, and changes will be applied to all users, and visible across all devices. For this functionality to work, you must be running Dashy with using the Docker container, or the Node server. A backup of your current configuration will also be saved in the same directory.
### Preventing Changes being Written to Disk
To disallow any changes from being written to disk via the UI config editor, set `appConfig.allowConfigEdit: false`. If you are using users, and have setup `auth` within Dashy, then only users with `type: admin` will be able to write config changes to disk.
---
### Top-Level Fields
@ -178,7 +194,7 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**Field** | **Type** | **Required**| **Description**
--- | --- | --- | ---
**`sortBy`** | `string` | _Optional_ | The sort order for items within the current section. By default items are displayed in the order in which they are listed in within the config. The following sort options are supported: `most-used` (most opened apps first), `last-used` (the most recently used apps), `alphabetical`, `reverse-alphabetical` and `default`
**`sortBy`** | `string` | _Optional_ | The sort order for items within the current section. By default items are displayed in the order in which they are listed in within the config. The following sort options are supported: `most-used` (most opened apps first), `last-used` (the most recently used apps), `alphabetical`, `reverse-alphabetical`, `random` and `default`
**`collapsed`** | `boolean` | _Optional_ | If true, the section will be collapsed initially, and will need to be clicked to open. Useful for less regularly used, or very long sections. Defaults to `false`
**`rows`** | `number` | _Optional_ | Height of the section, specified as the number of rows it should span vertically, e.g. `2`. Defaults to `1`. Max is `5`.
**`cols`** | `number` | _Optional_ | Width of the section, specified as the number of columns the section should span horizontally, e.g. `2`. Defaults to `1`. Max is `5`.
@ -202,6 +218,21 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)**
**[⬆️ Back to Top](#configuring)**
---
## Notes
### About YAML
If you're new to YAML, it's pretty straight-forward. The format is exactly the same as that of JSON, but instead of using curly braces, structure is denoted using whitespace. This [quick guide](https://linuxhandbook.com/yaml-basics/) should get you up to speed in a few minutes, for more advanced topics take a look at this [Wikipedia article](https://en.wikipedia.org/wiki/YAML).
### Config Saving Methods
When updating the config through the JSON editor in the UI, you have two save options: **Local** or **Write to Disk**.
- Changes saved locally will only be applied to the current user through the browser, and will not apply to other instances - you either need to use the cloud sync feature, or manually update the conf.yml file.
- On the other-hand, if you choose to write changes to disk, then your main `conf.yml` file will be updated, and changes will be applied to all users, and visible across all devices. For this functionality to work, you must be running Dashy with using the Docker container, or the Node server. A backup of your current configuration will also be saved in the same directory.
### Preventing Changes being Written to Disk
To disallow any changes from being written to disk via the UI config editor, set `appConfig.allowConfigEdit: false`. If you are using users, and have setup `auth` within Dashy, then only users with `type: admin` will be able to write config changes to disk.
### Example
```yaml

View File

@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.7.4",
"version": "1.7.5",
"license": "MIT",
"main": "server",
"scripts": {

View File

@ -1,9 +1,9 @@
<template>
<div id="dashy">
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash()" />
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
<Header :pageInfo="pageInfo" />
<router-view />
<Footer :text="getFooterText()" v-if="visibleComponents.footer" />
<Footer :text="footerText" v-if="visibleComponents.footer" />
</div>
</template>
<script>
@ -14,6 +14,7 @@ import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
import { componentVisibility } from '@/utils/ConfigHelpers';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { welcomeMsg } from '@/utils/CoolConsole';
import ErrorHandler from '@/utils/ErrorHandler';
import {
localStorageKeys,
splashScreenTime,
@ -45,53 +46,64 @@ export default {
visibleComponents,
};
},
methods: {
computed: {
/* If the user has specified custom text for footer - get it */
getFooterText() {
if (this.pageInfo && this.pageInfo.footerText) {
return this.pageInfo.footerText;
}
return '';
},
/* Injects the users custom CSS as a style tag */
injectCustomStyles(usersCss) {
const style = document.createElement('style');
style.textContent = usersCss;
document.head.append(style);
footerText() {
return this.pageInfo && this.pageInfo.footerText ? this.pageInfo.footerText : '';
},
/* Determine if splash screen should be shown */
shouldShowSplash() {
return (this.visibleComponents || defaultVisibleComponents).splashScreen
|| !localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
},
},
methods: {
/* Injects the users custom CSS as a style tag */
injectCustomStyles(usersCss) {
const style = document.createElement('style');
style.textContent = usersCss;
document.head.append(style);
},
/* Hide splash screen, either after 2 seconds, or immediately based on user preference */
hideSplash() {
if (this.shouldShowSplash()) {
if (this.shouldShowSplash) {
setTimeout(() => { this.isLoading = false; }, splashScreenTime || 1500);
} else {
this.isLoading = false;
}
},
/* Checks local storage, then appConfig, and if a custom language is specified, its applied */
applyLanguage() {
let language = defaultLanguage; // Language to apply
const availibleLocales = this.$i18n.availableLocales; // All available locales
// If user has specified a language, locally or in config, then check and apply it
/* Auto-detects users language from browser/ os, when not specified */
autoDetectLanguage(availibleLocales) {
const isLangSupported = (languageList, userLang) => languageList
.map(lang => lang.toLowerCase()).find((lang) => lang === userLang.toLowerCase());
const usersBorwserLang1 = window.navigator.language || ''; // e.g. en-GB or or ''
const usersBorwserLang2 = usersBorwserLang1.split('-')[0]; // e.g. en or undefined
const usersSpairLangs = window.navigator.languages; // e.g [en, en-GB, en-US]
return isLangSupported(availibleLocales, usersBorwserLang1)
|| isLangSupported(availibleLocales, usersBorwserLang2)
|| usersSpairLangs.find((spair) => isLangSupported(availibleLocales, spair))
|| defaultLanguage;
},
/* Get users language, if not available then call auto-detect */
getLanguage() {
const availibleLocales = this.$i18n.availableLocales; // All available locales
const usersLang = localStorage[localStorageKeys.LANGUAGE] || this.appConfig.language;
if (usersLang && availibleLocales.includes(usersLang)) {
language = usersLang;
} else {
// Otherwise, attempt to apply language automatically, based on their system language
const usersBorwserLang1 = window.navigator.language || ''; // e.g. en-GB or or ''
const usersBorwserLang2 = usersBorwserLang1.split('-')[0]; // e.g. en or undefined
if (availibleLocales.includes(usersBorwserLang1)) {
language = usersBorwserLang1;
} else if (availibleLocales.includes(usersBorwserLang2)) {
language = usersBorwserLang2;
if (usersLang) {
if (availibleLocales.includes(usersLang)) {
return usersLang;
} else {
ErrorHandler(`Unsupported Language: '${usersLang}'`);
}
}
// Apply Language
return this.autoDetectLanguage(availibleLocales);
},
/* Fetch or detect users language, then apply it */
applyLanguage() {
const language = this.getLanguage();
this.$i18n.locale = language;
document.getElementsByTagName('html')[0].setAttribute('lang', language);
},

View File

@ -290,10 +290,10 @@ div.code-container {
display: flex;
flex-direction: column;
background: var(--config-settings-background);
height: calc(100% - 4rem);
height: calc(100% - 2rem);
width: fit-content;
margin: 0 auto;
padding: 2rem 1rem;
padding: 2rem 1rem 0;
h2 {
margin: 0 auto 1rem auto;
color: var(--config-settings-color);

View File

@ -17,7 +17,7 @@
<!-- Modal for manually changing locale -->
<modal :name="modalNames.LANG_SWITCHER" classes="dashy-modal"
:resizable="true" width="35%" height="35%">
:resizable="true" width="35%" height="50%">
<LanguageSwitcher />
</modal>

View File

@ -5,16 +5,18 @@
<v-select
v-model="language"
:selectOnTab="true"
:options="availibleLanguages"
:options="languageList"
class="language-dropdown"
label="name"
:input="setLangLocally()"
label="friendlyName"
:input="applyLanguageLocally()"
/>
<Button class="save-button" :click="saveLanguage" :disallow="!language">
{{ $t('language-switcher.save-button') }}
<SaveConfigIcon />
</Button>
<p v-if="language">{{ language.flag }} {{ language.name }}</p>
<p v-if="language" class="current-lang">
🌐 {{ language.flag }} {{ language.name }}
</p>
<p v-if="$i18n.availableLocales.length <= 1" class="sad-times">
There are not currently any additional languages supported,
but stay tuned as more are on their way!
@ -24,8 +26,9 @@
<script>
import Button from '@/components/FormElements/Button';
import { languages } from '@/utils/languages';
import SaveConfigIcon from '@/assets/interface-icons/save-config.svg';
import ErrorHandler from '@/utils/ErrorHandler';
import { languages } from '@/utils/languages';
import { localStorageKeys, modalNames } from '@/utils/defaults';
export default {
@ -37,38 +40,54 @@ export default {
},
data() {
return {
availibleLanguages: languages,
language: '',
modalName: modalNames.LANG_SWITCHER,
language: this.getCurrentLanguage(), // The currently selected language
modalName: modalNames.LANG_SWITCHER, // Key for modal
};
},
computed: {
/* Return the array of language objects, plus a friends name */
languageList: () => languages.map((lang) => {
const newLang = lang;
newLang.friendlyName = `${lang.flag} ${lang.name}`;
return newLang;
}),
},
methods: {
/* Save language to local storage, show success msg and close modal */
saveLanguage() {
const selectedLanguage = this.language;
if (this.checkLocale(selectedLanguage)) {
localStorage.setItem(localStorageKeys.LANGUAGE, selectedLanguage.code);
this.setLangLocally();
const successMsg = `${selectedLanguage.flag} `
+ `${this.$t('language-switcher.success-msg')} ${selectedLanguage.name}`;
this.$toasted.show(successMsg, { className: 'toast-success' });
this.$modal.hide(this.modalName);
} else {
this.$toasted.show('Unable to update language', { className: 'toast-error' });
}
},
/* Check language is supported, before saving */
/* Check if language is supported */
checkLocale(selectedLanguage) {
if (!selectedLanguage || !selectedLanguage.code) return false;
const i18nLocales = this.$i18n.availableLocales;
return i18nLocales.includes(selectedLanguage.code);
},
/* Apply language locally */
setLangLocally() {
applyLanguageLocally() {
if (this.language && this.language.code) {
this.$i18n.locale = this.language.code;
} else {
ErrorHandler('Error applying language, it\'s config may be missing of incomplete');
}
},
/* Save language to local storage, show success msg and close modal */
saveLanguage() {
const selectedLanguage = this.language;
if (this.checkLocale(selectedLanguage)) {
localStorage.setItem(localStorageKeys.LANGUAGE, selectedLanguage.code);
this.applyLanguageLocally();
const successMsg = `${selectedLanguage.flag} `
+ `${this.$t('language-switcher.success-msg')} ${selectedLanguage.name}`;
this.$toasted.show(successMsg, { className: 'toast-success' });
this.$modal.hide(this.modalName);
} else {
this.$toasted.show('Unable to update language', { className: 'toast-error' });
ErrorHandler('Unable to apply language');
}
},
/* Gets the users current language from local storage */
getCurrentLanguage() {
const getLanguageFromIso = (iso) => languages.find((lang) => lang.code === iso);
const current = localStorage[localStorageKeys.LANGUAGE] || this.config.appConfig.language;
return getLanguageFromIso(current);
},
},
};
</script>
@ -81,29 +100,43 @@ export default {
padding: 1rem;
background: var(--config-settings-background);
color: var(--config-settings-color);
h3.title {
text-align: center;
}
p.intro {
margin: 0;
}
button.save-button {
margin: 0 auto;
width: 100%;
}
p.sad-times {
color: var(--warning);
text-align: center;
}
.language-dropdown {
p.current-lang {
color: var(--success);
opacity: var(--dimming-factor);
text-align: center;
position: absolute;
margin: 1rem auto;
div.vs__dropdown-toggle {
padding: 0.2rem 0;
}
cursor: default;
width: 100%;
bottom: 0;
}
}
</style>
<style lang="scss">
.language-dropdown {
margin: 1rem auto;
div.vs__dropdown-toggle {
padding: 0.2rem 0;
}
div, input {
cursor: pointer;
}
}

View File

@ -11,7 +11,7 @@ import {
pageInfo as defaultPageInfo,
iconSize as defaultIconSize,
layout as defaultLayout,
language as defaultLanguage,
// language as defaultLanguage,
} from '@/utils/defaults';
import conf from '../../public/conf.yml';
@ -38,8 +38,6 @@ export default class ConfigAccumulator {
|| appConfigFile.layout || defaultLayout;
usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE]
|| appConfigFile.iconSize || defaultIconSize;
usersAppConfig.language = localStorage[localStorageKeys.LANGUAGE]
|| appConfigFile.language || defaultLanguage;
// Don't let users modify users locally
if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth;
// All done, return final appConfig object

View File

@ -4,11 +4,13 @@ import { warningMsg } from '@/utils/CoolConsole';
/**
* Function called when an error happens
* If you wish to use an error logging service, put code for it here
* Will call to function which prints helpful message to console
* If error reporting is enabled, will also log the message to Sentry
* If you wish to use your own error logging service, put code for it here
*/
const ErrorHandler = function handler(msg) {
warningMsg(msg);
Sentry.captureMessage(msg);
Sentry.captureMessage(`[USER-WARN] ${msg || 'Uncaptured Message'}`);
};
export default ErrorHandler;