mirror of
https://github.com/CorentinTh/it-tools.git
synced 2025-04-24 08:46:15 -04:00
feat(i18n): language selector (#710)
This commit is contained in:
parent
58de8970f5
commit
e86fd96ae3
12 changed files with 182 additions and 46 deletions
|
@ -11,6 +11,13 @@ const styleStore = useStyleStore();
|
|||
|
||||
const theme = computed(() => (styleStore.isDarkTheme ? darkTheme : null));
|
||||
const themeOverrides = computed(() => (styleStore.isDarkTheme ? darkThemeOverrides : lightThemeOverrides));
|
||||
|
||||
const { locale } = useI18n();
|
||||
|
||||
syncRef(
|
||||
locale,
|
||||
useStorage('locale', locale),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
@ -36,7 +36,7 @@ const menuOptions = computed(() =>
|
|||
tools: components.map(tool => ({
|
||||
label: makeLabel(tool),
|
||||
icon: makeIcon(tool),
|
||||
key: tool.name,
|
||||
key: tool.path,
|
||||
})),
|
||||
})),
|
||||
);
|
||||
|
@ -62,7 +62,7 @@ const themeVars = useThemeVars();
|
|||
|
||||
<n-menu
|
||||
class="menu"
|
||||
:value="route.name as string"
|
||||
:value="route.path"
|
||||
:collapsed-width="64"
|
||||
:collapsed-icon-size="22"
|
||||
:options="tools"
|
||||
|
|
|
@ -4,10 +4,10 @@ import { NIcon, useThemeVars } from 'naive-ui';
|
|||
import { RouterLink } from 'vue-router';
|
||||
import { Heart, Home2, Menu2 } from '@vicons/tabler';
|
||||
|
||||
import { storeToRefs } from 'pinia';
|
||||
import HeroGradient from '../assets/hero-gradient.svg?component';
|
||||
import MenuLayout from '../components/MenuLayout.vue';
|
||||
import NavbarButtons from '../components/NavbarButtons.vue';
|
||||
import { toolsByCategory } from '@/tools';
|
||||
import { useStyleStore } from '@/stores/style.store';
|
||||
import { config } from '@/config';
|
||||
import type { ToolCategory } from '@/tools/tools.types';
|
||||
|
@ -21,12 +21,14 @@ const version = config.app.version;
|
|||
const commitSha = config.app.lastCommitSha.slice(0, 7);
|
||||
|
||||
const { tracker } = useTracker();
|
||||
const { t } = useI18n();
|
||||
|
||||
const toolStore = useToolStore();
|
||||
const { favoriteTools, toolsByCategory } = storeToRefs(toolStore);
|
||||
|
||||
const tools = computed<ToolCategory[]>(() => [
|
||||
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
|
||||
...toolsByCategory,
|
||||
...(favoriteTools.value.length > 0 ? [{ name: t('tools.categories.favorite-tools'), components: favoriteTools.value }] : []),
|
||||
...toolsByCategory.value,
|
||||
]);
|
||||
</script>
|
||||
|
||||
|
@ -47,8 +49,12 @@ const tools = computed<ToolCategory[]>(() => [
|
|||
</RouterLink>
|
||||
|
||||
<div class="sider-content">
|
||||
<div v-if="styleStore.isSmallScreen" flex justify-center>
|
||||
<NavbarButtons />
|
||||
<div v-if="styleStore.isSmallScreen" flex flex-col items-center>
|
||||
<locale-selector w="90%" />
|
||||
|
||||
<div flex justify-center>
|
||||
<NavbarButtons />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CollapsibleToolMenu :tools-by-category="tools" />
|
||||
|
@ -108,6 +114,8 @@ const tools = computed<ToolCategory[]>(() => [
|
|||
|
||||
<command-palette />
|
||||
|
||||
<locale-selector v-if="!styleStore.isSmallScreen" />
|
||||
|
||||
<div>
|
||||
<NavbarButtons v-if="!styleStore.isSmallScreen" />
|
||||
</div>
|
||||
|
|
|
@ -116,7 +116,7 @@ function activateOption(option: PaletteOption) {
|
|||
<span flex items-center gap-3 op-40>
|
||||
|
||||
<icon-mdi-search />
|
||||
Search...
|
||||
{{ $t('search.label') }}
|
||||
|
||||
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
|
||||
{{ isMac ? 'Cmd' : 'Ctrl' }} + K
|
||||
|
|
28
src/modules/i18n/components/locale-selector.vue
Normal file
28
src/modules/i18n/components/locale-selector.vue
Normal file
|
@ -0,0 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
const { availableLocales, locale } = useI18n();
|
||||
|
||||
const localesLong: Record<string, string> = {
|
||||
en: 'English',
|
||||
es: 'Español',
|
||||
fr: 'Français',
|
||||
pt: 'Português',
|
||||
ru: 'Русский',
|
||||
zh: '中文',
|
||||
};
|
||||
|
||||
const localeOptions = computed(() =>
|
||||
availableLocales.map(locale => ({
|
||||
label: localesLong[locale] ?? locale,
|
||||
value: locale,
|
||||
})),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<c-select
|
||||
v-model:value="locale"
|
||||
:options="localeOptions"
|
||||
placeholder="Select a language"
|
||||
w-100px
|
||||
/>
|
||||
</template>
|
|
@ -31,7 +31,8 @@ const { t } = useI18n();
|
|||
rel="noopener"
|
||||
target="_blank"
|
||||
:aria-label="$t('home.follow.twitterAccount')"
|
||||
>Twitter</a>{{ $t('home.follow.thankYou') }}
|
||||
>Twitter</a>.
|
||||
{{ $t('home.follow.thankYou') }}
|
||||
<n-icon :component="Heart" />
|
||||
</ColoredCard>
|
||||
</n-gi>
|
||||
|
|
|
@ -1,44 +1,57 @@
|
|||
import { type MaybeRef, get, useStorage } from '@vueuse/core';
|
||||
import { defineStore } from 'pinia';
|
||||
import type { Ref } from 'vue';
|
||||
import type { Tool, ToolWithCategory } from './tools.types';
|
||||
import _ from 'lodash';
|
||||
import type { Tool, ToolCategory, ToolWithCategory } from './tools.types';
|
||||
import { toolsWithCategory } from './index';
|
||||
|
||||
export const useToolStore = defineStore('tools', {
|
||||
state: () => ({
|
||||
favoriteToolsName: useStorage('favoriteToolsName', []) as Ref<string[]>,
|
||||
}),
|
||||
getters: {
|
||||
favoriteTools(state) {
|
||||
return state.favoriteToolsName
|
||||
.map(favoriteName => toolsWithCategory.find(({ name }) => name === favoriteName))
|
||||
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
|
||||
},
|
||||
export const useToolStore = defineStore('tools', () => {
|
||||
const favoriteToolsName = useStorage('favoriteToolsName', []) as Ref<string[]>;
|
||||
const { t } = useI18n();
|
||||
|
||||
notFavoriteTools(state): ToolWithCategory[] {
|
||||
return toolsWithCategory.filter(tool => !state.favoriteToolsName.includes(tool.name));
|
||||
},
|
||||
const tools = computed<ToolWithCategory[]>(() => toolsWithCategory.map((tool) => {
|
||||
const toolI18nKey = tool.path.replace(/\//g, '');
|
||||
|
||||
tools(): ToolWithCategory[] {
|
||||
return toolsWithCategory;
|
||||
},
|
||||
return ({
|
||||
...tool,
|
||||
name: t(`tools.${toolI18nKey}.title`, tool.name),
|
||||
description: t(`tools.${toolI18nKey}.description`, tool.description),
|
||||
category: t(`tools.categories.${tool.category.toLowerCase()}`, tool.category),
|
||||
});
|
||||
}));
|
||||
|
||||
newTools(): ToolWithCategory[] {
|
||||
return this.tools.filter(({ isNew }) => isNew);
|
||||
},
|
||||
},
|
||||
const toolsByCategory = computed<ToolCategory[]>(() => {
|
||||
return _.chain(tools.value)
|
||||
.groupBy('category')
|
||||
.map((components, name) => ({
|
||||
name,
|
||||
components,
|
||||
}))
|
||||
.value();
|
||||
});
|
||||
|
||||
const favoriteTools = computed(() => {
|
||||
return favoriteToolsName.value
|
||||
.map(favoriteName => tools.value.find(({ name }) => name === favoriteName))
|
||||
.filter(Boolean) as ToolWithCategory[]; // cast because .filter(Boolean) does not remove undefined from type
|
||||
});
|
||||
|
||||
return {
|
||||
tools,
|
||||
favoriteTools,
|
||||
toolsByCategory,
|
||||
newTools: computed(() => tools.value.filter(({ isNew }) => isNew)),
|
||||
|
||||
actions: {
|
||||
addToolToFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
this.favoriteToolsName.push(get(tool).name);
|
||||
favoriteToolsName.value.push(get(tool).name);
|
||||
},
|
||||
|
||||
removeToolFromFavorites({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
this.favoriteToolsName = this.favoriteToolsName.filter(name => get(tool).name !== name);
|
||||
favoriteToolsName.value = favoriteToolsName.value.filter(name => get(tool).name !== name);
|
||||
},
|
||||
|
||||
isToolFavorite({ tool }: { tool: MaybeRef<Tool> }) {
|
||||
return this.favoriteToolsName.includes(get(tool).name);
|
||||
return favoriteToolsName.value.includes(get(tool).name);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -33,4 +33,19 @@ const value = ref('');
|
|||
<c-select label="Label" label-position="left" label-align="left" mb-2 label-width="200px" />
|
||||
<c-select label="Label" label-position="left" label-align="center" mb-2 label-width="200px" />
|
||||
<c-select label="Label" label-position="left" label-align="right" mb-2 label-width="200px" />
|
||||
|
||||
<h2>Custom displayed value</h2>
|
||||
<c-select v-model:value="value" :options="optionsA" mb-2>
|
||||
<template #displayed-value>
|
||||
<span class="font-bold lh-normal">Hello</span>
|
||||
</template>
|
||||
</c-select>
|
||||
|
||||
<c-select v-model:value="value" :options="optionsA">
|
||||
<template #displayed-value>
|
||||
<span lh-normal>
|
||||
<icon-mdi-translate />
|
||||
</span>
|
||||
</template>
|
||||
</c-select>
|
||||
</template>
|
||||
|
|
|
@ -150,13 +150,15 @@ function onSearchInput() {
|
|||
@keydown="handleKeydown"
|
||||
>
|
||||
<div flex-1 truncate>
|
||||
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
|
||||
<span v-else-if="selectedOption" lh-normal>
|
||||
{{ selectedOption.label }}
|
||||
</span>
|
||||
<span v-else class="placeholder" lh-normal>
|
||||
{{ placeholder ?? 'Select an option' }}
|
||||
</span>
|
||||
<slot name="displayed-value">
|
||||
<input v-if="searchable && isOpen" ref="searchInputRef" v-model="searchQuery" type="text" placeholder="Search..." class="search-input" w-full lh-normal color-current @input="onSearchInput">
|
||||
<span v-else-if="selectedOption" lh-normal>
|
||||
{{ selectedOption.label }}
|
||||
</span>
|
||||
<span v-else class="placeholder" lh-normal>
|
||||
{{ placeholder ?? 'Select an option' }}
|
||||
</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<icon-mdi-chevron-down class="chevron" />
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue