feat(translate): Complete translation of all tools

This commit is contained in:
Amery2010 2024-02-21 15:56:48 +08:00
parent a2e498d0aa
commit ae71454a38
47 changed files with 456 additions and 120 deletions

View file

@ -7,6 +7,7 @@ import { arrayToMarkdownTable, computeAverage, computeVariance } from './benchma
import DynamicValues from './dynamic-values.vue'; import DynamicValues from './dynamic-values.vue';
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
const { t } = useI18n();
const suites = useStorage('benchmark-builder:suites', [ const suites = useStorage('benchmark-builder:suites', [
{ title: 'Suite 1', data: [5, 10] }, { title: 'Suite 1', data: [5, 10] },
{ title: 'Suite 2', data: [8, 12] }, { title: 'Suite 2', data: [8, 12] },
@ -51,11 +52,11 @@ const results = computed(() => {
const { copy } = useCopy({ createToast: false }); const { copy } = useCopy({ createToast: false });
const header = { const header = {
position: 'Position', position: t('tools.benchmark-builder.header.position'),
title: 'Suite', title: t('tools.benchmark-builder.header.title'),
size: 'Samples', size: t('tools.benchmark-builder.header.size'),
mean: 'Mean', mean: t('tools.benchmark-builder.header.mean'),
variance: 'Variance', variance: t('tools.benchmark-builder.header.variance'),
}; };
function copyAsMarkdown() { function copyAsMarkdown() {
@ -86,13 +87,13 @@ function copyAsBulletList() {
<c-input-text <c-input-text
v-model:value="suite.title" v-model:value="suite.title"
label-position="left" label-position="left"
label="Suite name" :label="t('tools.benchmark-builder.suiteNameLabel')"
placeholder="Suite name..." :placeholder="t('tools.benchmark-builder.suiteNamePlaceholder')"
clearable clearable
/> />
<n-divider /> <n-divider />
<n-form-item label="Suite values" :show-feedback="false"> <n-form-item :label="t('tools.benchmark-builder.suiteValues')" :show-feedback="false">
<DynamicValues v-model:values="suite.data" /> <DynamicValues v-model:values="suite.data" />
</n-form-item> </n-form-item>
</c-card> </c-card>
@ -100,14 +101,14 @@ function copyAsBulletList() {
<div flex justify-center> <div flex justify-center>
<c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)"> <c-button v-if="suites.length > 1" variant="text" @click="suites.splice(index, 1)">
<n-icon :component="Trash" depth="3" mr-2 size="18" /> <n-icon :component="Trash" depth="3" mr-2 size="18" />
Delete suite {{ t('tools.benchmark-builder.deleteSuite') }}
</c-button> </c-button>
<c-button <c-button
variant="text" variant="text"
@click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })" @click="suites.splice(index + 1, 0, { data: [0], title: `Suite ${suites.length + 1}` })"
> >
<n-icon :component="Plus" depth="3" mr-2 size="18" /> <n-icon :component="Plus" depth="3" mr-2 size="18" />
Add suite {{ t('tools.benchmark-builder.addSuite') }}
</c-button> </c-button>
</div> </div>
</div> </div>
@ -117,7 +118,7 @@ function copyAsBulletList() {
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div style="max-width: 600px; margin: 0 auto"> <div style="max-width: 600px; margin: 0 auto">
<div mx-auto max-w-sm flex justify-center gap-3> <div mx-auto max-w-sm flex justify-center gap-3>
<c-input-text v-model:value="unit" placeholder="Unit (eg: ms)" label="Unit" label-position="left" mb-4 /> <c-input-text v-model:value="unit" :placeholder="t('tools.benchmark-builder.unitPlaceholder')" :label="t('tools.benchmark-builder.unitLabel')" label-position="left" mb-4 />
<c-button <c-button
@click=" @click="
@ -127,7 +128,7 @@ function copyAsBulletList() {
] ]
" "
> >
Reset suites {{ t('tools.benchmark-builder.resetSuites') }}
</c-button> </c-button>
</div> </div>
@ -135,10 +136,10 @@ function copyAsBulletList() {
<div mt-5 flex justify-center gap-3> <div mt-5 flex justify-center gap-3>
<c-button @click="copyAsMarkdown()"> <c-button @click="copyAsMarkdown()">
Copy as markdown table {{ t('tools.benchmark-builder.copyAsMarkdown') }}
</c-button> </c-button>
<c-button @click="copyAsBulletList()"> <c-button @click="copyAsBulletList()">
Copy as bullet list {{ t('tools.benchmark-builder.copyAsBulletList') }}
</c-button> </c-button>
</div> </div>
</div> </div>

View file

@ -10,6 +10,7 @@ const emit = defineEmits(['update:values']);
const refs = useTemplateRefsList<typeof NInputNumber>(); const refs = useTemplateRefsList<typeof NInputNumber>();
const { t } = useI18n();
const values = useVModel(props, 'values', emit); const values = useVModel(props, 'values', emit);
async function addValue() { async function addValue() {
@ -35,11 +36,11 @@ function onInputEnter(index: number) {
:ref="refs.set" :ref="refs.set"
v-model:value="values[index]" v-model:value="values[index]"
:show-button="false" :show-button="false"
placeholder="Set your measure..." :placeholder="t('tools.benchmark-builder.setYourMeasure')"
autofocus autofocus
@keydown.enter="onInputEnter(index)" @keydown.enter="onInputEnter(index)"
/> />
<c-tooltip tooltip="Delete this value"> <c-tooltip :tooltip="t('tools.benchmark-builder.deleteThisValue')">
<c-button circle variant="text" @click="values.splice(index, 1)"> <c-button circle variant="text" @click="values.splice(index, 1)">
<n-icon :component="Trash" depth="3" size="18" /> <n-icon :component="Trash" depth="3" size="18" />
</c-button> </c-button>
@ -48,7 +49,7 @@ function onInputEnter(index: number) {
<c-button @click="addValue"> <c-button @click="addValue">
<n-icon :component="Plus" depth="3" mr-2 size="18" /> <n-icon :component="Plus" depth="3" mr-2 size="18" />
Add a measure {{ t('tools.benchmark-builder.addAMeasure') }}
</c-button> </c-button>
</div> </div>
</template> </template>

View file

@ -1,10 +1,11 @@
import { SpeedFilled } from '@vicons/material'; import { SpeedFilled } from '@vicons/material';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Benchmark builder', name: t('tools.benchmark-builder.title'),
path: '/benchmark-builder', path: '/benchmark-builder',
description: 'Easily compare execution time of tasks with this very simple online benchmark builder.', description: t('tools.benchmark-builder.description'),
keywords: ['benchmark', 'builder', 'execution', 'duration', 'mean', 'variance'], keywords: ['benchmark', 'builder', 'execution', 'duration', 'mean', 'variance'],
component: () => import('./benchmark-builder.vue'), component: () => import('./benchmark-builder.vue'),
icon: SpeedFilled, icon: SpeedFilled,

View file

@ -0,0 +1,26 @@
tools:
benchmark-builder:
title: Benchmark builder
description: Easily compare execution time of tasks with this very simple online benchmark builder.
header:
position: Position
title: Suite
size: Samples
mean: Mean
variance: Variance
suiteNameLabel: Suite name
suiteNamePlaceholder: Suite name...
suiteValues: Suite values
suiteTitle: Suite {index}
deleteSuite: Delete suite
addSuite: Add suite
resetSuites: Reset suites
unitLabel: Unit
unitPlaceholder: 'Unit (eg: ms)'
setYourMeasure: Set your measure...
deleteThisValue: Delete this value
addAMeasure: Add a measure
copyAsMarkdown: Copy as markdown table
copyAsBulletList: Copy as bullet list

View file

@ -0,0 +1,26 @@
tools:
benchmark-builder:
title: 基准测试生成器
description: 使用这个非常简单的在线基准测试生成器轻松比较任务的执行时间。
header:
position: 位置
title: 套件
size: 样本数
mean: 平均值
variance: 方差
suiteNameLabel: 套件名称
suiteNamePlaceholder: 输入套件名称...
suiteValues: 套件数值
suiteTitle: 套件 {index}
deleteSuite: 删除套件
addSuite: 添加套件
resetSuites: 重置套件
unitLabel: 单位
unitPlaceholder: '单位(例如:毫秒)'
setYourMeasure: 设置您的度量...
deleteThisValue: 删除此数值
addAMeasure: 添加一个度量
copyAsMarkdown: 复制为 markdown 表格
copyAsBulletList: 复制为项目列表

View file

@ -3,6 +3,7 @@ import { useRafFn } from '@vueuse/core';
import { formatMs } from './chronometer.service'; import { formatMs } from './chronometer.service';
const { t } = useI18n();
const isRunning = ref(false); const isRunning = ref(false);
const counter = ref(0); const counter = ref(0);
@ -37,14 +38,14 @@ function pause() {
</c-card> </c-card>
<div mt-5 flex justify-center gap-3> <div mt-5 flex justify-center gap-3>
<c-button v-if="!isRunning" type="primary" @click="resume"> <c-button v-if="!isRunning" type="primary" @click="resume">
Start {{ t('tools.chronometer.start') }}
</c-button> </c-button>
<c-button v-else type="warning" @click="pause"> <c-button v-else type="warning" @click="pause">
Stop {{ t('tools.chronometer.stop') }}
</c-button> </c-button>
<c-button @click="counter = 0"> <c-button @click="counter = 0">
Reset {{ t('tools.chronometer.reset') }}
</c-button> </c-button>
</div> </div>
</div> </div>

View file

@ -1,10 +1,11 @@
import { TimerOutlined } from '@vicons/material'; import { TimerOutlined } from '@vicons/material';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Chronometer', name: t('tools.chronometer.title'),
path: '/chronometer', path: '/chronometer',
description: 'Monitor the duration of a thing. Basically a chronometer with simple chronometer features.', description: t('tools.chronometer.description'),
keywords: ['chronometer', 'time', 'lap', 'duration', 'measure', 'pause', 'resume', 'stopwatch'], keywords: ['chronometer', 'time', 'lap', 'duration', 'measure', 'pause', 'resume', 'stopwatch'],
component: () => import('./chronometer.vue'), component: () => import('./chronometer.vue'),
icon: TimerOutlined, icon: TimerOutlined,

View file

@ -0,0 +1,8 @@
tools:
chronometer:
title: Chronometer
description: Monitor the duration of a thing. Basically a chronometer with simple chronometer features.
start: Start
stop: Stop
reset: Reset

View file

@ -0,0 +1,8 @@
tools:
chronometer:
title: 计时器
description: 监控事物的持续时间。基本上是一个具有简单计时器功能的计时器。
start: 开始
stop: 停止
reset: 重置

View file

@ -5,12 +5,13 @@ import { useCopy } from '@/composable/copy';
const props = (defineProps<{ emojiInfo: EmojiInfo }>()); const props = (defineProps<{ emojiInfo: EmojiInfo }>());
const { emojiInfo } = toRefs(props); const { emojiInfo } = toRefs(props);
const { t } = useI18n();
const { copy } = useCopy(); const { copy } = useCopy();
</script> </script>
<template> <template>
<c-card flex items-center gap-3 important:py-8px important:pl-10px important:pr-5px> <c-card flex items-center gap-3 important:py-8px important:pl-10px important:pr-5px>
<div cursor-pointer text-30px @click="copy(emojiInfo.emoji, { notificationMessage: `Emoji ${emojiInfo.emoji} copied to the clipboard` })"> <div cursor-pointer text-30px @click="copy(emojiInfo.emoji, { notificationMessage: t('tools.emoji-picker.emojiCopied', { emoji: emojiInfo.emoji }) })">
{{ emojiInfo.emoji }} {{ emojiInfo.emoji }}
</div> </div>
@ -30,10 +31,10 @@ const { copy } = useCopy();
</div> --> </div> -->
<div flex gap-2 text-xs font-mono op-70> <div flex gap-2 text-xs font-mono op-70>
<span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: `Code points '${emojiInfo.codePoints}' copied to the clipboard` })"> <span cursor-pointer transition hover:text-primary @click="copy(emojiInfo.codePoints, { notificationMessage: t('tools.emoji-picker.codePointsCopied', { codePoints: emojiInfo.codePoints }) })">
{{ emojiInfo.codePoints }} {{ emojiInfo.codePoints }}
</span> </span>
<span cursor-pointer truncate transition hover:text-primary @click="copy(emojiInfo.unicode, { notificationMessage: `Unicode '${emojiInfo.unicode}' copied to the clipboard` })"> <span cursor-pointer truncate transition hover:text-primary @click="copy(emojiInfo.unicode, { notificationMessage: t('tools.emoji-picker.unicodeCopied', { unicode: emojiInfo.unicode }) })">
{{ emojiInfo.unicode }} {{ emojiInfo.unicode }}
</span> </span>
</div> </div>

View file

@ -5,6 +5,7 @@ import _ from 'lodash';
import type { EmojiInfo } from './emoji.types'; import type { EmojiInfo } from './emoji.types';
import { useFuzzySearch } from '@/composable/fuzzySearch'; import { useFuzzySearch } from '@/composable/fuzzySearch';
const { t } = useI18n();
const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join(''); const escapeUnicode = ({ emoji }: { emoji: string }) => emoji.split('').map(unit => `\\u${unit.charCodeAt(0).toString(16).padStart(4, '0')}`).join('');
const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined; const getEmojiCodePoints = ({ emoji }: { emoji: string }) => emoji.codePointAt(0) ? `0x${emoji.codePointAt(0)?.toString(16)}` : undefined;
@ -42,7 +43,7 @@ const { searchResult } = useFuzzySearch({
<div flex items-center gap-3> <div flex items-center gap-3>
<c-input-text <c-input-text
v-model:value="searchQuery" v-model:value="searchQuery"
placeholder="Search emojis (e.g. 'smile')..." :placeholder="t('tools.emoji-picker.searchPlaceholder')"
mx-auto max-w-600px mx-auto max-w-600px
> >
<template #prefix> <template #prefix>
@ -58,12 +59,12 @@ const { searchResult } = useFuzzySearch({
text-20px text-20px
font-bold font-bold
> >
No results {{ t('tools.emoji-picker.noResults') }}
</div> </div>
<div v-else> <div v-else>
<div mt-4 text-20px font-bold> <div mt-4 text-20px font-bold>
Search result {{ t('tools.emoji-picker.searchResult') }}
</div> </div>
<emoji-grid :emoji-infos="searchResult" /> <emoji-grid :emoji-infos="searchResult" />

View file

@ -1,10 +1,11 @@
import { MoodSmile } from '@vicons/tabler'; import { MoodSmile } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Emoji picker', name: t('tools.emoji-picker.title'),
path: '/emoji-picker', path: '/emoji-picker',
description: 'Copy and paste emojis easily and get the unicode and code points value of each emoji.', description: t('tools.emoji-picker.description'),
keywords: ['emoji', 'picker', 'unicode', 'copy', 'paste'], keywords: ['emoji', 'picker', 'unicode', 'copy', 'paste'],
component: () => import('./emoji-picker.vue'), component: () => import('./emoji-picker.vue'),
icon: MoodSmile, icon: MoodSmile,

View file

@ -0,0 +1,12 @@
tools:
emoji-picker:
title: Emoji picker
description: Copy and paste emojis easily and get the unicode and code points value of each emoji.
searchPlaceholder: Search emojis (e.g. "smile")...
noResults: No results
searchResult: Search result
emojiCopied: Emoji {emoji} copied to the clipboard
codePointsCopied: Code points {codePoints} copied to the clipboard
unicodeCopied: Unicode {unicode} copied to the clipboard

View file

@ -0,0 +1,12 @@
tools:
emoji-picker:
title: 表情选择器
description: 轻松复制和粘贴表情符号,并获取每个表情符号的 Unicode 和代码点值。
searchPlaceholder: 搜索表情符号(例如“笑脸”)...
noResults: 没有结果
searchResult: 搜索结果
emojiCopied: 表情符号 {emoji} 已复制到剪贴板
codePointsCopied: 代码点 {codePoints} 已复制到剪贴板
unicodeCopied: Unicode {unicode} 已复制到剪贴板

View file

@ -1,16 +1,17 @@
import { ValidationErrorsIBAN } from 'ibantools'; import { ValidationErrorsIBAN } from 'ibantools';
import { translate as t } from '@/plugins/i18n.plugin';
export { getFriendlyErrors }; export { getFriendlyErrors };
const ibanErrorToMessage = { const ibanErrorToMessage = {
[ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided', [ValidationErrorsIBAN.NoIBANProvided]: t('tools.iban-validator-and-parser.noIBANProvided'),
[ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country', [ValidationErrorsIBAN.NoIBANCountry]: t('tools.iban-validator-and-parser.noIBANCountry'),
[ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length', [ValidationErrorsIBAN.WrongBBANLength]: t('tools.iban-validator-and-parser.wrongBBANLength'),
[ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format', [ValidationErrorsIBAN.WrongBBANFormat]: t('tools.iban-validator-and-parser.wrongBBANFormat'),
[ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number', [ValidationErrorsIBAN.ChecksumNotNumber]: t('tools.iban-validator-and-parser.checksumNotNumber'),
[ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum', [ValidationErrorsIBAN.WrongIBANChecksum]: t('tools.iban-validator-and-parser.wrongIBANChecksum'),
[ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum', [ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: t('tools.iban-validator-and-parser.wrongAccountBankBranchChecksum'),
[ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed', [ValidationErrorsIBAN.QRIBANNotAllowed]: t('tools.iban-validator-and-parser.QRIBANNotAllowed'),
}; };
function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) { function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) {

View file

@ -3,6 +3,7 @@ import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantoo
import { getFriendlyErrors } from './iban-validator-and-parser.service'; import { getFriendlyErrors } from './iban-validator-and-parser.service';
import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types'; import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types';
const { t } = useI18n();
const rawIban = ref(''); const rawIban = ref('');
const ibanInfo = computed<CKeyValueListItems>(() => { const ibanInfo = computed<CKeyValueListItems>(() => {
@ -19,31 +20,31 @@ const ibanInfo = computed<CKeyValueListItems>(() => {
return [ return [
{ {
label: 'Is IBAN valid ?', label: t('tools.iban-validator-and-parser.isIbanValid'),
value: isIbanValid, value: isIbanValid,
showCopyButton: false, showCopyButton: false,
}, },
{ {
label: 'IBAN errors', label: t('tools.iban-validator-and-parser.IBANErrors'),
value: errors.length === 0 ? undefined : errors, value: errors.length === 0 ? undefined : errors,
hideOnNil: true, hideOnNil: true,
showCopyButton: false, showCopyButton: false,
}, },
{ {
label: 'Is IBAN a QR-IBAN ?', label: t('tools.iban-validator-and-parser.isQRIBAN'),
value: isQRIBAN(iban), value: isQRIBAN(iban),
showCopyButton: false, showCopyButton: false,
}, },
{ {
label: 'Country code', label: t('tools.iban-validator-and-parser.countryCode'),
value: countryCode, value: countryCode,
}, },
{ {
label: 'BBAN', label: t('tools.iban-validator-and-parser.BBAN'),
value: bban, value: bban,
}, },
{ {
label: 'IBAN friendly format', label: t('tools.iban-validator-and-parser.IBANFriendlyFormat'),
value: friendlyFormatIBAN(iban), value: friendlyFormatIBAN(iban),
}, },
]; ];
@ -58,13 +59,13 @@ const ibanExamples = [
<template> <template>
<div> <div>
<c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" /> <c-input-text v-model:value="rawIban" :placeholder="t('tools.iban-validator-and-parser.inputPlaceholder')" test-id="iban-input" />
<c-card v-if="ibanInfo.length > 0" mt-5> <c-card v-if="ibanInfo.length > 0" mt-5>
<c-key-value-list :items="ibanInfo" data-test-id="iban-info" /> <c-key-value-list :items="ibanInfo" data-test-id="iban-info" />
</c-card> </c-card>
<c-card title="Valid IBAN examples" mt-5> <c-card :title="t('tools.iban-validator-and-parser.ibanExamples')" mt-5>
<div v-for="iban in ibanExamples" :key="iban"> <div v-for="iban in ibanExamples" :key="iban">
<c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" /> <c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" />
</div> </div>

View file

@ -1,10 +1,11 @@
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import Bank from '~icons/mdi/bank'; import Bank from '~icons/mdi/bank';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'IBAN validator and parser', name: t('tools.iban-validator-and-parser.title'),
path: '/iban-validator-and-parser', path: '/iban-validator-and-parser',
description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.', description: t('tools.iban-validator-and-parser.description'),
keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'], keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'],
component: () => import('./iban-validator-and-parser.vue'), component: () => import('./iban-validator-and-parser.vue'),
icon: Bank, icon: Bank,

View file

@ -0,0 +1,22 @@
tools:
iban-validator-and-parser:
title: IBAN validator and parser
description: Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.
inputPlaceholder: Enter an IBAN to check for validity...
ibanExamples: Valid IBAN examples
isIbanValid: Is IBAN valid ?
IBANErrors: IBAN errors
isQRIBAN: Is IBAN a QR-IBAN ?
countryCode: Country code
BBAN: BBAN
IBANFriendlyFormat: IBAN friendly format
noIBANProvided: No IBAN provided
noIBANCountry: No IBAN country
wrongBBANLength: Wrong BBAN length
wrongBBANFormat: Wrong BBAN format
checksumNotNumber: Checksum is not a number
wrongIBANChecksum: Wrong IBAN checksum
wrongAccountBankBranchChecksum: Wrong account bank branch checksum
QRIBANNotAllowed: QR-IBAN not allowed

View file

@ -0,0 +1,22 @@
tools:
iban-validator-and-parser:
title: IBAN 验证器和解析器
description: 验证和解析 IBAN 号码。检查 IBAN 是否有效并获取国家、BBAN、是否为 QR-IBAN 和 IBAN 友好格式。
inputPlaceholder: 输入要检查有效性的 IBAN…
ibanExamples: 有效的 IBAN 示例
isIbanValid: IBAN 是否有效?
IBANErrors: IBAN 错误
isQRIBAN: 是否为 QR-IBAN
countryCode: 国家代码
BBAN: BBAN
IBANFriendlyFormat: IBAN 友好格式
noIBANProvided: 未提供 IBAN
noIBANCountry: 未提供 IBAN 国家
wrongBBANLength: BBAN 长度错误
wrongBBANFormat: BBAN 格式错误
checksumNotNumber: 校验和不是数字
wrongIBANChecksum: IBAN 校验和错误
wrongAccountBankBranchChecksum: 帐户银行分行校验和错误
QRIBANNotAllowed: 不允许 QR-IBAN

View file

@ -1,11 +1,11 @@
import { AlignJustified } from '@vicons/tabler'; import { AlignJustified } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Lorem ipsum generator', name: t('tools.lorem-ipsum-generator.title'),
path: '/lorem-ipsum-generator', path: '/lorem-ipsum-generator',
description: description: t('tools.lorem-ipsum-generator.description'),
'Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content',
keywords: ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'placeholder', 'text', 'filler', 'random', 'generator'], keywords: ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'placeholder', 'text', 'filler', 'random', 'generator'],
component: () => import('./lorem-ipsum-generator.vue'), component: () => import('./lorem-ipsum-generator.vue'),
icon: AlignJustified, icon: AlignJustified,

View file

@ -0,0 +1,14 @@
tools:
lorem-ipsum-generator:
title: Lorem ipsum generator
description: Lorem ipsum is a placeholder text commonly used to demonstrate the visual form of a document or a typeface without relying on meaningful content.
paragraphs: Paragraphs
sentences: Sentences per paragraph
words: Words per sentence
startWithLoremIpsum: Start with lorem ipsum ?
asHTML: As html ?
loremIpsumText: Your lorem ipsum...
copyBtn: Copy
copied: Lorem ipsum copied to the clipboard

View file

@ -0,0 +1,14 @@
tools:
lorem-ipsum-generator:
title: Lorem Ipsum 生成器
description: Lorem Ipsum 是一种常用的占位文本,用于展示文档或字体的视觉形式,而不依赖于有意义的内容。
paragraphs: 段落
sentences: 每段句子数
words: 每句字数
startWithLoremIpsum: 以 Lorem Ipsum 开头?
asHTML: 作为 HTML
loremIpsumText: 您的 Lorem Ipsum...
copyBtn: 复制
copied: Lorem Ipsum 已复制到剪贴板

View file

@ -3,6 +3,7 @@ import { generateLoremIpsum } from './lorem-ipsum-generator.service';
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
import { randIntFromInterval } from '@/utils/random'; import { randIntFromInterval } from '@/utils/random';
const { t } = useI18n();
const paragraphs = ref(1); const paragraphs = ref(1);
const sentences = ref([3, 8]); const sentences = ref([3, 8]);
const words = ref([8, 15]); const words = ref([8, 15]);
@ -18,32 +19,32 @@ const loremIpsumText = computed(() =>
startWithLoremIpsum: startWithLoremIpsum.value, startWithLoremIpsum: startWithLoremIpsum.value,
}), }),
); );
const { copy } = useCopy({ source: loremIpsumText, text: 'Lorem ipsum copied to the clipboard' }); const { copy } = useCopy({ source: loremIpsumText, text: t('tools.lorem-ipsum-generator.copied') });
</script> </script>
<template> <template>
<c-card> <c-card>
<n-form-item label="Paragraphs" :show-feedback="false" label-width="200" label-placement="left"> <n-form-item :label="t('tools.lorem-ipsum-generator.paragraphs')" :show-feedback="false" label-width="200" label-placement="left">
<n-slider v-model:value="paragraphs" :step="1" :min="1" :max="20" /> <n-slider v-model:value="paragraphs" :step="1" :min="1" :max="20" />
</n-form-item> </n-form-item>
<n-form-item label="Sentences per paragraph" :show-feedback="false" label-width="200" label-placement="left"> <n-form-item :label="t('tools.lorem-ipsum-generator.sentences')" :show-feedback="false" label-width="200" label-placement="left">
<n-slider v-model:value="sentences" range :step="1" :min="1" :max="50" /> <n-slider v-model:value="sentences" range :step="1" :min="1" :max="50" />
</n-form-item> </n-form-item>
<n-form-item label="Words per sentence" :show-feedback="false" label-width="200" label-placement="left"> <n-form-item :label="t('tools.lorem-ipsum-generator.words')" :show-feedback="false" label-width="200" label-placement="left">
<n-slider v-model:value="words" range :step="1" :min="1" :max="50" /> <n-slider v-model:value="words" range :step="1" :min="1" :max="50" />
</n-form-item> </n-form-item>
<n-form-item label="Start with lorem ipsum ?" :show-feedback="false" label-width="200" label-placement="left"> <n-form-item :label="t('tools.lorem-ipsum-generator.startWithLoremIpsum')" :show-feedback="false" label-width="200" label-placement="left">
<n-switch v-model:value="startWithLoremIpsum" /> <n-switch v-model:value="startWithLoremIpsum" />
</n-form-item> </n-form-item>
<n-form-item label="As html ?" :show-feedback="false" label-width="200" label-placement="left"> <n-form-item :label="t('tools.lorem-ipsum-generator.asHTML')" :show-feedback="false" label-width="200" label-placement="left">
<n-switch v-model:value="asHTML" /> <n-switch v-model:value="asHTML" />
</n-form-item> </n-form-item>
<c-input-text :value="loremIpsumText" multiline placeholder="Your lorem ipsum..." readonly mt-5 rows="5" /> <c-input-text :value="loremIpsumText" multiline :placeholder="t('tools.lorem-ipsum-generator.loremIpsumText')" readonly mt-5 rows="5" />
<div mt-5 flex justify-center> <div mt-5 flex justify-center>
<c-button autofocus @click="copy()"> <c-button autofocus @click="copy()">
Copy {{ t('tools.lorem-ipsum-generator.copyBtn') }}
</c-button> </c-button>
</div> </div>
</c-card> </c-card>

View file

@ -1,10 +1,11 @@
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import n7mIcon from './n7m-icon.svg?component'; import n7mIcon from './n7m-icon.svg?component';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Numeronym generator', name: t('tools.numeronym-generator.title'),
path: '/numeronym-generator', path: '/numeronym-generator',
description: 'A numeronym is a word where a number is used to form an abbreviation. For example, "i18n" is a numeronym of "internationalization" where 18 stands for the number of letters between the first i and the last n in the word.', description: t('tools.numeronym-generator.description'),
keywords: ['numeronym', 'generator', 'abbreviation', 'i18n', 'a11y', 'l10n'], keywords: ['numeronym', 'generator', 'abbreviation', 'i18n', 'a11y', 'l10n'],
component: () => import('./numeronym-generator.vue'), component: () => import('./numeronym-generator.vue'),
icon: n7mIcon, icon: n7mIcon,

View file

@ -0,0 +1,7 @@
tools:
numeronym-generator:
title: Numeronym generator
description: A numeronym is a word where a number is used to form an abbreviation. For example, "i18n" is a numeronym of "internationalization" where 18 stands for the number of letters between the first i and the last n in the word.
inputPlaceholder: "Enter a word, e.g. 'internationalization'"
outputPlaceholder: "Your numeronym will be here, e.g. 'i18n'"

View file

@ -0,0 +1,7 @@
tools:
numeronym-generator:
title: 数字缩略词生成器
description: 数字缩略词是一种使用数字形成缩略词的词。例如,"i18n" 是 "internationalization" 的数字缩略词,其中的 18 代表单词中第一个 i 和最后一个 n 之间的字母数量。
inputPlaceholder: "输入一个单词,例如 'internationalization'"
outputPlaceholder: "您的数字缩略词将显示在此处,例如 'i18n'"

View file

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { generateNumeronym } from './numeronym-generator.service'; import { generateNumeronym } from './numeronym-generator.service';
const { t } = useI18n();
const word = ref(''); const word = ref('');
const numeronym = computed(() => generateNumeronym(word.value)); const numeronym = computed(() => generateNumeronym(word.value));
@ -8,10 +9,10 @@ const numeronym = computed(() => generateNumeronym(word.value));
<template> <template>
<div flex flex-col items-center gap-4> <div flex flex-col items-center gap-4>
<c-input-text v-model:value="word" placeholder="Enter a word, e.g. 'internationalization'" size="large" clearable test-id="word-input" /> <c-input-text v-model:value="word" :placeholder="t('tools.numeronym-generator.inputPlaceholder')" size="large" clearable test-id="word-input" />
<icon-mdi-arrow-down text-30px /> <icon-mdi-arrow-down text-30px />
<input-copyable :value="numeronym" size="large" readonly placeholder="Your numeronym will be here, e.g. 'i18n'" test-id="numeronym" /> <input-copyable :value="numeronym" size="large" readonly :placeholder="t('tools.numeronym-generator.outputPlaceholder')" test-id="numeronym" />
</div> </div>
</template> </template>

View file

@ -1,11 +1,11 @@
import { Phone } from '@vicons/tabler'; import { Phone } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Phone parser and formatter', name: t('tools.phone-parser-and-formatter.title'),
path: '/phone-parser-and-formatter', path: '/phone-parser-and-formatter',
description: description: t('tools.phone-parser-and-formatter.description'),
'Parse, validate and format phone numbers. Get information about the phone number, like the country code, type, etc.',
keywords: [ keywords: [
'phone', 'phone',
'parser', 'parser',

View file

@ -0,0 +1,31 @@
tools:
phone-parser-and-formatter:
title: Phone parser and formatter
description: Parse, validate and format phone numbers. Get information about the phone number, like the country code, type, etc.
country: Country
countryCallingCode: Country calling code
isValid: Is valid?
isPossible: Is possible?
type: Type
internationalFormat: International format
nationalFormat: National format
E164Format: E.164 format
RFC3966Format: RFC3966 format
defaultCountryCode: 'Default country code:'
phoneNumberLabel: 'Phone number:'
phoneNumberPlaceholder: Enter a phone number
unknown: Unknown
mobile: Mobile
fixedLine: Fixed line
fixedLineOrMobile: Fixed line or mobile
personalNumber: Personal number
premiumRate: Premium rate
sharedCost: Shared cost
tollFree: Toll free
UAN: Universal access number
voicemail: Voicemail
VoIP: VoIP
pager: Pager
invalidMessage: Invalid phone number

View file

@ -0,0 +1,31 @@
tools:
phone-parser-and-formatter:
title: 电话号码解析和格式化工具
description: 解析、验证和格式化电话号码。获取有关电话号码的信息,如国家代码、类型等。
country: 国家
countryCallingCode: 国家区号
isValid: 是否有效?
isPossible: 是否可能?
type: 类型
internationalFormat: 国际格式
nationalFormat: 国内格式
E164Format: E.164 格式
RFC3966Format: RFC3966 格式
defaultCountryCode: '默认国家代码:'
phoneNumberLabel: '电话号码:'
phoneNumberPlaceholder: 输入电话号码
unknown: 未知
mobile: 移动电话
fixedLine: 固定电话
fixedLineOrMobile: 固定电话或移动电话
personalNumber: 个人号码
premiumRate: 高费率
sharedCost: 共享费用
tollFree: 免费电话
UAN: 通用访问号码
voicemail: 语音信箱
VoIP: 互联网电话
pager: 传呼机
invalidMessage: 无效的电话号码

View file

@ -1,20 +1,21 @@
import type { CountryCode, NumberType } from 'libphonenumber-js/types'; import type { CountryCode, NumberType } from 'libphonenumber-js/types';
import lookup from 'country-code-lookup'; import lookup from 'country-code-lookup';
import { translate as t } from '@/plugins/i18n.plugin';
export { formatTypeToHumanReadable, getFullCountryName, getDefaultCountryCode }; export { formatTypeToHumanReadable, getFullCountryName, getDefaultCountryCode };
const typeToLabel: Record<NonNullable<NumberType>, string> = { const typeToLabel: Record<NonNullable<NumberType>, string> = {
MOBILE: 'Mobile', MOBILE: t('tools.phone-parser-and-formatter.mobile'),
FIXED_LINE: 'Fixed line', FIXED_LINE: t('tools.phone-parser-and-formatter.fixedLine'),
FIXED_LINE_OR_MOBILE: 'Fixed line or mobile', FIXED_LINE_OR_MOBILE: t('tools.phone-parser-and-formatter.fixedLineOrMobile'),
PERSONAL_NUMBER: 'Personal number', PERSONAL_NUMBER: t('tools.phone-parser-and-formatter.personalNumber'),
PREMIUM_RATE: 'Premium rate', PREMIUM_RATE: t('tools.phone-parser-and-formatter.premiumRate'),
SHARED_COST: 'Shared cost', SHARED_COST: t('tools.phone-parser-and-formatter.sharedCost'),
TOLL_FREE: 'Toll free', TOLL_FREE: t('tools.phone-parser-and-formatter.tollFree'),
UAN: 'Universal access number', UAN: t('tools.phone-parser-and-formatter.UAN'),
VOICEMAIL: 'Voicemail', VOICEMAIL: t('tools.phone-parser-and-formatter.voicemail'),
VOIP: 'VoIP', VOIP: t('tools.phone-parser-and-formatter.VoIP'),
PAGER: 'Pager', PAGER: t('tools.phone-parser-and-formatter.pager'),
}; };
function formatTypeToHumanReadable(type: NumberType): string | undefined { function formatTypeToHumanReadable(type: NumberType): string | undefined {

View file

@ -10,6 +10,7 @@ import { withDefaultOnError } from '@/utils/defaults';
import { booleanToHumanReadable } from '@/utils/boolean'; import { booleanToHumanReadable } from '@/utils/boolean';
import { useValidation } from '@/composable/validation'; import { useValidation } from '@/composable/validation';
const { t } = useI18n();
const rawPhone = ref(''); const rawPhone = ref('');
const defaultCountryCode = ref(getDefaultCountryCode()); const defaultCountryCode = ref(getDefaultCountryCode());
const validation = useValidation({ const validation = useValidation({
@ -17,7 +18,7 @@ const validation = useValidation({
rules: [ rules: [
{ {
validator: value => value === '' || /^[0-9 +\-()]+$/.test(value), validator: value => value === '' || /^[0-9 +\-()]+$/.test(value),
message: 'Invalid phone number', message: t('tools.phone-parser-and-formatter.invalidMessage'),
}, },
], ],
}); });
@ -35,43 +36,43 @@ const parsedDetails = computed(() => {
return [ return [
{ {
label: 'Country', label: t('tools.phone-parser-and-formatter.country'),
value: parsed.country, value: parsed.country,
}, },
{ {
label: 'Country', label: t('tools.phone-parser-and-formatter.country'),
value: getFullCountryName(parsed.country), value: getFullCountryName(parsed.country),
}, },
{ {
label: 'Country calling code', label: t('tools.phone-parser-and-formatter.countryCallingCode'),
value: parsed.countryCallingCode, value: parsed.countryCallingCode,
}, },
{ {
label: 'Is valid?', label: t('tools.phone-parser-and-formatter.isValid'),
value: booleanToHumanReadable(parsed.isValid()), value: booleanToHumanReadable(parsed.isValid()),
}, },
{ {
label: 'Is possible?', label: t('tools.phone-parser-and-formatter.isPossible'),
value: booleanToHumanReadable(parsed.isPossible()), value: booleanToHumanReadable(parsed.isPossible()),
}, },
{ {
label: 'Type', label: t('tools.phone-parser-and-formatter.type'),
value: formatTypeToHumanReadable(parsed.getType()), value: formatTypeToHumanReadable(parsed.getType()),
}, },
{ {
label: 'International format', label: t('tools.phone-parser-and-formatter.internationalFormat'),
value: parsed.formatInternational(), value: parsed.formatInternational(),
}, },
{ {
label: 'National format', label: t('tools.phone-parser-and-formatter.nationalFormat'),
value: parsed.formatNational(), value: parsed.formatNational(),
}, },
{ {
label: 'E.164 format', label: t('tools.phone-parser-and-formatter.E164Format'),
value: parsed.format('E.164'), value: parsed.format('E.164'),
}, },
{ {
label: 'RFC3966 format', label: t('tools.phone-parser-and-formatter.RFC3966Format'),
value: parsed.format('RFC3966'), value: parsed.format('RFC3966'),
}, },
]; ];
@ -85,12 +86,12 @@ const countriesOptions = getCountries().map(code => ({
<template> <template>
<div> <div>
<c-select v-model:value="defaultCountryCode" label="Default country code:" :options="countriesOptions" searchable mb-5 /> <c-select v-model:value="defaultCountryCode" :label="t('tools.phone-parser-and-formatter.defaultCountryCode')" :options="countriesOptions" searchable mb-5 />
<c-input-text <c-input-text
v-model:value="rawPhone" v-model:value="rawPhone"
placeholder="Enter a phone number" :placeholder="t('tools.phone-parser-and-formatter.phoneNumberPlaceholder')"
label="Phone number:" :label="t('tools.phone-parser-and-formatter.phoneNumberLabel')"
:validation="validation" :validation="validation"
mb-5 mb-5
/> />
@ -104,7 +105,7 @@ const countriesOptions = getCountries().map(code => ({
<td> <td>
<span-copyable v-if="value" :value="value" /> <span-copyable v-if="value" :value="value" />
<span v-else op-70> <span v-else op-70>
Unknown {{ t('tools.phone-parser-and-formatter.unknown') }}
</span> </span>
</td> </td>
</tr> </tr>

View file

@ -1,10 +1,11 @@
import { EyeOff } from '@vicons/tabler'; import { EyeOff } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'String obfuscator', name: t('tools.string-obfuscator.title'),
path: '/string-obfuscator', path: '/string-obfuscator',
description: 'Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.', description: t('tools.string-obfuscator.description'),
keywords: ['string', 'obfuscator', 'secret', 'token', 'hide', 'obscure', 'mask', 'masking'], keywords: ['string', 'obfuscator', 'secret', 'token', 'hide', 'obscure', 'mask', 'masking'],
component: () => import('./string-obfuscator.vue'), component: () => import('./string-obfuscator.vue'),
icon: EyeOff, icon: EyeOff,

View file

@ -0,0 +1,10 @@
tools:
string-obfuscator:
title: String obfuscator
description: Obfuscate a string (like a secret, an IBAN, or a token) to make it shareable and identifiable without revealing its content.
inputLabel: 'String to obfuscate:'
inputPlaceholder: Enter string to obfuscate
keepFirst: 'Keep first:'
keepLast: 'Keep last:'
KeepSpaces: 'Keep spaces:'

View file

@ -0,0 +1,10 @@
tools:
string-obfuscator:
title: 字符串混淆器
description: 将字符串如密码、IBAN 或令牌)混淆,使其可共享和识别,而不会暴露内容。
inputLabel: '要混淆的字符串:'
inputPlaceholder: 输入要混淆的字符串
keepFirst: '保留前:'
keepLast: '保留后:'
KeepSpaces: '保留空格:'

View file

@ -2,6 +2,7 @@
import { useObfuscateString } from './string-obfuscator.model'; import { useObfuscateString } from './string-obfuscator.model';
import { useCopy } from '@/composable/copy'; import { useCopy } from '@/composable/copy';
const { t } = useI18n();
const str = ref('Lorem ipsum dolor sit amet'); const str = ref('Lorem ipsum dolor sit amet');
const keepFirst = ref(4); const keepFirst = ref(4);
const keepLast = ref(4); const keepLast = ref(4);
@ -13,22 +14,22 @@ const { copy } = useCopy({ source: obfuscatedString });
<template> <template>
<div> <div>
<c-input-text v-model:value="str" raw-text placeholder="Enter string to obfuscate" label="String to obfuscate:" clearable multiline /> <c-input-text v-model:value="str" raw-text :placeholder="t('tools.string-obfuscator.inputPlaceholder')" :label="t('tools.string-obfuscator.inputLabel')" clearable multiline />
<div mt-4 flex gap-10px> <div mt-4 flex gap-10px>
<div> <div>
<div>Keep first:</div> <div>{{ t('tools.string-obfuscator.keepFirst') }}</div>
<n-input-number v-model:value="keepFirst" min="0" /> <n-input-number v-model:value="keepFirst" min="0" />
</div> </div>
<div> <div>
<div>Keep last:</div> <div>{{ t('tools.string-obfuscator.keepLast') }}</div>
<n-input-number v-model:value="keepLast" min="0" /> <n-input-number v-model:value="keepLast" min="0" />
</div> </div>
<div> <div>
<div mb-5px> <div mb-5px>
Keep&nbsp;spaces: {{ t('tools.string-obfuscator.KeepSpaces') }}
</div> </div>
<n-switch v-model:value="keepSpace" /> <n-switch v-model:value="keepSpace" />
</div> </div>

View file

@ -1,11 +1,11 @@
import { Temperature } from '@vicons/tabler'; import { Temperature } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Temperature converter', name: t('tools.temperature-converter.title'),
path: '/temperature-converter', path: '/temperature-converter',
description: description: t('tools.temperature-converter.description'),
'Temperature degrees conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur and Rømer.',
keywords: [ keywords: [
'temperature', 'temperature',
'converter', 'converter',

View file

@ -0,0 +1,13 @@
tools:
temperature-converter:
title: Temperature converter
description: Temperature degrees conversions for Kelvin, Celsius, Fahrenheit, Rankine, Delisle, Newton, Réaumur and Rømer.
kelvin: Kelvin
celsius: Celsius
fahrenheit: Fahrenheit
rankine: Rankine
delisle: Delisle
newton: Newton
reaumur: Réaumur
romer: Rømer

View file

@ -0,0 +1,13 @@
tools:
temperature-converter:
title: 温度转换器
description: 开尔文、摄氏度、华氏度、兰氏度、德利斯尔度、牛顿度、瑞摄姆度和罗默度的温度转换。
kelvin: 开尔文
celsius: 摄氏度
fahrenheit: 华氏度
rankine: 兰氏度
delisle: 德利斯尔度
newton: 牛顿度
reaumur: 瑞摄姆度
romer: 罗默度

View file

@ -19,6 +19,7 @@ import {
type TemperatureScale = 'kelvin' | 'celsius' | 'fahrenheit' | 'rankine' | 'delisle' | 'newton' | 'reaumur' | 'romer'; type TemperatureScale = 'kelvin' | 'celsius' | 'fahrenheit' | 'rankine' | 'delisle' | 'newton' | 'reaumur' | 'romer';
const { t } = useI18n();
const units = reactive< const units = reactive<
Record< Record<
string | TemperatureScale, string | TemperatureScale,
@ -26,56 +27,56 @@ const units = reactive<
> >
>({ >({
kelvin: { kelvin: {
title: 'Kelvin', title: t('tools.temperature-converter.kelvin'),
unit: 'K', unit: 'K',
ref: 0, ref: 0,
toKelvin: _.identity, toKelvin: _.identity,
fromKelvin: _.identity, fromKelvin: _.identity,
}, },
celsius: { celsius: {
title: 'Celsius', title: t('tools.temperature-converter.celsius'),
unit: '°C', unit: '°C',
ref: 0, ref: 0,
toKelvin: convertCelsiusToKelvin, toKelvin: convertCelsiusToKelvin,
fromKelvin: convertKelvinToCelsius, fromKelvin: convertKelvinToCelsius,
}, },
fahrenheit: { fahrenheit: {
title: 'Fahrenheit', title: t('tools.temperature-converter.fahrenheit'),
unit: '°F', unit: '°F',
ref: 0, ref: 0,
toKelvin: convertFahrenheitToKelvin, toKelvin: convertFahrenheitToKelvin,
fromKelvin: convertKelvinToFahrenheit, fromKelvin: convertKelvinToFahrenheit,
}, },
rankine: { rankine: {
title: 'Rankine', title: t('tools.temperature-converter.rankine'),
unit: '°R', unit: '°R',
ref: 0, ref: 0,
toKelvin: convertRankineToKelvin, toKelvin: convertRankineToKelvin,
fromKelvin: convertKelvinToRankine, fromKelvin: convertKelvinToRankine,
}, },
delisle: { delisle: {
title: 'Delisle', title: t('tools.temperature-converter.delisle'),
unit: '°De', unit: '°De',
ref: 0, ref: 0,
toKelvin: convertDelisleToKelvin, toKelvin: convertDelisleToKelvin,
fromKelvin: convertKelvinToDelisle, fromKelvin: convertKelvinToDelisle,
}, },
newton: { newton: {
title: 'Newton', title: t('tools.temperature-converter.newton'),
unit: '°N', unit: '°N',
ref: 0, ref: 0,
toKelvin: convertNewtonToKelvin, toKelvin: convertNewtonToKelvin,
fromKelvin: convertKelvinToNewton, fromKelvin: convertKelvinToNewton,
}, },
reaumur: { reaumur: {
title: 'Réaumur', title: t('tools.temperature-converter.reaumur'),
unit: '°Ré', unit: '°Ré',
ref: 0, ref: 0,
toKelvin: convertReaumurToKelvin, toKelvin: convertReaumurToKelvin,
fromKelvin: convertKelvinToReaumur, fromKelvin: convertKelvinToReaumur,
}, },
romer: { romer: {
title: 'Rømer', title: t('tools.temperature-converter.romer'),
unit: '°Rø', unit: '°Rø',
ref: 0, ref: 0,
toKelvin: convertRomerToKelvin, toKelvin: convertRomerToKelvin,

View file

@ -1,10 +1,11 @@
import { FileDiff } from '@vicons/tabler'; import { FileDiff } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Text diff', name: t('tools.text-diff.title'),
path: '/text-diff', path: '/text-diff',
description: 'Compare two texts and see the differences between them.', description: t('tools.text-diff.description'),
keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'], keywords: ['text', 'diff', 'compare', 'string', 'text diff', 'code'],
component: () => import('./text-diff.vue'), component: () => import('./text-diff.vue'),
icon: FileDiff, icon: FileDiff,

View file

@ -0,0 +1,4 @@
tools:
text-diff:
title: Text diff
description: Compare two texts and see the differences between them.

View file

@ -0,0 +1,4 @@
tools:
text-diff:
title: 文本差异
description: 比较两个文本并查看它们之间的差异。

View file

@ -1,10 +1,11 @@
import { FileText } from '@vicons/tabler'; import { FileText } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate as t } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Text statistics', name: t('tools.text-statistics.title'),
path: '/text-statistics', path: '/text-statistics',
description: 'Get information about a text, the amount of characters, the amount of words, it\'s size, ...', description: t('tools.text-statistics.description'),
keywords: ['text', 'statistics', 'length', 'characters', 'count', 'size', 'bytes'], keywords: ['text', 'statistics', 'length', 'characters', 'count', 'size', 'bytes'],
component: () => import('./text-statistics.vue'), component: () => import('./text-statistics.vue'),
icon: FileText, icon: FileText,

View file

@ -0,0 +1,10 @@
tools:
text-statistics:
title: Text statistics
description: Get information about a text, the amount of characters, the amount of words, it's size, ...
inputPlaceholder: Your text...
characterCount: Character count
wordCount: Word count
lineCount: Line count
byteSize: Byte size

View file

@ -0,0 +1,10 @@
tools:
text-statistics:
title: 文本统计
description: 获取文本信息,包括字符数、单词数、大小等...
inputPlaceholder: 请输入文本...
characterCount: 字符数
wordCount: 单词数
lineCount: 行数
byteSize: 字节大小

View file

@ -2,18 +2,19 @@
import { getStringSizeInBytes } from './text-statistics.service'; import { getStringSizeInBytes } from './text-statistics.service';
import { formatBytes } from '@/utils/convert'; import { formatBytes } from '@/utils/convert';
const { t } = useI18n();
const text = ref(''); const text = ref('');
</script> </script>
<template> <template>
<c-card> <c-card>
<c-input-text v-model:value="text" multiline placeholder="Your text..." rows="5" /> <c-input-text v-model:value="text" multiline :placeholder="t('tools.text-statistics.inputPlaceholder')" rows="5" />
<div mt-5 flex> <div mt-5 flex>
<n-statistic label="Character count" :value="text.length" flex-1 /> <n-statistic :label="t('tools.text-statistics.characterCount')" :value="text.length" flex-1 />
<n-statistic label="Word count" :value="text === '' ? 0 : text.split(/\s+/).length" flex-1 /> <n-statistic :label="t('tools.text-statistics.wordCount')" :value="text === '' ? 0 : text.split(/\s+/).length" flex-1 />
<n-statistic label="Line count" :value="text === '' ? 0 : text.split(/\r\n|\r|\n/).length" flex-1 /> <n-statistic :label="t('tools.text-statistics.lineCount')" :value="text === '' ? 0 : text.split(/\r\n|\r|\n/).length" flex-1 />
<n-statistic label="Byte size" :value="formatBytes(getStringSizeInBytes(text))" flex-1 /> <n-statistic :label="t('tools.text-statistics.byteSize')" :value="formatBytes(getStringSizeInBytes(text))" flex-1 />
</div> </div>
</c-card> </c-card>
</template> </template>