WIP(translate): translate ulid-generator, encryption, bip39-generator, hmac-generator, rsa-key-pair-generator, password-strength-analyser and pdf-signature-checker tools

This commit is contained in:
Amery2010 2023-12-22 22:42:03 +08:00
parent 70515d32a5
commit 2ee3b01105
30 changed files with 383 additions and 88 deletions

View file

@ -21,6 +21,7 @@ const messages = _.merge(
const i18n = createI18n({ const i18n = createI18n({
legacy: false, legacy: false,
locale: 'en', locale: 'en',
fallbackLocale: 'en',
messages, messages,
}); });
@ -32,6 +33,5 @@ export const i18nPlugin: Plugin = {
export const translate = function (localeKey: string) { export const translate = function (localeKey: string) {
// @ts-expect-error global // @ts-expect-error global
const hasKey = i18n.global.te(localeKey, i18n.global.locale); return i18n.global.t(localeKey);
return hasKey ? i18n.global.t(localeKey) : localeKey;
}; };

View file

@ -36,6 +36,7 @@ const languages = {
const entropy = ref(generateEntropy()); const entropy = ref(generateEntropy());
const passphraseInput = ref(''); const passphraseInput = ref('');
const { t } = useI18n();
const language = ref<keyof typeof languages>('English'); const language = ref<keyof typeof languages>('English');
const passphrase = computed({ const passphrase = computed({
@ -53,11 +54,11 @@ const entropyValidation = useValidation({
rules: [ rules: [
{ {
validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0), validator: value => value === '' || (value.length <= 32 && value.length >= 16 && value.length % 4 === 0),
message: 'Entropy length should be >= 16, <= 32 and be a multiple of 4', message: t('tools.bip39-generator.validation.lengthError'),
}, },
{ {
validator: value => /^[a-fA-F0-9]*$/.test(value), validator: value => /^[a-fA-F0-9]*$/.test(value),
message: 'Entropy should be an hexadecimal string', message: t('tools.bip39-generator.validation.stringTypeError'),
}, },
], ],
}); });
@ -67,7 +68,7 @@ const mnemonicValidation = useValidation({
rules: [ rules: [
{ {
validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])), validator: value => isNotThrowing(() => mnemonicToEntropy(value, languages[language.value])),
message: 'Invalid mnemonic', message: t('tools.bip39-generator.validation.mnemonicError'),
}, },
], ],
}); });
@ -76,8 +77,8 @@ function refreshEntropy() {
entropy.value = generateEntropy(); entropy.value = generateEntropy();
} }
const { copy: copyEntropy } = useCopy({ source: entropy, text: 'Entropy copied to the clipboard' }); const { copy: copyEntropy } = useCopy({ source: entropy, text: t('tools.bip39-generator.copied.entropy') });
const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase copied to the clipboard' }); const { copy: copyPassphrase } = useCopy({ source: passphrase, text: t('tools.bip39-generator.copied.passphrase') });
</script> </script>
<template> <template>
@ -87,18 +88,18 @@ const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase
<c-select <c-select
v-model:value="language" v-model:value="language"
searchable searchable
label="Language:" :label="t('tools.bip39-generator.languageLabel')"
:options="Object.keys(languages)" :options="Object.keys(languages)"
/> />
</n-gi> </n-gi>
<n-gi span="2"> <n-gi span="2">
<n-form-item <n-form-item
label="Entropy (seed):" :label="t('tools.bip39-generator.entropyLabel')"
:feedback="entropyValidation.message" :feedback="entropyValidation.message"
:validation-status="entropyValidation.status" :validation-status="entropyValidation.status"
> >
<n-input-group> <n-input-group>
<c-input-text v-model:value="entropy" placeholder="Your string..." /> <c-input-text v-model:value="entropy" :placeholder="t('tools.bip39-generator.entropyPlaceholder')" />
<c-button @click="refreshEntropy()"> <c-button @click="refreshEntropy()">
<n-icon size="22"> <n-icon size="22">
@ -115,12 +116,12 @@ const { copy: copyPassphrase } = useCopy({ source: passphrase, text: 'Passphrase
</n-gi> </n-gi>
</n-grid> </n-grid>
<n-form-item <n-form-item
label="Passphrase (mnemonic):" :label="t('tools.bip39-generator.passphraseLabel')"
:feedback="mnemonicValidation.message" :feedback="mnemonicValidation.message"
:validation-status="mnemonicValidation.status" :validation-status="mnemonicValidation.status"
> >
<n-input-group> <n-input-group>
<c-input-text v-model:value="passphrase" placeholder="Your mnemonic..." raw-text /> <c-input-text v-model:value="passphrase" :placeholder="t('tools.bip39-generator.passphrasePlaceholder')" raw-text />
<c-button @click="copyPassphrase()"> <c-button @click="copyPassphrase()">
<n-icon size="22" :component="Copy" /> <n-icon size="22" :component="Copy" />

View file

@ -1,10 +1,11 @@
import { AlignJustified } from '@vicons/tabler'; import { AlignJustified } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'BIP39 passphrase generator', name: translate('tools.bip39-generator.title'),
path: '/bip39-generator', path: '/bip39-generator',
description: 'Generate BIP39 passphrase from existing or random mnemonic, or get the mnemonic from the passphrase.', description: translate('tools.bip39-generator.description'),
keywords: ['BIP39', 'passphrase', 'generator', 'mnemonic', 'entropy'], keywords: ['BIP39', 'passphrase', 'generator', 'mnemonic', 'entropy'],
component: () => import('./bip39-generator.vue'), component: () => import('./bip39-generator.vue'),
icon: AlignJustified, icon: AlignJustified,

View file

@ -0,0 +1,18 @@
tools:
bip39-generator:
title: BIP39 passphrase generator
description: Generate BIP39 passphrase from existing or random mnemonic, or get the mnemonic from the passphrase.
languageLabel: 'Language:'
entropyLabel: 'Entropy (seed):'
entropyPlaceholder: Your string...
passphraseLabel: 'Passphrase (mnemonic):'
passphrasePlaceholder: Your mnemonic...
validation:
lengthError: 'Entropy length should be >= 16, <= 32 and be a multiple of 4'
stringTypeError: Entropy should be an hexadecimal string
mnemonicError: Invalid mnemonic
copied:
entropy: Entropy copied to the clipboard
passphrase: Passphrase copied to the clipboard

View file

@ -0,0 +1,18 @@
tools:
bip39-generator:
title: BIP39 助记词生成器
description: 从现有或随机助记词生成 BIP39 助记词,或从助记词获取助记词。
languageLabel: '语言:'
entropyLabel: '熵(种子):'
entropyPlaceholder: 您的字符串...
passphraseLabel: '助记词(口令):'
passphrasePlaceholder: 您的助记词...
validation:
lengthError: '熵的长度应为 >= 16<= 32并且是4的倍数'
stringTypeError: 熵应为十六进制字符串
mnemonicError: 无效的助记词
copied:
entropy: 熵已复制到剪贴板
passphrase: 助记词已复制到剪贴板

View file

@ -4,45 +4,47 @@ import { computedCatch } from '@/composable/computed/catchedComputed';
const algos = { AES, TripleDES, Rabbit, RC4 }; const algos = { AES, TripleDES, Rabbit, RC4 };
const { t } = useI18n();
const cypherInput = ref('Lorem ipsum dolor sit amet'); const cypherInput = ref('Lorem ipsum dolor sit amet');
const cypherAlgo = ref<keyof typeof algos>('AES'); const cypherAlgo = ref<keyof typeof algos>('AES');
const cypherSecret = ref('my secret key'); const cypherSecret = ref('my secret key');
const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString()); const cypherOutput = computed(() => algos[cypherAlgo.value].encrypt(cypherInput.value, cypherSecret.value).toString());
const decryptInput = ref('U2FsdGVkX1/EC3+6P5dbbkZ3e1kQ5o2yzuU0NHTjmrKnLBEwreV489Kr0DIB+uBs'); const decryptInput = ref('U2FsdGVkX18oguRKlUyr7Po25uzcRyz1DVR6+3ULIoQ/gvlXJ+JT+uVJkz+WmSEZ');
const decryptAlgo = ref<keyof typeof algos>('AES'); const decryptAlgo = ref<keyof typeof algos>('AES');
const decryptSecret = ref('my secret key'); const decryptSecret = ref('my secret key');
const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), { const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.value].decrypt(decryptInput.value, decryptSecret.value).toString(enc.Utf8), {
defaultValue: '', defaultValue: '',
defaultErrorMessage: 'Unable to decrypt your text', defaultErrorMessage: t('tools.encryption.errorMessage'),
}); });
</script> </script>
<template> <template>
<c-card title="Encrypt"> <c-card :title="t('tools.encryption.encrypt.title')">
<div flex gap-3> <div flex gap-3>
<c-input-text <c-input-text
v-model:value="cypherInput" v-model:value="cypherInput"
label="Your text:" :label="t('tools.encryption.encrypt.inputLabel')"
placeholder="The string to cypher" :placeholder="t('tools.encryption.encrypt.inputPlaceholder')"
rows="4" rows="4"
multiline raw-text monospace autosize flex-1 multiline raw-text monospace autosize flex-1
/> />
<div flex flex-1 flex-col gap-2> <div flex flex-1 flex-col gap-2>
<c-input-text v-model:value="cypherSecret" label="Your secret key:" clearable raw-text /> <c-input-text v-model:value="cypherSecret" :label="t('tools.encryption.encrypt.secretLabel')" clearable raw-text />
<c-select <c-select
v-model:value="cypherAlgo" v-model:value="cypherAlgo"
label="Encryption algorithm:" :label="t('tools.encryption.encrypt.algoLabel')"
:options="Object.keys(algos).map((label) => ({ label, value: label }))" :options="Object.keys(algos).map((label) => ({ label, value: label }))"
/> />
</div> </div>
</div> </div>
<c-input-text <c-input-text
label="Your text encrypted:" :label="t('tools.encryption.encrypt.outputLabel')"
:value="cypherOutput" :value="cypherOutput"
rows="3" rows="3"
placeholder="Your string hash" :placeholder="t('tools.encryption.encrypt.outputPlaceholder')"
multiline monospace readonly autosize mt-5 multiline monospace readonly autosize mt-5
/> />
</c-card> </c-card>
@ -50,29 +52,29 @@ const [decryptOutput, decryptError] = computedCatch(() => algos[decryptAlgo.valu
<div flex gap-3> <div flex gap-3>
<c-input-text <c-input-text
v-model:value="decryptInput" v-model:value="decryptInput"
label="Your encrypted text:" :label="t('tools.encryption.decrypt.inputLabel')"
placeholder="The string to cypher" :placeholder="t('tools.encryption.decrypt.inputPlaceholder')"
rows="4" rows="4"
multiline raw-text monospace autosize flex-1 multiline raw-text monospace autosize flex-1
/> />
<div flex flex-1 flex-col gap-2> <div flex flex-1 flex-col gap-2>
<c-input-text v-model:value="decryptSecret" label="Your secret key:" clearable raw-text /> <c-input-text v-model:value="decryptSecret" :label="t('tools.encryption.decrypt.secretLabel')" clearable raw-text />
<c-select <c-select
v-model:value="decryptAlgo" v-model:value="decryptAlgo"
label="Encryption algorithm:" :label="t('tools.encryption.decrypt.algoLabel')"
:options="Object.keys(algos).map((label) => ({ label, value: label }))" :options="Object.keys(algos).map((label) => ({ label, value: label }))"
/> />
</div> </div>
</div> </div>
<c-alert v-if="decryptError" type="error" mt-12 title="Error while decrypting"> <c-alert v-if="decryptError" type="error" mt-12 :title="t('tools.encryption.decrypt.decryptError')">
{{ decryptError }} {{ decryptError }}
</c-alert> </c-alert>
<c-input-text <c-input-text
v-else v-else
label="Your decrypted text:" :label="t('tools.encryption.decrypt.outputLabel')"
:value="decryptOutput" :value="decryptOutput"
placeholder="Your string hash" :placeholder="t('tools.encryption.decrypt.outputPlaceholder')"
rows="3" rows="3"
multiline monospace readonly autosize mt-5 multiline monospace readonly autosize mt-5
/> />

View file

@ -1,10 +1,11 @@
import { Lock } from '@vicons/tabler'; import { Lock } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Encrypt / decrypt text', name: translate('tools.encryption.title'),
path: '/encryption', path: '/encryption',
description: 'Encrypt and decrypt text clear text using crypto algorithm like AES, TripleDES, Rabbit or RC4.', description: translate('tools.encryption.description'),
keywords: ['cypher', 'encipher', 'text', 'AES', 'TripleDES', 'Rabbit', 'RC4'], keywords: ['cypher', 'encipher', 'text', 'AES', 'TripleDES', 'Rabbit', 'RC4'],
component: () => import('./encryption.vue'), component: () => import('./encryption.vue'),
icon: Lock, icon: Lock,

View file

@ -0,0 +1,24 @@
tools:
encryption:
title: Encrypt / decrypt text
description: Encrypt and decrypt text clear text using crypto algorithm like AES, TripleDES, Rabbit or RC4.
encrypt:
title: Encrypt
inputLabel: 'Your text:'
inputPlaceholder: The string to cypher
secretLabel: 'Your secret key:'
algoLabel: 'Encryption algorithm:'
outputLabel: 'Your text encrypted:'
outputPlaceholder: Your string hash
decrypt:
title: Decrypt
inputLabel: 'Your encrypted text:'
inputPlaceholder: The string to cypher
secretLabel: 'Your secret key:'
algoLabel: 'Encryption algorithm:'
outputLabel: 'Your decrypted text:'
outputPlaceholder: Your string hash
decryptError: Error while decrypting
errorMessage: Unable to decrypt your text

View file

@ -0,0 +1,24 @@
tools:
encryption:
title: 加密/解密文本
description: 使用 AES、TripleDES、Rabbit 或 RC4 等加密算法对文本进行加密和解密。
encrypt:
title: 加密
inputLabel: '您的文本:'
inputPlaceholder: 要加密的字符串
secretLabel: '您的密钥:'
algoLabel: '加密算法:'
outputLabel: '加密后的文本:'
outputPlaceholder: 您的字符串哈希值
decrypt:
title: 解密
inputLabel: '您的加密文本:'
inputPlaceholder: 要解密的字符串
secretLabel: '您的密钥:'
algoLabel: '加密算法:'
outputLabel: '解密后的文本:'
outputPlaceholder: 您的字符串哈希值
decryptError: 解密时出错
errorMessage: 无法解密您的文本

View file

@ -42,49 +42,50 @@ const encoding = ref<Encoding>('Hex');
const hmac = computed(() => const hmac = computed(() =>
formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value), formatWithEncoding(algos[hashFunction.value](plainText.value, secret.value), encoding.value),
); );
const { t } = useI18n();
const { copy } = useCopy({ source: hmac }); const { copy } = useCopy({ source: hmac });
</script> </script>
<template> <template>
<div flex flex-col gap-4> <div flex flex-col gap-4>
<c-input-text v-model:value="plainText" multiline raw-text placeholder="Plain text to compute the hash..." rows="3" autosize autofocus label="Plain text to compute the hash" /> <c-input-text v-model:value="plainText" multiline raw-text :placeholder="t('tools.hmac-generator.plainText.placeholder')" rows="3" autosize autofocus :label="t('tools.hmac-generator.plainText.label')" />
<c-input-text v-model:value="secret" raw-text placeholder="Enter the secret key..." label="Secret key" clearable /> <c-input-text v-model:value="secret" raw-text :placeholder="t('tools.hmac-generator.secret.placeholder')" :label="t('tools.hmac-generator.secret.label')" clearable />
<div flex gap-2> <div flex gap-2>
<c-select <c-select
v-model:value="hashFunction" label="Hashing function" v-model:value="hashFunction" :label="t('tools.hmac-generator.hashFunction.label')"
flex-1 flex-1
placeholder="Select an hashing function..." :placeholder="t('tools.hmac-generator.hashFunction.placeholder')"
:options="Object.keys(algos).map((label) => ({ label, value: label }))" :options="Object.keys(algos).map((label) => ({ label, value: label }))"
/> />
<c-select <c-select
v-model:value="encoding" label="Output encoding" v-model:value="encoding" :label="t('tools.hmac-generator.encoding.label')"
flex-1 flex-1
placeholder="Select the result encoding..." :placeholder="t('tools.hmac-generator.encoding.placeholder')"
:options="[ :options="[
{ {
label: 'Binary (base 2)', label: t('tools.hmac-generator.encoding.binary'),
value: 'Bin', value: 'Bin',
}, },
{ {
label: 'Hexadecimal (base 16)', label: t('tools.hmac-generator.encoding.hexadecimal'),
value: 'Hex', value: 'Hex',
}, },
{ {
label: 'Base64 (base 64)', label: t('tools.hmac-generator.encoding.base64'),
value: 'Base64', value: 'Base64',
}, },
{ {
label: 'Base64-url (base 64 with url safe chars)', label: t('tools.hmac-generator.encoding.Base64Url'),
value: 'Base64url', value: 'Base64url',
}, },
]" ]"
/> />
</div> </div>
<input-copyable v-model:value="hmac" type="textarea" placeholder="The result of the HMAC..." label="HMAC of your text" /> <input-copyable v-model:value="hmac" type="textarea" :placeholder="t('tools.hmac-generator.result.placeholder')" :label="t('tools.hmac-generator.result.label')" />
<div flex justify-center> <div flex justify-center>
<c-button @click="copy()"> <c-button @click="copy()">
Copy HMAC {{ t('tools.hmac-generator.button.copy') }}
</c-button> </c-button>
</div> </div>
</div> </div>

View file

@ -1,11 +1,11 @@
import { ShortTextRound } from '@vicons/material'; import { ShortTextRound } from '@vicons/material';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Hmac generator', name: translate('tools.hmac-generator.title'),
path: '/hmac-generator', path: '/hmac-generator',
description: description: translate('tools.hmac-generator.description'),
'Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.',
keywords: ['hmac', 'generator', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160'], keywords: ['hmac', 'generator', 'MD5', 'SHA1', 'SHA256', 'SHA224', 'SHA512', 'SHA384', 'SHA3', 'RIPEMD160'],
component: () => import('./hmac-generator.vue'), component: () => import('./hmac-generator.vue'),
icon: ShortTextRound, icon: ShortTextRound,

View file

@ -0,0 +1,26 @@
tools:
hmac-generator:
title: Hmac generator
description: Computes a hash-based message authentication code (HMAC) using a secret key and your favorite hashing function.
plainText:
label: Plain text to compute the hash
placeholder: Plain text to compute the hash...
secret:
label: Secret key
placeholder: Enter the secret key...
hashFunction:
label: Hashing function
placeholder: Select an hashing function...
encoding:
label: Output encoding
placeholder: Select the result encoding...
binary: Binary (base 2)
hexadecimal: Hexadecimal (base 16)
base64: Base64 (base 64)
Base64Url: Base64-url (base 64 with url safe chars)
result:
label: HMAC of your text
placeholder: The result of the HMAC...
button:
copy: Copy HMAC

View file

@ -0,0 +1,26 @@
tools:
hmac-generator:
title: HMAC 生成器
description: 使用密钥和您喜欢的哈希函数计算基于哈希的消息认证码HMAC
plainText:
label: 要计算哈希的明文
placeholder: 要计算哈希的明文...
secret:
label: 密钥
placeholder: 输入密钥...
hashFunction:
label: 哈希函数
placeholder: 选择一个哈希函数...
encoding:
label: 输出编码
placeholder: 选择结果的编码方式...
binary: 二进制基数2
hexadecimal: 十六进制基数16
base64: Base64基数64
Base64Url: Base64-url带有URL安全字符的基数64
result:
label: 您的文本的 HMAC
placeholder: HMAC 的结果...
button:
copy: 复制 HMAC

View file

@ -1,10 +1,11 @@
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import PasswordIcon from '~icons/mdi/form-textbox-password'; import PasswordIcon from '~icons/mdi/form-textbox-password';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'Password strength analyser', name: translate('tools.password-strength-analyser.title'),
path: '/password-strength-analyser', path: '/password-strength-analyser',
description: 'Discover the strength of your password with this client side only password strength analyser and crack time estimation tool.', description: translate('tools.password-strength-analyser.description'),
keywords: ['password', 'strength', 'analyser', 'and', 'crack', 'time', 'estimation', 'brute', 'force', 'attack', 'entropy', 'cracking', 'hash', 'hashing', 'algorithm', 'algorithms', 'md5', 'sha1', 'sha256', 'sha512', 'bcrypt', 'scrypt', 'argon2', 'argon2id', 'argon2i', 'argon2d'], keywords: ['password', 'strength', 'analyser', 'and', 'crack', 'time', 'estimation', 'brute', 'force', 'attack', 'entropy', 'cracking', 'hash', 'hashing', 'algorithm', 'algorithms', 'md5', 'sha1', 'sha256', 'sha512', 'bcrypt', 'scrypt', 'argon2', 'argon2id', 'argon2i', 'argon2d'],
component: () => import('./password-strength-analyser.vue'), component: () => import('./password-strength-analyser.vue'),
icon: PasswordIcon, icon: PasswordIcon,

View file

@ -0,0 +1,38 @@
tools:
password-strength-analyser:
title: Password strength analyser
description: Discover the strength of your password with this client side only password strength analyser and crack time estimation tool.
passwordPlaceholder: Enter a password...
bruteForceDuration: Duration to crack this password with brute force
note: 'Note: '
noteInfor: The computed strength is based on the time it would take to crack the password using a brute force approach, it does not take into account the possibility of a dictionary attack.
details:
length: 'Password length:'
entropy: 'Entropy:'
characterSize: 'Character set size:'
score: 'Score:'
instantly: Instantly
lessThanASecond: Less than a second
millenium: millenium
century: century
decade: decade
year: year
month: month
week: week
day: day
hour: hour
minute: minute
second: second
millennia: millennia
centuries: centuries
decades: decades
years: years
months: months
weeks: weeks
days: days
hours: hours
minutes: minutes
seconds: seconds

View file

@ -0,0 +1,38 @@
tools:
password-strength-analyser:
title: 密码强度分析器
description: 使用此仅在客户端运行的密码强度分析器和破解时间估算工具来了解您的密码强度。
passwordPlaceholder: 输入密码...
bruteForceDuration: 使用暴力破解破解此密码所需时间
note: '注意:'
noteInfor: 计算的强度是基于使用暴力破解方法破解密码所需的时间,不考虑字典攻击的可能性。
details:
length: '密码长度:'
entropy: '熵:'
characterSize: '字符集大小:'
score: '得分:'
instantly: 瞬间
lessThanASecond: 不到一秒钟
millenium: 千年
century: 世纪
decade: 十年
year:
month:
week:
day:
hour: 小时
minute: 分钟
second:
millennia: 千年
centuries: 世纪
decades: 十年
years:
months:
weeks:
days:
hours: 小时
minutes: 分钟
seconds:

View file

@ -1,4 +1,5 @@
import _ from 'lodash'; import _ from 'lodash';
import { translate } from '@/plugins/i18n.plugin';
export { getPasswordCrackTimeEstimation, getCharsetLength }; export { getPasswordCrackTimeEstimation, getCharsetLength };
@ -11,24 +12,24 @@ function prettifyExponentialNotation(exponentialNotation: number) {
function getHumanFriendlyDuration({ seconds }: { seconds: number }) { function getHumanFriendlyDuration({ seconds }: { seconds: number }) {
if (seconds <= 0.001) { if (seconds <= 0.001) {
return 'Instantly'; return translate('tools.password-strength-analyser.instantly');
} }
if (seconds <= 1) { if (seconds <= 1) {
return 'Less than a second'; return translate('tools.password-strength-analyser.lessThanASecond');
} }
const timeUnits = [ const timeUnits = [
{ unit: 'millenium', secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: 'millennia' }, { unit: translate('tools.password-strength-analyser.millenium'), secondsInUnit: 31536000000, format: prettifyExponentialNotation, plural: translate('tools.password-strength-analyser.millennia') },
{ unit: 'century', secondsInUnit: 3153600000, plural: 'centuries' }, { unit: translate('tools.password-strength-analyser.century'), secondsInUnit: 3153600000, plural: translate('tools.password-strength-analyser.centuries') },
{ unit: 'decade', secondsInUnit: 315360000, plural: 'decades' }, { unit: translate('tools.password-strength-analyser.decade'), secondsInUnit: 315360000, plural: translate('tools.password-strength-analyser.decades') },
{ unit: 'year', secondsInUnit: 31536000, plural: 'years' }, { unit: translate('tools.password-strength-analyser.year'), secondsInUnit: 31536000, plural: translate('tools.password-strength-analyser.years') },
{ unit: 'month', secondsInUnit: 2592000, plural: 'months' }, { unit: translate('tools.password-strength-analyser.month'), secondsInUnit: 2592000, plural: translate('tools.password-strength-analyser.months') },
{ unit: 'week', secondsInUnit: 604800, plural: 'weeks' }, { unit: translate('tools.password-strength-analyser.week'), secondsInUnit: 604800, plural: translate('tools.password-strength-analyser.weeks') },
{ unit: 'day', secondsInUnit: 86400, plural: 'days' }, { unit: translate('tools.password-strength-analyser.day'), secondsInUnit: 86400, plural: translate('tools.password-strength-analyser.days') },
{ unit: 'hour', secondsInUnit: 3600, plural: 'hours' }, { unit: translate('tools.password-strength-analyser.hour'), secondsInUnit: 3600, plural: translate('tools.password-strength-analyser.hours') },
{ unit: 'minute', secondsInUnit: 60, plural: 'minutes' }, { unit: translate('tools.password-strength-analyser.minute'), secondsInUnit: 60, plural: translate('tools.password-strength-analyser.minutes') },
{ unit: 'second', secondsInUnit: 1, plural: 'seconds' }, { unit: translate('tools.password-strength-analyser.second'), secondsInUnit: 1, plural: translate('tools.password-strength-analyser.seconds') },
]; ];
return _.chain(timeUnits) return _.chain(timeUnits)

View file

@ -1,24 +1,25 @@
<script setup lang="ts"> <script setup lang="ts">
import { getPasswordCrackTimeEstimation } from './password-strength-analyser.service'; import { getPasswordCrackTimeEstimation } from './password-strength-analyser.service';
const { t } = useI18n();
const password = ref(''); const password = ref('');
const crackTimeEstimation = computed(() => getPasswordCrackTimeEstimation({ password: password.value })); const crackTimeEstimation = computed(() => getPasswordCrackTimeEstimation({ password: password.value }));
const details = computed(() => [ const details = computed(() => [
{ {
label: 'Password length:', label: t('tools.password-strength-analyser.details.length'),
value: crackTimeEstimation.value.passwordLength, value: crackTimeEstimation.value.passwordLength,
}, },
{ {
label: 'Entropy:', label: t('tools.password-strength-analyser.details.entropy'),
value: Math.round(crackTimeEstimation.value.entropy * 100) / 100, value: Math.round(crackTimeEstimation.value.entropy * 100) / 100,
}, },
{ {
label: 'Character set size:', label: t('tools.password-strength-analyser.details.characterSize'),
value: crackTimeEstimation.value.charsetLength, value: crackTimeEstimation.value.charsetLength,
}, },
{ {
label: 'Score:', label: t('tools.password-strength-analyser.details.score'),
value: `${Math.round(crackTimeEstimation.value.score * 100)} / 100`, value: `${Math.round(crackTimeEstimation.value.score * 100)} / 100`,
}, },
]); ]);
@ -29,7 +30,7 @@ const details = computed(() => [
<c-input-text <c-input-text
v-model:value="password" v-model:value="password"
type="password" type="password"
placeholder="Enter a password..." :placeholder="t('tools.password-strength-analyser.passwordPlaceholder')"
clearable clearable
autofocus autofocus
raw-text raw-text
@ -38,7 +39,7 @@ const details = computed(() => [
<c-card text-center> <c-card text-center>
<div op-60> <div op-60>
Duration to crack this password with brute force {{ t('tools.password-strength-analyser.bruteForceDuration') }}
</div> </div>
<div text-2xl data-test-id="crack-duration"> <div text-2xl data-test-id="crack-duration">
{{ crackTimeEstimation.crackDurationFormatted }} {{ crackTimeEstimation.crackDurationFormatted }}
@ -55,8 +56,8 @@ const details = computed(() => [
</div> </div>
</c-card> </c-card>
<div op-70> <div op-70>
<span font-bold>Note: </span> <span font-bold>{{ t('tools.password-strength-analyser.note') }}</span>
The computed strength is based on the time it would take to crack the password using a brute force approach, it does not take into account the possibility of a dictionary attack. {{ t('tools.password-strength-analyser.noteInfor') }}
</div> </div>
</div> </div>
</template> </template>

View file

@ -1,10 +1,11 @@
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import FileCertIcon from '~icons/mdi/file-certificate-outline'; import FileCertIcon from '~icons/mdi/file-certificate-outline';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'PDF signature checker', name: translate('tools.pdf-signature-checker.title'),
path: '/pdf-signature-checker', path: '/pdf-signature-checker',
description: 'Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.', description: translate('tools.pdf-signature-checker.description'),
keywords: ['pdf', 'signature', 'checker', 'verify', 'validate', 'sign'], keywords: ['pdf', 'signature', 'checker', 'verify', 'validate', 'sign'],
component: () => import('./pdf-signature-checker.vue'), component: () => import('./pdf-signature-checker.vue'),
icon: FileCertIcon, icon: FileCertIcon,

View file

@ -0,0 +1,9 @@
tools:
pdf-signature-checker:
title: PDF signature checker
description: Verify the signatures of a PDF file. A signed PDF file contains one or more signatures that may be used to determine whether the contents of the file have been altered since the file was signed.
upload:
tip: Drag and drop a PDF file here, or click to select a file
error: No signatures found in the provided file.
parsed: 'Signature {index} certificates :'

View file

@ -0,0 +1,9 @@
tools:
pdf-signature-checker:
title: PDF 签名检查器
description: 验证 PDF 文件的签名。签名的 PDF 文件包含一个或多个签名,可用于确定文件内容是否自签名后被更改。
upload:
tip: 拖放PDF文件到此处或点击选择文件
error: 在提供的文件中未找到签名。
parsed: '第 {index} 个签名证书:'

View file

@ -7,6 +7,8 @@ const signatures = ref<SignatureInfo[]>([]);
const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle'); const status = ref<'idle' | 'parsed' | 'error' | 'loading'>('idle');
const file = ref<File | null>(null); const file = ref<File | null>(null);
const { t } = useI18n();
async function onVerifyClicked(uploadedFile: File) { async function onVerifyClicked(uploadedFile: File) {
file.value = uploadedFile; file.value = uploadedFile;
const fileBuffer = await uploadedFile.arrayBuffer(); const fileBuffer = await uploadedFile.arrayBuffer();
@ -27,7 +29,7 @@ async function onVerifyClicked(uploadedFile: File) {
<template> <template>
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div mx-auto max-w-600px> <div mx-auto max-w-600px>
<c-file-upload title="Drag and drop a PDF file here, or click to select a file" accept=".pdf" @file-upload="onVerifyClicked" /> <c-file-upload :title="t('tools.pdf-signature-checker.upload.tip')" accept=".pdf" @file-upload="onVerifyClicked" />
<c-card v-if="file" mt-4 flex gap-2> <c-card v-if="file" mt-4 flex gap-2>
<div font-bold> <div font-bold>
@ -41,7 +43,7 @@ async function onVerifyClicked(uploadedFile: File) {
<div v-if="status === 'error'"> <div v-if="status === 'error'">
<c-alert mt-4> <c-alert mt-4>
No signatures found in the provided file. {{ t('tools.pdf-signature-checker.upload.error') }}
</c-alert> </c-alert>
</div> </div>
</div> </div>
@ -50,7 +52,7 @@ async function onVerifyClicked(uploadedFile: File) {
<div v-if="status === 'parsed' && signatures.length" style="flex: 0 0 100%" mt-5 flex flex-col gap-4> <div v-if="status === 'parsed' && signatures.length" style="flex: 0 0 100%" mt-5 flex flex-col gap-4>
<div v-for="(signature, index) of signatures" :key="index"> <div v-for="(signature, index) of signatures" :key="index">
<div mb-2 font-bold> <div mb-2 font-bold>
Signature {{ index + 1 }} certificates : {{ t('tools.pdf-signature-checker.upload.parsed', { index: index + 1 }) }}
</div> </div>
<pdf-signature-details :signature="signature" /> <pdf-signature-details :signature="signature" />

View file

@ -1,10 +1,11 @@
import { Certificate } from '@vicons/tabler'; import { Certificate } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'RSA key pair generator', name: translate('tools.rsa-key-pair-generator.title'),
path: '/rsa-key-pair-generator', path: '/rsa-key-pair-generator',
description: 'Generate new random RSA private and public key pem certificates.', description: translate('tools.rsa-key-pair-generator.description'),
keywords: ['rsa', 'key', 'pair', 'generator', 'public', 'private', 'secret', 'ssh', 'pem'], keywords: ['rsa', 'key', 'pair', 'generator', 'public', 'private', 'secret', 'ssh', 'pem'],
component: () => import('./rsa-key-pair-generator.vue'), component: () => import('./rsa-key-pair-generator.vue'),
icon: Certificate, icon: Certificate,

View file

@ -0,0 +1,12 @@
tools:
rsa-key-pair-generator:
title: RSA key pair generator
description: Generate new random RSA private and public key pem certificates.
bits: 'Bits :'
publicKey: Public key
privateKey: Private key
button:
refresh: Refresh key-pair
validationError: 'Bits should be 256 <= bits <= 16384 and be a multiple of 8'

View file

@ -0,0 +1,12 @@
tools:
rsa-key-pair-generator:
title: RSA 密钥对生成器
description: 生成新的随机 RSA 私钥和公钥 PEM 证书。
bits: '位数:'
publicKey: 公钥
privateKey: 私钥
button:
refresh: 刷新密钥对
validationError: '位数应为256 <= bits <= 16384并且是8的倍数'

View file

@ -8,11 +8,13 @@ import { computedRefreshableAsync } from '@/composable/computedRefreshable';
const bits = ref(2048); const bits = ref(2048);
const emptyCerts = { publicKeyPem: '', privateKeyPem: '' }; const emptyCerts = { publicKeyPem: '', privateKeyPem: '' };
const { t } = useI18n();
const { attrs: bitsValidationAttrs } = useValidation({ const { attrs: bitsValidationAttrs } = useValidation({
source: bits, source: bits,
rules: [ rules: [
{ {
message: 'Bits should be 256 <= bits <= 16384 and be a multiple of 8', message: t('tools.rsa-key-pair-generator.validationError'),
validator: value => value >= 256 && value <= 16384 && value % 8 === 0, validator: value => value >= 256 && value <= 16384 && value % 8 === 0,
}, },
], ],
@ -27,23 +29,23 @@ const [certs, refreshCerts] = computedRefreshableAsync(
<template> <template>
<div style="flex: 0 0 100%"> <div style="flex: 0 0 100%">
<div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3> <div item-style="flex: 1 1 0" style="max-width: 600px" mx-auto flex gap-3>
<n-form-item label="Bits :" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100"> <n-form-item :label="t('tools.rsa-key-pair-generator.bits')" v-bind="bitsValidationAttrs as any" label-placement="left" label-width="100">
<n-input-number v-model:value="bits" min="256" max="16384" step="8" /> <n-input-number v-model:value="bits" min="256" max="16384" step="8" />
</n-form-item> </n-form-item>
<c-button @click="refreshCerts"> <c-button @click="refreshCerts">
Refresh key-pair {{ t('tools.rsa-key-pair-generator.button.refresh') }}
</c-button> </c-button>
</div> </div>
</div> </div>
<div> <div>
<h3>Public key</h3> <h3>{{ t('tools.rsa-key-pair-generator.publicKey') }}</h3>
<TextareaCopyable :value="certs.publicKeyPem" /> <TextareaCopyable :value="certs.publicKeyPem" />
</div> </div>
<div> <div>
<h3>Private key</h3> <h3>{{ t('tools.rsa-key-pair-generator.privateKey') }}</h3>
<TextareaCopyable :value="certs.privateKeyPem" /> <TextareaCopyable :value="certs.privateKeyPem" />
</div> </div>
</template> </template>

View file

@ -1,10 +1,11 @@
import { SortDescendingNumbers } from '@vicons/tabler'; import { SortDescendingNumbers } from '@vicons/tabler';
import { defineTool } from '../tool'; import { defineTool } from '../tool';
import { translate } from '@/plugins/i18n.plugin';
export const tool = defineTool({ export const tool = defineTool({
name: 'ULID generator', name: translate('tools.ulid-generator.title'),
path: '/ulid-generator', path: '/ulid-generator',
description: 'Generate random Universally Unique Lexicographically Sortable Identifier (ULID).', description: translate('tools.ulid-generator.description'),
keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'], keywords: ['ulid', 'generator', 'random', 'id', 'alphanumeric', 'identity', 'token', 'string', 'identifier', 'unique'],
component: () => import('./ulid-generator.vue'), component: () => import('./ulid-generator.vue'),
icon: SortDescendingNumbers, icon: SortDescendingNumbers,

View file

@ -0,0 +1,12 @@
tools:
ulid-generator:
title: ULID generator
description: Generate random Universally Unique Lexicographically Sortable Identifier (ULID).
quantity: 'Quantity: '
format: 'Format: '
copied: ULIDs copied to the clipboard
button:
refresh: Refresh
copy: Copy

View file

@ -0,0 +1,12 @@
tools:
ulid-generator:
title: ULID 生成器
description: 生成随机的通用唯一词典排序标识符ULID
quantity: '数量:'
format: '格式:'
copied: ULID 已复制到剪贴板
button:
refresh: 刷新
copy: 复制

View file

@ -7,6 +7,7 @@ import { useCopy } from '@/composable/copy';
const amount = useStorage('ulid-generator-amount', 1); const amount = useStorage('ulid-generator-amount', 1);
const formats = [{ label: 'Raw', value: 'raw' }, { label: 'JSON', value: 'json' }] as const; const formats = [{ label: 'Raw', value: 'raw' }, { label: 'JSON', value: 'json' }] as const;
const format = useStorage<typeof formats[number]['value']>('ulid-generator-format', formats[0].value); const format = useStorage<typeof formats[number]['value']>('ulid-generator-format', formats[0].value);
const { t } = useI18n();
const [ulids, refreshUlids] = computedRefreshable(() => { const [ulids, refreshUlids] = computedRefreshable(() => {
const ids = _.times(amount.value, () => ulid()); const ids = _.times(amount.value, () => ulid());
@ -18,17 +19,17 @@ const [ulids, refreshUlids] = computedRefreshable(() => {
return ids.join('\n'); return ids.join('\n');
}); });
const { copy } = useCopy({ source: ulids, text: 'ULIDs copied to the clipboard' }); const { copy } = useCopy({ source: ulids, text: t('tools.ulid-generator.copied') });
</script> </script>
<template> <template>
<div flex flex-col justify-center gap-2> <div flex flex-col justify-center gap-2>
<div flex items-center> <div flex items-center>
<label w-75px> Quantity:</label> <label w-75px>{{ t('tools.ulid-generator.quantity') }}</label>
<n-input-number v-model:value="amount" min="1" max="100" flex-1 /> <n-input-number v-model:value="amount" min="1" max="100" flex-1 />
</div> </div>
<c-buttons-select v-model:value="format" :options="formats" label="Format: " label-width="75px" /> <c-buttons-select v-model:value="format" :options="formats" :label="t('tools.ulid-generator.format')" label-width="75px" />
<c-card mt-5 flex data-test-id="ulids"> <c-card mt-5 flex data-test-id="ulids">
<pre m-0 m-x-auto>{{ ulids }}</pre> <pre m-0 m-x-auto>{{ ulids }}</pre>
@ -36,10 +37,10 @@ const { copy } = useCopy({ source: ulids, text: 'ULIDs copied to the clipboard'
<div flex justify-center gap-2> <div flex justify-center gap-2>
<c-button data-test-id="refresh" @click="refreshUlids()"> <c-button data-test-id="refresh" @click="refreshUlids()">
Refresh {{ t('tools.ulid-generator.button.refresh') }}
</c-button> </c-button>
<c-button @click="copy()"> <c-button @click="copy()">
Copy {{ t('tools.ulid-generator.button.copy') }}
</c-button> </c-button>
</div> </div>
</div> </div>