i18n added language selector

Added language selector and some translations.
This commit is contained in:
José Miguel Manzano 2023-06-20 18:37:16 +02:00
parent bcb98b359c
commit fcf79cc248
11 changed files with 110 additions and 28 deletions

View file

@ -1,3 +1,26 @@
home: home:
commandPalette:
search: Search...
placeholder: Type to search a tool or a command...
commandPaletteStore:
actions: Actions
external: External
tools: Tools
categories: categories:
newestTools: "Newest tools" newestTools: Newest tools
title: IT Tools - Handy online tools for developers
allTools: All the tools
favoriteTools: Your favorite tools
thanks: '! Thank you'
giveUsAStarOn: Give us a star on
orFollowUsOn: or follow us on
youLikeItTools: You like it-tools?
tools:
xmlFormat:
description: Prettify your XML string to a human friendly readable format.
indentSize: 'Indent size:'
collapseContent: 'Collapse content:'
providedXmlIsNotValid: Provided XML is not valid.
inputLabel: Your XML
outputLabel: Formatted XML from your XML
placeHolder: Paste your XML here...

26
locales/es.yml Normal file
View file

@ -0,0 +1,26 @@
home:
commandPalette:
search: Buscar...
placeholder: Escribe para buscar una herramienta o comando...
commandPaletteStore:
actions: Acciones
external: Externa
tools: Herramientas
categories:
newestTools: Herramientas Nuevas
title: 'IT Tools - Prácticas herramientas en línea para desarrolladores.'
allTools: Todas las herramientas
favoriteTools: Tus herramientas favorias
thanks: '! Thank you'
giveUsAStarOn: Give us a star on
orFollowUsOn: or follow us on
youLikeItTools: You like it-tools?
tools:
xmlFormat:
description: Embellece tu XML a un formato XML más amigable.
indentSize: 'Tamaño de indentación:'
collapseContent: 'Colapsar contenido:'
providedXmlIsNotValid: El XML proporcionado no es válido.
inputLabel: Tu XML
outputLabel: XML Formateado desde tu XML
placeHolder: Pega tu XML aquí...

View file

@ -8,6 +8,8 @@ const props = defineProps<{ tool: Tool & { category: string } }>();
const { tool } = toRefs(props); const { tool } = toRefs(props);
const theme = useThemeVars(); const theme = useThemeVars();
const { t } = useI18n();
const appTheme = useAppTheme(); const appTheme = useAppTheme();
</script> </script>
@ -38,7 +40,7 @@ const appTheme = useAppTheme();
<div class="description"> <div class="description">
<n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px"> <n-ellipsis :line-clamp="2" :tooltip="false" style="min-height: 44.78px">
{{ tool.description }} {{ t(tool.description) }}
<br>&nbsp; <br>&nbsp;
</n-ellipsis> </n-ellipsis>
</div> </div>

View file

@ -7,6 +7,7 @@ import { Heart, Home2, Menu2 } from '@vicons/tabler';
import HeroGradient from '../assets/hero-gradient.svg?component'; import HeroGradient from '../assets/hero-gradient.svg?component';
import MenuLayout from '../components/MenuLayout.vue'; import MenuLayout from '../components/MenuLayout.vue';
import NavbarButtons from '../components/NavbarButtons.vue'; import NavbarButtons from '../components/NavbarButtons.vue';
import { availableLocales, loadLanguageAsync } from '../plugins/i18n.plugin';
import { toolsByCategory } from '@/tools'; import { toolsByCategory } from '@/tools';
import { useStyleStore } from '@/stores/style.store'; import { useStyleStore } from '@/stores/style.store';
import { config } from '@/config'; import { config } from '@/config';
@ -24,10 +25,19 @@ const { tracker } = useTracker();
const toolStore = useToolStore(); const toolStore = useToolStore();
const currentLang = useStorage('application:selected-language', 'en');
loadLanguageAsync(currentLang.value);
const tools = computed<ToolCategory[]>(() => [ const tools = computed<ToolCategory[]>(() => [
...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []), ...(toolStore.favoriteTools.length > 0 ? [{ name: 'Your favorite tools', components: toolStore.favoriteTools }] : []),
...toolsByCategory, ...toolsByCategory,
]); ]);
function onChange(event: any) {
currentLang.value = event;
loadLanguageAsync(event);
}
</script> </script>
<template> <template>
@ -106,6 +116,15 @@ const tools = computed<ToolCategory[]>(() => [
<command-palette mx-2 /> <command-palette mx-2 />
<n-select
class="i18n"
:model:value="currentLang"
:options="availableLocales.map(lang => ({ value: lang, label: lang }))"
placeholder="Select language"
:on-update-value="onChange"
:default-value="currentLang"
/>
<NavbarButtons v-if="!styleStore.isSmallScreen" /> <NavbarButtons v-if="!styleStore.isSmallScreen" />
<n-tooltip trigger="hover"> <n-tooltip trigger="hover">
@ -223,4 +242,7 @@ const tools = computed<ToolCategory[]>(() => [
flex-grow: 1; flex-grow: 1;
} }
} }
.i18n{
width: 75px;
}
</style> </style>

View file

@ -9,6 +9,7 @@ import type { Tool } from '@/tools/tools.types';
const route = useRoute(); const route = useRoute();
const { t } = useI18n();
const head = computed<HeadObject>(() => ({ const head = computed<HeadObject>(() => ({
title: `${route.meta.name} - IT Tools`, title: `${route.meta.name} - IT Tools`,
meta: [ meta: [
@ -42,7 +43,7 @@ useHead(head);
<div class="separator" /> <div class="separator" />
<div class="description"> <div class="description">
{{ route.meta.description }} {{ t(route.meta.description) }}
</div> </div>
</div> </div>
</div> </div>

View file

@ -18,7 +18,7 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
...tool, ...tool,
to: tool.path, to: tool.path,
toolCategory: tool.category, toolCategory: tool.category,
category: 'Tools', category: 'home.commandPaletteStore.tools',
})); }));
const searchOptions: PaletteOption[] = [ const searchOptions: PaletteOption[] = [
@ -28,13 +28,13 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
description: 'Toggle dark mode on or off.', description: 'Toggle dark mode on or off.',
action: () => styleStore.toggleDark(), action: () => styleStore.toggleDark(),
icon: SunIcon, icon: SunIcon,
category: 'Actions', category: 'home.commandPaletteStore.actions',
keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'], keywords: ['dark', 'theme', 'toggle', 'mode', 'light', 'system'],
}, },
{ {
name: 'Github repository', name: 'Github repository',
href: 'https://github.com/CorentinTh/it-tools', href: 'https://github.com/CorentinTh/it-tools',
category: 'External', category: 'home.commandPaletteStore.external',
description: 'View the source code of it-tools on Github.', description: 'View the source code of it-tools on Github.',
keywords: ['github', 'repo', 'repository', 'source', 'code'], keywords: ['github', 'repo', 'repository', 'source', 'code'],
icon: GithubIcon, icon: GithubIcon,
@ -43,7 +43,7 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
name: 'Report a bug or an issue', name: 'Report a bug or an issue',
description: 'Report a bug or an issue to help improve it-tools.', description: 'Report a bug or an issue to help improve it-tools.',
href: 'https://github.com/CorentinTh/it-tools/issues/new/choose', href: 'https://github.com/CorentinTh/it-tools/issues/new/choose',
category: 'Actions', category: 'home.commandPaletteStore.actions',
keywords: ['report', 'issue', 'bug', 'problem', 'error'], keywords: ['report', 'issue', 'bug', 'problem', 'error'],
icon: BugIcon, icon: BugIcon,
}, },
@ -59,7 +59,11 @@ export const useCommandPaletteStore = defineStore('command-palette', () => {
}); });
const filteredSearchResult = computed(() => const filteredSearchResult = computed(() =>
_.chain(searchResult.value).groupBy('category').mapValues(categoryOptions => _.take(categoryOptions, 5)).value()); _.chain(searchResult.value)
.groupBy('category')
.mapValues(categoryOptions => _.take(categoryOptions, 5))
.value(),
);
return { return {
filteredSearchResult, filteredSearchResult,

View file

@ -9,6 +9,8 @@ const inputRef = ref();
const router = useRouter(); const router = useRouter();
const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac')); const isMac = computed(() => window.navigator.userAgent.toLowerCase().includes('mac'));
const { t } = useI18n();
const commandPaletteStore = useCommandPaletteStore(); const commandPaletteStore = useCommandPaletteStore();
const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore); const { searchPrompt, filteredSearchResult } = storeToRefs(commandPaletteStore);
@ -99,7 +101,7 @@ function activateOption(option: PaletteOption) {
<span flex items-center gap-3 op-40> <span flex items-center gap-3 op-40>
<icon-mdi-search /> <icon-mdi-search />
Search... {{ t('home.commandPalette.search') }}
<span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline> <span hidden flex-1 border border-current border-op-40 rounded border-solid px-5px py-3px sm:inline>
{{ isMac ? 'Cmd' : 'Ctrl' }}&nbsp;+&nbsp;K {{ isMac ? 'Cmd' : 'Ctrl' }}&nbsp;+&nbsp;K
@ -108,11 +110,11 @@ function activateOption(option: PaletteOption) {
</c-button> </c-button>
<c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown"> <c-modal v-model:open="isModalOpen" class="palette-modal" shadow-xl important:max-w-650px important:pa-12px @keydown="handleKeydown">
<c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text placeholder="Type to search a tool or a command..." autofocus clearable /> <c-input-text ref="inputRef" v-model:value="searchPrompt" raw-text :placeholder="t('home.commandPalette.placeholder')" autofocus clearable />
<div v-for="(options, category) in filteredSearchResult" :key="category"> <div v-for="(options, category) in filteredSearchResult" :key="category">
<div ml-3 mt-3 text-sm font-bold text-primary op-60> <div ml-3 mt-3 text-sm font-bold text-primary op-60>
{{ category }} {{ t(category) }}
</div> </div>
<command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" /> <command-palette-option v-for="option in options" :key="option.name" :option="option" :selected="selectedOptionIndex === getOptionIndex(option)" @activated="activateOption" />
</div> </div>

View file

@ -8,6 +8,7 @@ const emit = defineEmits(['activated']);
const { option } = toRefs(props); const { option } = toRefs(props);
const { selected } = toRefs(props); const { selected } = toRefs(props);
const { t } = useI18n();
</script> </script>
<template> <template>
@ -29,7 +30,7 @@ const { selected } = toRefs(props);
</div> </div>
<div v-if="option.description" truncate lh-tight op-60> <div v-if="option.description" truncate lh-tight op-60>
{{ option.description }} {{ t(option.description) }}
</div> </div>
</div> </div>
</div> </div>

View file

@ -7,9 +7,9 @@ import { useToolStore } from '@/tools/tools.store';
import { config } from '@/config'; import { config } from '@/config';
const toolStore = useToolStore(); const toolStore = useToolStore();
useHead({ title: 'IT Tools - Handy online tools for developers' });
const { t } = useI18n(); const { t } = useI18n();
const title = t('home.categories.title');
useHead({ title });
</script> </script>
<template> <template>
@ -17,29 +17,28 @@ const { t } = useI18n();
<div class="grid-wrapper"> <div class="grid-wrapper">
<n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-grid v-if="config.showBanner" x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi> <n-gi>
<ColoredCard title="You like it-tools?" :icon="Heart"> <ColoredCard title="{{t('home.categories.youLikeItTools')}}" :icon="Heart">
Give us a star on t('home.categories.giveUsAStarOn')
<a <a
href="https://github.com/CorentinTh/it-tools" href="https://github.com/CorentinTh/it-tools"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
aria-label="IT-Tools' GitHub repository" aria-label="IT-Tools' GitHub repository"
>GitHub</a> >GitHub</a>
or follow us on t('home.categories.orFollowUsOn')
<a <a
href="https://twitter.com/ittoolsdottech" href="https://twitter.com/ittoolsdottech"
rel="noopener" rel="noopener"
target="_blank" target="_blank"
aria-label="IT-Tools' Twitter account" aria-label="IT-Tools' Twitter account"
>Twitter</a>! Thank you >Twitter</a>t('home.categories.thanks')
<n-icon :component="Heart" /> <n-icon :component="Heart" />
</ColoredCard> </ColoredCard>
</n-gi> </n-gi>
</n-grid> </n-grid>
<transition name="height"> <transition name="height">
<div v-if="toolStore.favoriteTools.length > 0"> <div v-if="toolStore.favoriteTools.length > 0">
<n-h3>Your favorite tools</n-h3> <n-h3>{{ t('home.categories.favoriteTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name"> <n-gi v-for="tool in toolStore.favoriteTools" :key="tool.name">
<ToolCard :tool="tool" /> <ToolCard :tool="tool" />
@ -57,7 +56,7 @@ const { t } = useI18n();
</n-grid> </n-grid>
</div> </div>
<n-h3>All the tools</n-h3> <n-h3>{{ t('home.categories.allTools') }}</n-h3>
<n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8"> <n-grid x-gap="12" y-gap="12" cols="1 400:2 800:3 1200:4 2000:8">
<n-gi v-for="tool in toolStore.tools" :key="tool.name"> <n-gi v-for="tool in toolStore.tools" :key="tool.name">
<transition> <transition>

View file

@ -4,7 +4,7 @@ import { defineTool } from '../tool';
export const tool = defineTool({ export const tool = defineTool({
name: 'XML formatter', name: 'XML formatter',
path: '/xml-formatter', path: '/xml-formatter',
description: 'Prettify your XML string to a human friendly readable format.', description: 'tools.xmlFormat.description',
keywords: ['xml', 'prettify', 'format'], keywords: ['xml', 'prettify', 'format'],
component: () => import('./xml-formatter.vue'), component: () => import('./xml-formatter.vue'),
icon: Code, icon: Code,

View file

@ -2,6 +2,8 @@
import { formatXml, isValidXML } from './xml-formatter.service'; import { formatXml, isValidXML } from './xml-formatter.service';
import type { UseValidationRule } from '@/composable/validation'; import type { UseValidationRule } from '@/composable/validation';
const { t } = useI18n();
const defaultValue = '<hello><world>foo</world><world>bar</world></hello>'; const defaultValue = '<hello><world>foo</world><world>bar</world></hello>';
const indentSize = useStorage('xml-formatter:indent-size', 2); const indentSize = useStorage('xml-formatter:indent-size', 2);
const collapseContent = useStorage('xml-formatter:collapse-content', true); const collapseContent = useStorage('xml-formatter:collapse-content', true);
@ -17,7 +19,7 @@ function transformer(value: string) {
const rules: UseValidationRule<string>[] = [ const rules: UseValidationRule<string>[] = [
{ {
validator: isValidXML, validator: isValidXML,
message: 'Provided XML is not valid.', message: t('tools.xmlFormat.providedXmlIsNotValid'),
}, },
]; ];
</script> </script>
@ -25,19 +27,19 @@ const rules: UseValidationRule<string>[] = [
<template> <template>
<div important:flex-full important:flex-shrink-0 important:flex-grow-0> <div important:flex-full important:flex-shrink-0 important:flex-grow-0>
<div flex justify-center> <div flex justify-center>
<n-form-item label="Collapse content:" label-placement="left"> <n-form-item :label="t('tools.xmlFormat.collapseContent')" label-placement="left">
<n-switch v-model:value="collapseContent" /> <n-switch v-model:value="collapseContent" />
</n-form-item> </n-form-item>
<n-form-item label="Indent size:" label-placement="left" label-width="100" :show-feedback="false"> <n-form-item :label="t('tools.xmlFormat.indentSize')" label-placement="left" label-width="100" :show-feedback="false">
<n-input-number v-model:value="indentSize" min="0" max="10" w-100px /> <n-input-number v-model:value="indentSize" min="0" max="10" w-100px />
</n-form-item> </n-form-item>
</div> </div>
</div> </div>
<format-transformer <format-transformer
input-label="Your XML" :input-label="t('tools.xmlFormat.inputLabel')"
input-placeholder="Paste your XML here..." :input-placeholder="t('tools.xmlFormat.placeHolder')"
output-label="Formatted XML from your XML" :output-label="t('tools.xmlFormat.outputLabel')"
output-language="xml" output-language="xml"
:input-validation-rules="rules" :input-validation-rules="rules"
:transformer="transformer" :transformer="transformer"