Skip to content

Commit

Permalink
🔀 Merge pull request #223 from Lissy93/REFACTOR/improved-language-det…
Browse files Browse the repository at this point in the history
…ection

[REFACTOR] Improved Language Detection
  • Loading branch information
Lissy93 authored Sep 11, 2021
2 parents d6b7b9a + 5a8e24b commit 51e2129
Show file tree
Hide file tree
Showing 9 changed files with 158 additions and 78 deletions.
4 changes: 4 additions & 0 deletions .github/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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

Expand Down
53 changes: 42 additions & 11 deletions docs/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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

Expand Down Expand Up @@ -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`.
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Dashy",
"version": "1.7.4",
"version": "1.7.5",
"license": "MIT",
"main": "server",
"scripts": {
Expand Down
72 changes: 42 additions & 30 deletions src/App.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand All @@ -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,
Expand Down Expand Up @@ -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 '';
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);
},
/* Determine if splash screen should be shown */
shouldShowSplash() {
return (this.visibleComponents || defaultVisibleComponents).splashScreen
|| !localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
},
/* 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);
},
Expand Down
4 changes: 2 additions & 2 deletions src/components/Configuration/ConfigContainer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/components/Settings/ConfigLauncher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down
89 changes: 61 additions & 28 deletions src/components/Settings/LanguageSwitcher.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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 {
Expand All @@ -37,37 +40,53 @@ 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: {
/* 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 */
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.setLangLocally();
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');
}
},
/* Check language is supported, before saving */
checkLocale(selectedLanguage) {
if (!selectedLanguage || !selectedLanguage.code) return false;
const i18nLocales = this.$i18n.availableLocales;
return i18nLocales.includes(selectedLanguage.code);
},
/* Apply language locally */
setLangLocally() {
if (this.language && this.language.code) {
this.$i18n.locale = this.language.code;
}
/* 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);
},
},
};
Expand All @@ -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;
}
}
Expand Down
Loading

0 comments on commit 51e2129

Please sign in to comment.