diff --git a/analyze-hardcoded-strings.ts b/analyze-hardcoded-strings.ts new file mode 100644 index 00000000..f0cf1a97 --- /dev/null +++ b/analyze-hardcoded-strings.ts @@ -0,0 +1,181 @@ +#!/usr/bin/env tsx + +/** + * Script to analyze and categorize hardcoded strings for i18n extraction + */ + +import fs from 'fs'; +import path from 'path'; +import { execSync } from 'child_process'; + +interface HardcodedString { + file: string; + line: number; + text: string; + context: string; +} + +interface CategoryStats { + count: number; + files: Set; + examples: string[]; +} + +// Get the hardcoded strings output +console.log('πŸ” Running hardcoded text detection...'); +const output = execSync('npm run i18n:find-hardcoded', { + encoding: 'utf-8', + cwd: process.cwd() +}); + +// Parse the output +const lines = output.split('\n'); +const strings: HardcodedString[] = []; +let currentFile = ''; + +for (const line of lines) { + // Match file headers: πŸ“„ path/to/file.svelte (XX items) + const fileMatch = line.match(/^πŸ“„\s+(.+?)\s+\(\d+\s+items?\)$/); + if (fileMatch) { + currentFile = fileMatch[1]; + continue; + } + + // Match string entries: Line XX: "text" + const stringMatch = line.match(/^\s+Line\s+(\d+):\s+"(.+?)"$/); + if (stringMatch && currentFile) { + const lineNum = parseInt(stringMatch[1]); + const text = stringMatch[2]; + + // Get context from next line if available + const contextMatch = lines[lines.indexOf(line) + 1]?.match(/^\s+Context:\s+(.+)$/); + const context = contextMatch ? contextMatch[1] : ''; + + strings.push({ + file: currentFile, + line: lineNum, + text, + context + }); + } +} + +console.log(`\nπŸ“Š Found ${strings.length} hardcoded strings in ${new Set(strings.map(s => s.file)).size} files`); + +// Categorize strings +const categories = new Map(); + +function addToCategory(category: string, str: HardcodedString) { + if (!categories.has(category)) { + categories.set(category, { + count: 0, + files: new Set(), + examples: [] + }); + } + + const cat = categories.get(category)!; + cat.count++; + cat.files.add(str.file); + + if (cat.examples.length < 5) { + cat.examples.push(str.text); + } +} + +// Categorization logic +for (const str of strings) { + const text = str.text.toLowerCase(); + const file = str.file; + + // Common UI actions + if (['copy', 'copied!', 'save', 'cancel', 'delete', 'edit', 'close', 'back', 'next', 'search', 'clear', 'reset', 'export', 'import', 'download', 'upload', 'refresh', 'apply', 'submit', 'confirm'].some(action => text === action)) { + addToCategory('actions', str); + } + // Loading states + else if (text.includes('loading') || text.includes('calculating') || text.includes('processing') || text.endsWith('...')) { + addToCategory('states', str); + } + // Error messages + else if (text.includes('error') || text.includes('failed') || text.includes('invalid') || text.includes('not found') || text.includes('unknown')) { + addToCategory('errors', str); + } + // Form labels + else if (text.endsWith(':') && text.length < 30) { + addToCategory('labels', str); + } + // Navigation + else if (file.includes('nav') || file.includes('header') || file.includes('sidebar') || text.match(/^(home|about|settings|help|documentation)$/i)) { + addToCategory('navigation', str); + } + // Tool-specific content + else if (file.startsWith('src/routes/') && !file.includes('+layout')) { + const pathParts = file.split('/'); + const toolCategory = pathParts[2]; // diagnostics, dns, cidr, etc. + addToCategory(`tools.${toolCategory}`, str); + } + // Page titles and headings + else if (str.context.includes('

') || str.context.includes('

') || str.context.includes('

') || str.context.includes('')) { + addToCategory('headings', str); + } + // Educational content + else if (text.length > 50 && (text.includes('what is') || text.includes('how to') || text.includes('explanation') || text.includes('about'))) { + addToCategory('educational', str); + } + // Validation messages + else if (text.includes('required') || text.includes('must be') || text.includes('should be') || text.includes('valid')) { + addToCategory('validation', str); + } + // Tooltips and help text + else if (str.context.includes('tooltip') || str.context.includes('title=')) { + addToCategory('tooltips', str); + } + // Component-specific + else if (file.includes('/lib/components/')) { + addToCategory('components', str); + } + // Placeholder text + else if (str.context.includes('placeholder') || text.startsWith('enter ') || text.startsWith('type ')) { + addToCategory('placeholders', str); + } + // Time/date related + else if (text.includes('seconds') || text.includes('minutes') || text.includes('hours') || text.includes('days') || text.includes('date') || text.includes('time')) { + addToCategory('time', str); + } + // API/technical terms + else if (text.includes('api') || text.includes('http') || text.includes('dns') || text.includes('ip') || text.includes('tcp') || text.includes('ssl')) { + addToCategory('technical', str); + } + // Generic content + else { + addToCategory('content', str); + } +} + +// Generate report +console.log('\nπŸ“‹ Category Analysis:'); +console.log('=' .repeat(80)); + +const sortedCategories = Array.from(categories.entries()).sort((a, b) => b[1].count - a[1].count); + +for (const [category, stats] of sortedCategories) { + console.log(`\n${category.toUpperCase()}: ${stats.count} strings in ${stats.files.size} files`); + console.log('Examples:', stats.examples.slice(0, 3).join(', ')); +} + +console.log('\nπŸ“ Files by hardcoded string count:'); +const fileStats = new Map<string, number>(); +for (const str of strings) { + fileStats.set(str.file, (fileStats.get(str.file) || 0) + 1); +} + +const sortedFiles = Array.from(fileStats.entries()).sort((a, b) => b[1] - a[1]); +for (const [file, count] of sortedFiles.slice(0, 20)) { + console.log(` ${count.toString().padStart(3)} - ${file}`); +} + +console.log(`\n🎯 Next steps:`); +console.log(`1. Create translation files for categories with >50 strings`); +console.log(`2. Start with tools.diagnostics (likely largest category)`); +console.log(`3. Extract common strings to reduce duplication`); +console.log(`4. Update source files systematically by category`); \ No newline at end of file diff --git a/analyze-progress.ts b/analyze-progress.ts new file mode 100644 index 00000000..27f4562f --- /dev/null +++ b/analyze-progress.ts @@ -0,0 +1,124 @@ +#!/usr/bin/env tsx + +/** + * Analyze progress on i18n extraction and provide systematic approach + */ + +import { execSync } from 'child_process'; +import fs from 'fs'; +import path from 'path'; + +interface StringCount { + file: string; + count: number; + category: string; +} + +console.log('πŸ” Analyzing hardcoded string extraction progress...\n'); + +// Get current hardcoded strings output +const output = execSync('npm run i18n:find-hardcoded 2>/dev/null', { encoding: 'utf-8' }); +const lines = output.split('\n'); + +// Extract summary info +const summaryMatch = lines.find(line => line.includes('Found') && line.includes('potential hardcoded strings')); +if (summaryMatch) { + const match = summaryMatch.match(/Found (\d+) potential hardcoded strings in (\d+) files/); + if (match) { + console.log(`πŸ“Š Current status: ${match[1]} strings in ${match[2]} files\n`); + } +} + +// Parse files and their string counts +const fileCounts: StringCount[] = []; +const filePattern = /πŸ“„\s+(.+?)\s+\((\d+)\s+items?\)$/; + +for (const line of lines) { + const match = line.match(filePattern); + if (match) { + const filePath = match[1]; + const count = parseInt(match[2]); + + let category = 'Other'; + + if (filePath.includes('/diagnostics/')) { + category = 'Diagnostics'; + } else if (filePath.includes('/dns/')) { + category = 'DNS Tools'; + } else if (filePath.includes('/cidr/')) { + category = 'CIDR Tools'; + } else if (filePath.includes('/subnetting/')) { + category = 'Subnetting'; + } else if (filePath.includes('/ip-address-convertor/')) { + category = 'IP Conversion'; + } else if (filePath.includes('/dhcp/')) { + category = 'DHCP'; + } else if (filePath.includes('/reference/')) { + category = 'Reference'; + } else if (filePath.includes('/components/')) { + category = 'Components'; + } else if (filePath.includes('/routes/')) { + category = 'Pages'; + } + + fileCounts.push({ + file: filePath, + count, + category + }); + } +} + +// Sort by count descending +fileCounts.sort((a, b) => b.count - a.count); + +console.log('πŸ“ˆ Files with most hardcoded strings:'); +console.log('=' .repeat(60)); +for (const item of fileCounts.slice(0, 15)) { + console.log(`${item.count.toString().padStart(3)} strings - ${item.category} - ${item.file.replace('src/routes/', '')}`); +} + +// Categorize by type +const categoryTotals = new Map<string, number>(); +const categoryFiles = new Map<string, number>(); + +for (const item of fileCounts) { + categoryTotals.set(item.category, (categoryTotals.get(item.category) || 0) + item.count); + categoryFiles.set(item.category, (categoryFiles.get(item.category) || 0) + 1); +} + +console.log('\nπŸ“‹ Breakdown by category:'); +console.log('=' .repeat(60)); +for (const [category, total] of Array.from(categoryTotals.entries()).sort((a, b) => b[1] - a[1])) { + const fileCount = categoryFiles.get(category) || 0; + console.log(`${category.padEnd(15)} - ${total.toString().padStart(4)} strings in ${fileCount.toString().padStart(2)} files`); +} + +// Check existing translation files +console.log('\nπŸ“ Existing diagnostic translation files:'); +const diagnosticTranslations = fs.readdirSync( + '/Users/alicia.sykes/Documents/alicia-code/webmono69/src/lib/i18n/translations/en/diagnostics' +).length; +console.log(` ${diagnosticTranslations} translation files already exist`); + +console.log('\n🎯 Recommended next steps:'); +console.log('1. Focus on files with highest string counts (40+ strings)'); +console.log('2. Prioritize diagnostic pages that already have translation files'); +console.log('3. Extract common patterns to common.json'); +console.log('4. Create translation files for high-volume pages without existing translations'); + +// Show top diagnostic files that likely have translations +console.log('\nπŸ”§ High-priority diagnostic pages to process next:'); +const diagnosticFiles = fileCounts + .filter(f => f.category === 'Diagnostics' && f.count > 20) + .slice(0, 8); + +for (const file of diagnosticFiles) { + // Check if translation file exists + const basename = path.basename(path.dirname(file.file)); + const translationPath = `/Users/alicia.sykes/Documents/alicia-code/webmono69/src/lib/i18n/translations/en/diagnostics/${basename}.json`; + const hasTranslation = fs.existsSync(translationPath); + const indicator = hasTranslation ? 'βœ“' : 'βœ—'; + + console.log(` ${indicator} ${file.count.toString().padStart(2)} strings - ${file.file.replace('src/routes/', '')}`); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c4a9d5cb..b196bd0b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "networking-toolbox", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "networking-toolbox", - "version": "1.4.0", + "version": "1.5.0", "devDependencies": { "@axe-core/playwright": "^4.10.2", "@codecov/rollup-plugin": "^1.9.1", @@ -38,6 +38,7 @@ "sass-embedded": "^1.91.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tsx": "^4.20.6", "typescript": "^5.0.0", "typescript-eslint": "^8.44.0", "vite": "^7.0.4", @@ -4418,6 +4419,19 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -5937,6 +5951,16 @@ "node": ">=8" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/rettime": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.7.0.tgz", @@ -7084,6 +7108,41 @@ "dev": true, "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/tsx/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index 4e629a75..3a96e513 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,9 @@ "test:coverage": "vitest run --coverage", "test:api": "vitest run --config tests/vitest.api.config.ts", "test:e2e": "playwright test", + "i18n:check": "tsx scripts/check-translations.ts", + "i18n:check:verbose": "tsx scripts/check-translations.ts --verbose", + "i18n:find-hardcoded": "tsx scripts/find-hardcoded-text.ts", "hold-my-beer": "npm run format && npm run lint && npm run types && npm run check && npm run build-check && npm run test" }, "devDependencies": { @@ -53,6 +56,7 @@ "sass-embedded": "^1.91.0", "svelte": "^5.0.0", "svelte-check": "^4.0.0", + "tsx": "^4.20.6", "typescript": "^5.0.0", "typescript-eslint": "^8.44.0", "vite": "^7.0.4", diff --git a/playwright.config.ts b/playwright.config.ts index 6e0df1b5..0edfccd2 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -6,9 +6,9 @@ export default defineConfig({ forbidOnly: !!process.env.CI, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 1 : undefined, - timeout: 30000, + timeout: 45000, // 45 secs for translation bundle expect: { - timeout: 5000, + timeout: 6000, // 6 sec }, reporter: [ ['github'], @@ -37,7 +37,7 @@ export default defineConfig({ webServer: { command: 'npm run build && npm run preview', port: 4173, - timeout: 120000, + timeout: 180000, // 3 fucking minutes. reuseExistingServer: !process.env.CI, }, }); diff --git a/scripts/check-translations.ts b/scripts/check-translations.ts new file mode 100644 index 00000000..df2b2b32 --- /dev/null +++ b/scripts/check-translations.ts @@ -0,0 +1,356 @@ +#!/usr/bin/env node +/** + * Translation Validation Script + * + * Validates translation files by: + * - Checking for missing keys compared to English (source) + * - Detecting extra keys that don't exist in source + * - Finding empty translation values + * - Reporting coverage statistics + * + * Usage: + * npm run check-translations + * npm run check-translations -- --verbose + * npm run check-translations -- --lang=de + */ + +import { readFileSync, readdirSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const TRANSLATIONS_DIR = join(__dirname, '../src/lib/i18n/translations'); +const SOURCE_LANG = 'en'; + +interface TranslationObject { + [key: string]: string | TranslationObject; +} + +interface ValidationResult { + lang: string; + namespace: string; + missing: string[]; + extra: string[]; + empty: string[]; + total: number; + translated: number; +} + +/** + * Recursively flatten nested object into dot-notation keys + */ +function flattenKeys(obj: TranslationObject, prefix = ''): Record<string, string> { + const result: Record<string, string> = {}; + + for (const [key, value] of Object.entries(obj)) { + // Validate key to prevent prototype pollution + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; + } + + const fullKey = prefix ? `${prefix}.${key}` : key; + + if (value !== null && typeof value === 'object' && !Array.isArray(value)) { + Object.assign(result, flattenKeys(value as TranslationObject, fullKey)); + } else { + // Safe assignment - key already validated above + const safeResult = result; + safeResult[fullKey] = String(value); + } + } + + return result; +} + +/** + * Sanitize path component to prevent path traversal + */ +function sanitizePathComponent(component: string): string { + // Remove any path traversal attempts and keep only safe characters + return component.replace(/[^a-zA-Z0-9_-]/g, ''); +} + +/** + * Load and parse a translation file + */ +function loadTranslation(lang: string, namespace: string): Record<string, string> | null { + // Sanitize inputs to prevent path traversal + const safeLang = sanitizePathComponent(lang); + const safeNamespace = sanitizePathComponent(namespace); + + // Validate sanitized components are not empty + if (!safeLang || !safeNamespace) { + return null; + } + + const filePath = join(TRANSLATIONS_DIR, safeLang, `${safeNamespace}.json`); + + // Verify the constructed path is within TRANSLATIONS_DIR + if (!filePath.startsWith(TRANSLATIONS_DIR)) { + console.error(`Invalid path attempted: ${filePath}`); + return null; + } + + if (!existsSync(filePath)) { + return null; + } + + try { + const content = readFileSync(filePath, 'utf-8'); + const parsed = JSON.parse(content); + return flattenKeys(parsed); + } catch (error) { + console.error(`Error parsing ${filePath}:`, error); + return null; + } +} + +/** + * Get all available namespaces from source language + */ +function getNamespaces(): string[] { + const sourcePath = join(TRANSLATIONS_DIR, SOURCE_LANG); + + if (!existsSync(sourcePath)) { + throw new Error(`Source language directory not found: ${sourcePath}`); + } + + return readdirSync(sourcePath) + .filter(file => file.endsWith('.json')) + .map(file => file.replace('.json', '')); +} + +/** + * Get all available languages + */ +function getLanguages(): string[] { + if (!existsSync(TRANSLATIONS_DIR)) { + throw new Error(`Translations directory not found: ${TRANSLATIONS_DIR}`); + } + + return readdirSync(TRANSLATIONS_DIR, { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name); +} + +/** + * Validate a single language namespace against source + */ +function validateNamespace(lang: string, namespace: string): ValidationResult { + const source = loadTranslation(SOURCE_LANG, namespace); + const target = loadTranslation(lang, namespace); + + if (!source) { + throw new Error(`Source translation not found: ${SOURCE_LANG}/${namespace}.json`); + } + + const result: ValidationResult = { + lang, + namespace, + missing: [], + extra: [], + empty: [], + total: Object.keys(source).length, + translated: 0, + }; + + if (!target) { + // Entire namespace is missing + // codacy-disable-next-line + result.missing = Object.keys(source); + return result; + } + + const sourceKeys = new Set(Object.keys(source)); + const targetKeys = new Set(Object.keys(target)); + + // Find missing keys + for (const key of sourceKeys) { + // Validate key to prevent prototype pollution + if (key === '__proto__' || key === 'constructor' || key === 'prototype') { + continue; + } + + if (!targetKeys.has(key)) { + result.missing.push(key); + } else { + const value = target[key]; + if (value && value.trim() === '') { + result.empty.push(key); + } else if (value) { + result.translated++; + } + } + } + + // Find extra keys + for (const key of targetKeys) { + if (!sourceKeys.has(key)) { + result.extra.push(key); + } + } + + return result; +} + +/** + * Format validation results + */ +function formatResults(results: ValidationResult[], verbose: boolean): string { + let output = ''; + + // Group by language + const byLang = results.reduce((acc, r) => { + // Validate language key to prevent prototype pollution + const lang = r.lang; + if (lang === '__proto__' || lang === 'constructor' || lang === 'prototype') { + return acc; + } + + if (!Object.prototype.hasOwnProperty.call(acc, lang)) { + acc[lang] = []; + } + acc[lang].push(r); + return acc; + }, {} as Record<string, ValidationResult[]>); + + for (const [lang, langResults] of Object.entries(byLang)) { + const totalKeys = langResults.reduce((sum, r) => sum + r.total, 0); + const translatedKeys = langResults.reduce((sum, r) => sum + r.translated, 0); + const missingKeys = langResults.reduce((sum, r) => sum + r.missing.length, 0); + const emptyKeys = langResults.reduce((sum, r) => sum + r.empty.length, 0); + const extraKeys = langResults.reduce((sum, r) => sum + r.extra.length, 0); + const coverage = totalKeys > 0 ? ((translatedKeys / totalKeys) * 100).toFixed(1) : '0.0'; + + output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + output += `πŸ“ Language: ${lang.toUpperCase()}\n`; + output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + output += `Coverage: ${coverage}% (${translatedKeys}/${totalKeys} keys)\n`; + + if (missingKeys > 0) { + output += `❌ Missing: ${missingKeys} keys\n`; + } + if (emptyKeys > 0) { + output += `⚠️ Empty: ${emptyKeys} keys\n`; + } + if (extraKeys > 0) { + output += `πŸ” Extra: ${extraKeys} keys\n`; + } + + if (missingKeys === 0 && emptyKeys === 0 && extraKeys === 0) { + output += `βœ… All translations complete!\n`; + } + + // Detailed breakdown by namespace + if (verbose) { + output += `\nNamespaces:\n`; + for (const result of langResults) { + const nsCoverage = result.total > 0 + ? ((result.translated / result.total) * 100).toFixed(1) + : '0.0'; + + output += ` ${result.namespace}: ${nsCoverage}% (${result.translated}/${result.total})`; + + if (result.missing.length > 0) { + output += ` - Missing: ${result.missing.length}`; + } + if (result.empty.length > 0) { + output += ` - Empty: ${result.empty.length}`; + } + if (result.extra.length > 0) { + output += ` - Extra: ${result.extra.length}`; + } + + output += '\n'; + + // Show missing keys + if (result.missing.length > 0 && result.missing.length <= 10) { + output += ` Missing keys:\n`; + for (const key of result.missing) { + output += ` - ${key}\n`; + } + } else if (result.missing.length > 10) { + output += ` Missing keys (first 10):\n`; + for (const key of result.missing.slice(0, 10)) { + output += ` - ${key}\n`; + } + output += ` ... and ${result.missing.length - 10} more\n`; + } + + // Show empty keys + if (result.empty.length > 0) { + output += ` Empty values:\n`; + for (const key of result.empty) { + output += ` - ${key}\n`; + } + } + + // Show extra keys + if (result.extra.length > 0) { + output += ` Extra keys (not in source):\n`; + for (const key of result.extra) { + output += ` - ${key}\n`; + } + } + } + } + } + + return output; +} + +/** + * Main function + */ +function main() { + const args = process.argv.slice(2); + const verbose = args.includes('--verbose') || args.includes('-v'); + const langFilter = args.find(arg => arg.startsWith('--lang='))?.split('=')[1]; + + console.log('🌍 Translation Validation Script\n'); + console.log(`Source language: ${SOURCE_LANG}`); + console.log(`Translations directory: ${TRANSLATIONS_DIR}\n`); + + try { + const namespaces = getNamespaces(); + const languages = getLanguages().filter(lang => lang !== SOURCE_LANG); + + if (languages.length === 0) { + console.log('No translation languages found.'); + return; + } + + console.log(`Found ${namespaces.length} namespaces: ${namespaces.join(', ')}`); + console.log(`Found ${languages.length} languages: ${languages.join(', ')}\n`); + + const results: ValidationResult[] = []; + + for (const lang of languages) { + if (langFilter && lang !== langFilter) { + continue; + } + + for (const namespace of namespaces) { + const result = validateNamespace(lang, namespace); + results.push(result); + } + } + + const output = formatResults(results, verbose); + console.log(output); + + // Exit with error if there are missing translations + const hasMissing = results.some(r => r.missing.length > 0 || r.empty.length > 0); + if (hasMissing) { + process.exit(1); + } + + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +main(); diff --git a/scripts/extract-to-translations.ts b/scripts/extract-to-translations.ts new file mode 100644 index 00000000..103fcec4 --- /dev/null +++ b/scripts/extract-to-translations.ts @@ -0,0 +1,87 @@ +#!/usr/bin/env node +/** + * Translation Extraction Helper + * Helps identify and extract hardcoded strings from Svelte files + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; + +interface StringMatch { + line: number; + original: string; + suggested Key: string; + context: string; +} + +/** + * Extract hardcoded strings from a file and suggest translation keys + */ +function extractStrings(filePath: string): StringMatch[] { + const content = readFileSync(filePath, 'utf-8'); + const lines = content.split('\n'); + const matches: StringMatch[] = []; + + const patterns = [ + // String in quotes + /'([^']{5,})'/g, + /"([^"]{5,})"/g, + // JSX text content + />([A-Z][^<>{}]{4,})</g, + ]; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i] || ''; + + // Skip translation calls and imports + if (line.includes('$t(') || line.includes('import')) continue; + + for (const pattern of patterns) { + let match; + const regex = new RegExp(pattern); + while ((match = regex.exec(line)) !== null) { + const text = match[1]; + + // Skip if looks like code + if (/^[a-z_]+$/.test(text) || /^\d+$/.test(text)) continue; + + const suggestedKey = text + .toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_|_$/g, '') + .substring(0, 40); + + matches.push({ + line: i + 1, + original: text, + suggestedKey, + context: line.trim().substring(0, 80), + }); + } + } + } + + return matches; +} + +// Example usage +const args = process.argv.slice(2); +if (args.length === 0) { + console.log('Usage: tsx extract-to-translations.ts <file-path>'); + process.exit(1); +} + +const filePath = args[0]; +const matches = extractStrings(filePath); + +console.log(`\nFound ${matches.length} potential strings in ${filePath}\n`); + +for (const match of matches.slice(0, 20)) { + console.log(`Line ${match.line}: "${match.original}"`); + console.log(` Suggested key: ${match.suggestedKey}`); + console.log(` Context: ${match.context}\n`); +} + +if (matches.length > 20) { + console.log(`... and ${matches.length - 20} more`); +} diff --git a/scripts/find-hardcoded-text.ts b/scripts/find-hardcoded-text.ts new file mode 100644 index 00000000..299ecf4a --- /dev/null +++ b/scripts/find-hardcoded-text.ts @@ -0,0 +1,319 @@ +#!/usr/bin/env node +/** + * Hardcoded Text Finder + * + * Scans Svelte files for hardcoded English text that should be moved to translation files. + * Helps identify strings that need internationalization. + * + * Usage: + * npm run find-hardcoded + * npm run find-hardcoded -- --path=src/routes/settings + * npm run find-hardcoded -- --min-length=10 + */ + +import { readFileSync, readdirSync, statSync } from 'fs'; +import { join, relative, dirname, resolve, normalize } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const PROJECT_ROOT = join(__dirname, '..'); +const DEFAULT_SCAN_PATH = join(PROJECT_ROOT, 'src'); + +interface HardcodedText { + file: string; + line: number; + text: string; + context: string; +} + +/** + * Patterns to detect hardcoded text in Svelte files + */ +const TEXT_PATTERNS = [ + // HTML text content: >Text< + />([A-Z][^<>{}\n]{3,})</g, + + // Attribute values: title="Text", placeholder="Text", etc. + /(?:title|placeholder|aria-label|alt|label)=["']([A-Z][^"']{3,})["']/g, + + // String literals in script: 'Text' or "Text" + /['"]([A-Z][A-Za-z\s]{4,})["']/g, +]; + +/** + * Patterns to exclude (these are likely not user-facing text) + */ +const EXCLUDE_PATTERNS = [ + /^\d+$/, // Just numbers + /^[A-Z]+$/, // All caps (likely constants) + /^[a-z-]+$/, // kebab-case (likely CSS/HTML) + /^[A-Z][a-z]+$/, // Single word capitalized (might be component names) + /^\$[a-z]/, // Svelte store references + /^var\(/, // CSS variables + /^#[0-9a-fA-F]{3,6}$/, // Color codes + /^\//, // Paths + /^http/, // URLs + /\{.*\}/, // Template expressions + /^\w+\(/, // Function calls + /^[A-Z][a-z]+[A-Z]/, // PascalCase (component names) +]; + +/** + * Check if text should be excluded + */ +function shouldExclude(text: string): boolean { + const trimmed = text.trim(); + + if (trimmed.length < 3) return true; + if (trimmed.length > 100) return true; // Too long, likely not a translation + + return EXCLUDE_PATTERNS.some(pattern => pattern.test(trimmed)); +} + +/** + * Extract hardcoded text from file content + */ +function findHardcodedText(filePath: string, content: string): HardcodedText[] { + const results: HardcodedText[] = []; + const lines = content.split('\n'); + + for (let i = 0; i < lines.length; i++) { + // Safe array access - i is controlled loop variable within bounds + const line = String(lines[i] || ''); + const lineNumber = i + 1; + + // Skip script/style blocks content (rough heuristic) + // Safe string check - line is a string from split operation + const lowerLine = line.toLowerCase(); + if (lowerLine.includes('<script') || lowerLine.includes('<style')) { + continue; + } + + // Skip lines with translation helpers + if (line.includes('$t(') || line.includes('t(')) { + continue; + } + + // Skip comments + const trimmed = line.trim(); + if (trimmed.startsWith('//') || trimmed.startsWith('/*') || trimmed.startsWith('*')) { + continue; + } + + for (const pattern of TEXT_PATTERNS) { + let match; + while ((match = pattern.exec(line)) !== null) { + const text = match[1]; + + if (!shouldExclude(text)) { + results.push({ + file: relative(PROJECT_ROOT, filePath), + line: lineNumber, + text: text.trim(), + context: line.trim().substring(0, 80), + }); + } + } + } + } + + return results; +} + +/** + * Recursively scan directory for Svelte files + * Only scans within PROJECT_ROOT to prevent path traversal + */ +function scanDirectory(dirPath: string): HardcodedText[] { + let results: HardcodedText[] = []; + + // Verify dirPath is within PROJECT_ROOT + if (!dirPath.startsWith(PROJECT_ROOT)) { + console.error(`Invalid directory path: ${dirPath}`); + return results; + } + + try { + // Safe read - dirPath already validated to be within PROJECT_ROOT + const entries = readdirSync(dirPath, { withFileTypes: true }); + + for (const entry of entries) { + // Sanitize entry name to prevent directory traversal + const safeName = entry.name.replace(/\.\./g, '').replace(/^\/+/, ''); + + // Skip empty names after sanitization + if (!safeName) { + continue; + } + + // Skip node_modules, .svelte-kit, etc. + if (safeName.startsWith('.') || safeName === 'node_modules' || safeName === 'build') { + continue; + } + + // Validate safeName to prevent path traversal + // Allow SvelteKit file naming patterns: +page.svelte, [lang], (sections), etc. + if (safeName.includes('..')) { + console.error(`Unsafe file or directory name blocked: ${safeName}`); + continue; + } + // Construct path and verify it stays within PROJECT_ROOT + const fullPath = join(dirPath, safeName); + if (!fullPath.startsWith(PROJECT_ROOT)) { + console.error(`Path traversal attempt blocked: ${safeName}`); + continue; + } + + if (entry.isDirectory()) { + results = results.concat(scanDirectory(fullPath)); + } else if (entry.isFile() && safeName.endsWith('.svelte')) { + try { + // Safe read - fullPath already validated to be within PROJECT_ROOT + const content = readFileSync(fullPath, 'utf-8'); + const found = findHardcodedText(fullPath, content); + results = results.concat(found); + } catch (error) { + console.error(`Error reading ${fullPath}:`, error); + } + } + } + } catch (error) { + console.error(`Error scanning ${dirPath}:`, error); + } + + return results; +} + +/** + * Group results by file + */ +function groupByFile(results: HardcodedText[]): Map<string, HardcodedText[]> { + const grouped = new Map<string, HardcodedText[]>(); + + for (const result of results) { + if (!grouped.has(result.file)) { + grouped.set(result.file, []); + } + const fileGroup = grouped.get(result.file); + if (fileGroup) { + fileGroup.push(result); + } + } + + return grouped; +} + +/** + * Format results for display + */ +function formatResults(results: HardcodedText[], minLength: number): string { + const filtered = results.filter(r => r.text.length >= minLength); + const grouped = groupByFile(filtered); + + let output = ''; + + output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + output += `πŸ“ Hardcoded Text Finder\n`; + output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + output += `Found ${filtered.length} potential hardcoded strings in ${grouped.size} files\n`; + output += `Minimum length: ${minLength} characters\n\n`; + + if (filtered.length === 0) { + output += `βœ… No hardcoded text found!\n`; + return output; + } + + // Sort files by number of issues + const sortedFiles = Array.from(grouped.entries()) + .sort((a, b) => b[1].length - a[1].length); + + for (const [file, items] of sortedFiles) { + output += `\nπŸ“„ ${file} (${items.length} items)\n`; + output += `${'─'.repeat(60)}\n`; + + for (const item of items) { + output += ` Line ${item.line}: "${item.text}"\n`; + output += ` Context: ${item.context}\n`; + } + } + + output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + output += `Summary: ${filtered.length} strings need translation\n`; + output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`; + + return output; +} + +/** + * Validate and sanitize scan path to prevent directory traversal + * All paths are resolved to be within PROJECT_ROOT + */ +function validateScanPath(userPath: string): string { + // Remove any directory traversal attempts + const sanitized = userPath.replace(/\.\./g, '').replace(/^\/+/, ''); + + // Only allow alphanumeric, dash, underscore, and forward slash + const safePath = sanitized.replace(/[^a-zA-Z0-9/_-]/g, ''); + + if (!safePath) { + throw new Error(`Invalid path: ${userPath} (contains no valid characters)`); + } + + // Build path components safely + const normalized = normalize(safePath); + + // Resolve path by manually joining with PROJECT_ROOT to avoid resolve() warning + const parts = normalized.split('/').filter(p => p && p !== '.'); + let fullPath = PROJECT_ROOT; + + for (const part of parts) { + // Double-check each part doesn't contain traversal + if (part === '..' || part.includes('..')) { + throw new Error(`Invalid path component: ${part}`); + } + fullPath = join(fullPath, part); + } + + // Final verification that path is within PROJECT_ROOT + if (!fullPath.startsWith(PROJECT_ROOT)) { + throw new Error(`Invalid path: ${userPath} (attempts to access outside project root)`); + } + + return fullPath; +} + +/** + * Main function + */ +function main() { + const args = process.argv.slice(2); + const pathArg = args.find(arg => arg.startsWith('--path='))?.split('=')[1]; + const minLengthArg = args.find(arg => arg.startsWith('--min-length='))?.split('=')[1]; + + const scanPath = pathArg ? validateScanPath(pathArg) : DEFAULT_SCAN_PATH; + const minLength = minLengthArg ? parseInt(minLengthArg, 10) : 3; + + console.log('πŸ” Scanning for hardcoded text...\n'); + console.log(`Path: ${relative(PROJECT_ROOT, scanPath)}`); + console.log(`Min length: ${minLength}\n`); + + try { + const results = scanDirectory(scanPath); + const output = formatResults(results, minLength); + console.log(output); + + // Exit with warning if hardcoded text found + if (results.length > 0) { + console.log('\nπŸ’‘ Tip: Move these strings to translation files in src/lib/i18n/translations/'); + process.exit(1); + } + + } catch (error) { + console.error('Error:', error); + process.exit(1); + } +} + +main(); diff --git a/src/lib/components/furniture/BurgerMenu.svelte b/src/lib/components/furniture/BurgerMenu.svelte index 2801f3b4..4fedc67c 100644 --- a/src/lib/components/furniture/BurgerMenu.svelte +++ b/src/lib/components/furniture/BurgerMenu.svelte @@ -32,6 +32,7 @@ import { site, author, license } from '$lib/constants/site'; import { tooltip } from '$lib/actions/tooltip'; import { browser } from '$app/environment'; + import { t } from '$lib/stores/language'; export let isOpen = false; @@ -192,7 +193,7 @@ class="action-button burger-button" class:active={isOpen} on:click={toggleMenu} - aria-label={isOpen ? 'Close menu' : 'Open menu'} + aria-label={isOpen ? $t('furniture.menu.close') : $t('furniture.menu.open')} aria-expanded={isOpen} aria-controls="mobile-menu" use:tooltip={`Menu (${shortcutKey}+M)`} @@ -212,7 +213,7 @@ class="menu-content" class:open={isOpen} id="mobile-menu" - aria-label="Mobile navigation" + aria-label={$t('furniture.navigation.mobile')} bind:this={menuContentElement} > <div class="menu-header"> @@ -220,7 +221,7 @@ <Icon name="networking" size="lg" /> <h2>{site.title}</h2> </a> - <button class="close-button" on:click={() => (isOpen = false)} aria-label="Close menu"> + <button class="close-button" on:click={() => (isOpen = false)} aria-label={$t('furniture.menu.close')}> <Icon name="x" size="md" /> </button> </div> diff --git a/src/lib/components/furniture/Header.svelte b/src/lib/components/furniture/Header.svelte index 15ae1edf..3d5e38b2 100644 --- a/src/lib/components/furniture/Header.svelte +++ b/src/lib/components/furniture/Header.svelte @@ -9,6 +9,7 @@ import ShortcutsDialog from '$lib/components/furniture/ShortcutsDialog.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { formatShortcut } from '$lib/utils/keyboard'; + import { t } from '$lib/stores/language'; let globalSearchRef: GlobalSearch; let shortcutsDialogRef: ShortcutsDialog; @@ -31,7 +32,7 @@ </div> <div> <h1><a href="/">{site.title}</a></h1> - <p class="subtitle">{SITE_DESCRIPTION || "The sysadmin's Swiss Army knife"}</p> + <p class="subtitle">{SITE_DESCRIPTION || $t('furniture.header.subtitle')}</p> </div> </div> @@ -44,7 +45,7 @@ <button class="action-button shortcuts-trigger" onclick={() => shortcutsDialogRef?.showDialog()} - aria-label="Keyboard shortcuts" + aria-label={$t('furniture.header.shortcuts')} use:tooltip={`Shortcuts (${formatShortcut('^/')})`} > <Icon name="cli" size="sm" /> diff --git a/src/lib/components/furniture/QuickTips.svelte b/src/lib/components/furniture/QuickTips.svelte index 1712b05a..6c06b759 100644 --- a/src/lib/components/furniture/QuickTips.svelte +++ b/src/lib/components/furniture/QuickTips.svelte @@ -4,33 +4,27 @@ import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { SHOW_TIPS_ON_HOMEPAGE } from '$lib/config/customizable-settings'; + import { t } from '$lib/stores/language'; - interface Tip { - icon: string; - title: string; - description: string; - shortcut?: string; - } - - const tips: Tip[] = [ + const tips = $derived([ { icon: 'settings', - title: 'Customize the app in the settings', - description: 'Choose your homepage layout, nav links, theme and more', + title: $t('furniture.quick_tips.tips.customize.title'), + description: $t('furniture.quick_tips.tips.customize.description'), shortcut: 'Ctrl + ,', }, { icon: 'bookmarks', - title: 'Bookmark tools for easy access and offline use', - description: 'Just right-click on any tool to bookmark or edit it', + title: $t('furniture.quick_tips.tips.bookmarks.title'), + description: $t('furniture.quick_tips.tips.bookmarks.description'), }, { icon: 'search', - title: 'Use Ctrl + K to quickly search all tools', - description: 'Or, try Ctrl + / to view all shortcuts', + title: $t('furniture.quick_tips.tips.search.title'), + description: $t('furniture.quick_tips.tips.search.description'), shortcut: 'Ctrl + K', }, - ]; + ]); let visible = $state(false); let currentTipIndex = $state(0); @@ -114,11 +108,11 @@ </script> {#if visible} - <div class="quick-tips" role="complementary" aria-label="Quick tips"> + <div class="quick-tips" role="complementary" aria-label={$t('furniture.quick_tips.title')}> <button class="close-btn" onclick={dismissTips} - aria-label="Dismiss tips" + aria-label={$t('furniture.quick_tips.dismiss')} use:tooltip={"Hide, and don't show tips again"} > <Icon name="x" size="sm" /> @@ -142,10 +136,10 @@ </div> <div class="tip-controls"> - <button class="nav-btn" onclick={previousTip} aria-label="Previous tip"> + <button class="nav-btn" onclick={previousTip} aria-label={$t('furniture.quick_tips.previous')}> <Icon name="arrow-left" size="sm" /> </button> - <button class="nav-btn" onclick={nextTip} aria-label="Next tip"> + <button class="nav-btn" onclick={nextTip} aria-label={$t('furniture.quick_tips.next')}> <Icon name="arrow-right" size="sm" /> </button> </div> diff --git a/src/lib/components/furniture/SearchFilter.svelte b/src/lib/components/furniture/SearchFilter.svelte index a953d6f3..9ae261fa 100644 --- a/src/lib/components/furniture/SearchFilter.svelte +++ b/src/lib/components/furniture/SearchFilter.svelte @@ -2,6 +2,7 @@ import { ALL_PAGES, type NavItem } from '$lib/constants/nav'; import Icon from '$lib/components/global/Icon.svelte'; import { debounce } from '$lib/utils/debounce'; + import { t } from '$lib/stores/language'; let { filteredTools = $bindable(), @@ -143,12 +144,12 @@ <input bind:this={searchInput} type="text" - placeholder="Search tools and reference..." + placeholder={$t('furniture.search.placeholder')} class="search-input" value={searchQuery} oninput={handleSearch} /> - <button class="close-search-button" onclick={clearSearch} aria-label="Close search"> + <button class="close-search-button" onclick={clearSearch} aria-label={$t('furniture.search.close')}> <Icon name="x" size="sm" /> </button> </div> @@ -160,7 +161,7 @@ Start typing to search {ALL_PAGES.length} tools </span> {:else if filteredTools.length === 0} - <span class="no-results">No results found</span> + <span class="no-results">{$t('furniture.search.no_results')}</span> {:else} <span class="results-count"> Showing {filteredTools.length} of {ALL_PAGES.length} tools diff --git a/src/lib/components/furniture/SettingsMenu.svelte b/src/lib/components/furniture/SettingsMenu.svelte index d502bfd5..a50d2b9f 100644 --- a/src/lib/components/furniture/SettingsMenu.svelte +++ b/src/lib/components/furniture/SettingsMenu.svelte @@ -8,6 +8,7 @@ import { theme } from '$lib/stores/theme'; import { navbarDisplay } from '$lib/stores/navbarDisplay'; import { homepageLayout } from '$lib/stores/homepageLayout'; + import { t } from '$lib/stores/language'; import SettingsPanel from '$lib/components/furniture/SettingsPanel.svelte'; let isOpen = $state(false); @@ -76,7 +77,7 @@ class="action-button settings-trigger" onclick={() => (isOpen = !isOpen)} ondblclick={handleDoubleClick} - aria-label="Open Settings" + aria-label={$t('furniture.settings_menu.open')} aria-expanded={isOpen} aria-haspopup="menu" use:tooltip={`Settings (${shortcutKey}+,)`} diff --git a/src/lib/components/furniture/SettingsPanel.svelte b/src/lib/components/furniture/SettingsPanel.svelte index b22de330..49b76f9b 100644 --- a/src/lib/components/furniture/SettingsPanel.svelte +++ b/src/lib/components/furniture/SettingsPanel.svelte @@ -13,6 +13,8 @@ import { storage } from '$lib/utils/localStorage'; import * as config from '$lib/config/customizable-settings'; import SegmentedControl from '$lib/components/global/SegmentedControl.svelte'; + import { locale, setLanguage, loadNamespaces, t } from '$lib/stores/language'; + import { SUPPORTED_LANGUAGES } from '$lib/i18n/supported-languages'; interface Props { standalone?: boolean; @@ -25,7 +27,6 @@ let showMoreA11y = $state(standalone); let showMoreThemes = $state(standalone); let showCustomColorInput = $state(false); - let selectedLanguage = $state('en'); // Store subscriptions let accessibilitySettings = $state(accessibility); @@ -73,12 +74,6 @@ '#44aaff', '#7777ff', ]; - const LANGUAGES = [ - { code: 'en', name: 'English', available: true, flag: 'πŸ‡ΊπŸ‡Έ' }, - { code: 'es', name: 'EspaΓ±ol', available: false, flag: 'πŸ‡ͺπŸ‡Έ' }, - { code: 'fr', name: 'FranΓ§ais', available: false, flag: 'πŸ‡«πŸ‡·' }, - { code: 'de', name: 'Deutsch', available: false, flag: 'πŸ‡©πŸ‡ͺ' }, - ]; // Derived state const primaryOptions = $derived( @@ -135,6 +130,14 @@ fontScale.setLevel(parseInt((e.currentTarget as HTMLInputElement).value, 10) as FontScaleLevel), linkClick: () => onClose?.(), + languageChange: async (langCode: string) => { + // Update language store and localStorage + setLanguage(langCode); + + // Load necessary translation namespaces for the new language + await loadNamespaces(langCode, ['common', 'nav', 'settings', 'tools']); + }, + applyCustomCss: () => { const validation = customCss.validate(cssInput); validationErrors = validation.errors; @@ -296,8 +299,8 @@ <div class="settings-panel" class:standalone> <!-- Theme Selection --> <div class="settings-section theme-section"> - <h3>Theme</h3> - <div class="theme-options" role="radiogroup" aria-label="Theme selection"> + <h3>{$t('settings.theme.title')}</h3> + <div class="theme-options" role="radiogroup" aria-label={$t('furniture.settings_panel.aria.theme_selection')}> <!-- Primary themes (always visible - first 6) --> {#each themes.slice(0, 6) as themeOption (themeOption.id)} {@render themeButton(themeOption)} @@ -317,14 +320,14 @@ {#if themes.length > 6 && !standalone} <button class="show-more-btn" onclick={() => (showMoreThemes = !showMoreThemes)} aria-expanded={showMoreThemes}> <Icon name={showMoreThemes ? 'chevron-up' : 'chevron-down'} size="sm" /> - <span>{showMoreThemes ? 'Show less' : 'Show more themes'}</span> + <span>{showMoreThemes ? $t('settings.theme.show_less') : $t('settings.theme.show_more')}</span> </button> {/if} </div> {#if standalone} <div class="settings-section font-size-section"> - <h3>Font Scale</h3> + <h3>{$t('settings.font_scale.title')}</h3> <div class="font-scale-slider"> <input type="range" @@ -333,7 +336,7 @@ step="1" value={$currentFontScale} oninput={handlers.fontScaleChange} - aria-label="Font scale" + aria-label={$t('furniture.settings_panel.aria.font_scale')} class="slider" /> <div class="slider-labels"> @@ -347,15 +350,13 @@ <!-- Language Selection --> <div class="settings-section language-section"> - <h3>Language</h3> + <h3>{$t('settings.language.title')}</h3> <div class="language-dropdown"> - {#each LANGUAGES as lang (lang.code)} + {#each SUPPORTED_LANGUAGES as lang (lang.code)} <button class="language-option" - class:active={selectedLanguage === lang.code} - class:disabled={!lang.available} - onclick={() => (selectedLanguage = lang.code)} - disabled={!lang.available} + class:active={$locale === lang.code} + onclick={() => handlers.languageChange(lang.code)} > {lang.flag} {lang.name} @@ -366,7 +367,7 @@ <!-- Homepage Layout --> <div class="settings-section homepage-layout-section"> - <h3>Homepage Layout</h3> + <h3>{$t('settings.homepage_layout.title')}</h3> <div class="navbar-select-wrapper"> <select class="navbar-select" value={$currentHomepageLayout} onchange={handlers.homepageChange}> {#each homepageLayoutOptions as option (option.id)} @@ -384,7 +385,7 @@ <!-- Navbar Display --> <div class="settings-section navbar-display-section"> - <h3>Top Navigation</h3> + <h3>{$t('settings.top_navigation.title')}</h3> <div class="navbar-select-wrapper"> <select class="navbar-select" value={$currentNavbarDisplay} onchange={handlers.navbarChange}> {#each navbarDisplayOptions as option (option.id)} @@ -402,7 +403,7 @@ <!-- Accessibility Options --> <div class="settings-section accessibility-section"> - <h3>Accessibility</h3> + <h3>{$t('settings.accessibility.title')}</h3> <div class="accessibility-options"> <!-- Primary options (always visible) --> {#each primaryOptions as option (option.id)} @@ -422,7 +423,7 @@ {#if !standalone} <button class="show-more-btn" onclick={() => (showMoreA11y = !showMoreA11y)} aria-expanded={showMoreA11y}> <Icon name={showMoreA11y ? 'chevron-up' : 'chevron-down'} size="sm" /> - <span>{showMoreA11y ? 'Show less' : 'Show all a11y options'}</span> + <span>{showMoreA11y ? $t('settings.accessibility.show_less') : $t('settings.accessibility.show_all')}</span> </button> {/if} </div> @@ -431,36 +432,36 @@ <!-- Site Customization (only in standalone mode) --> {#if standalone} <div class="settings-section site-branding-section"> - <h3>Site Branding</h3> - <p class="section-description">Customize the site title, description, and icon.</p> + <h3>{$t('settings.site_branding.title')}</h3> + <p class="section-description">{$t('settings.site_branding.description')}</p> <div class="form-field"> - <label for="site-title">Site Title</label> + <label for="site-title">{$t('settings.site_branding.site_title')}</label> <input id="site-title" type="text" bind:value={siteTitleInput} - placeholder="Networking Toolbox" + placeholder={$t('furniture.settings_panel.placeholders.site_title')} maxlength="100" /> </div> <div class="form-field"> - <label for="site-description">Description</label> + <label for="site-description">{$t('settings.site_branding.site_description')}</label> <input id="site-description" type="text" bind:value={siteDescriptionInput} - placeholder="Your companion for all-things networking" + placeholder={$t('furniture.settings_panel.placeholders.site_description')} maxlength="300" /> </div> <div class="form-field"> - <label for="site-icon-url">Icon URL</label> + <label for="site-icon-url">{$t('settings.site_branding.site_icon_url')}</label> <input id="site-icon-url" type="text" bind:value={siteIconUrlInput} - placeholder="/favicon.svg or https://example.com/icon.png" + placeholder={$t('furniture.settings_panel.placeholders.site_icon')} /> </div> @@ -470,8 +471,8 @@ <!-- Primary Color (only in standalone mode) --> <div class="settings-section color-section"> - <h3>Primary Color</h3> - <p class="section-description">Choose a primary color for the interface.</p> + <h3>{$t('settings.primary_color.title')}</h3> + <p class="section-description">{$t('settings.primary_color.description')}</p> <div class="color-palette"> {#each COLOR_PALETTE as color (color)} @@ -480,7 +481,7 @@ class:active={primaryColorInput === color} style="background-color: {color};" onclick={() => (primaryColorInput = color)} - aria-label="Select color {color}" + aria-label={$t('furniture.settings_panel.aria.select_color', { color })} ></button> {/each} </div> @@ -499,12 +500,18 @@ {#if showCustomColorInput} <div class="custom-color-inputs" transition:slide={{ duration: 200 }}> <div class="color-picker-wrapper"> - <label for="color-picker">Color Picker</label> + <label for="color-picker">{$t('furniture.settings_panel.color_picker.title')}</label> <input id="color-picker" type="color" bind:value={primaryColorInput} /> </div> <div class="form-field"> <label for="custom-hex">Hex Code</label> - <input id="custom-hex" type="text" bind:value={primaryColorInput} placeholder="#2563eb" maxlength="7" /> + <input + id="custom-hex" + type="text" + bind:value={primaryColorInput} + placeholder={$t('furniture.settings_panel.placeholders.hex_color')} + maxlength="7" + /> </div> </div> {/if} @@ -512,13 +519,13 @@ <!-- Custom CSS (only in standalone mode) --> <div class="settings-section custom-css-section"> - <h3>Custom CSS</h3> - <p class="section-description">Add your own CSS to customize the appearance globally.</p> + <h3>{$t('settings.custom_css.title')}</h3> + <p class="section-description">{$t('settings.custom_css.description')}</p> <textarea bind:value={cssInput} class="css-editor" - placeholder="/* Enter your custom CSS here */" + placeholder={$t('furniture.settings_panel.placeholders.custom_css')} spellcheck="false" rows="5" ></textarea> @@ -534,7 +541,7 @@ {#if standalone} <div class="settings-section info-more-section"> - <h3>Not found what you were looking for?</h3> + <h3>{$t('furniture.settings_panel.not_found.title')}</h3> <p class="line-1">Good news! The code is open source and easy to work with.</p> <p> Simply <a href="https://github.com/Lissy93/networking-toolbox/fork">fork the repo</a>, follow our @@ -549,14 +556,14 @@ {/if} {#if standalone} <div class="settings-section delete-section"> - <h3>Delete Data</h3> + <h3>{$t('settings.delete_data.title')}</h3> <div class="caution-message"> <Icon name="alert-triangle" size="sm" /> - <p>Caution: This will reset all local data.</p> + <p>{$t('settings.delete_data.caution')}</p> </div> <button class="action-btn danger" onclick={handlers.clearAllData}> <Icon name="trash" size="sm" /> - Clear all Data + {$t('settings.delete_data.button')} </button> </div> {/if} @@ -564,7 +571,7 @@ <!-- Docs for saving settings --> {#if standalone} <div class="settings-section saving-section"> - <h3>Syncing Settings and Backup/Restore</h3> + <h3>{$t('settings.sync.title')}</h3> <p class="line-1"> Your settings are saved in your browser's local storage, and so they will be retained even after you quit the app. @@ -581,12 +588,12 @@ aria-expanded={showExportSettings} > <Icon name={showExportSettings ? 'chevron-up' : 'chevron-down'} size="sm" /> - <span>Export Settings</span> + <span>{$t('furniture.settings_panel.export.settings')}</span> </button> {#if showExportSettings} <div class="env-vars-section" transition:slide={{ duration: 300 }}> <div class="env-header"> - <h4>Environment Variables</h4> + <h4>{$t('furniture.settings_panel.export.env_vars_title')}</h4> <SegmentedControl options={[ { value: 'env', label: '.env' }, @@ -603,7 +610,9 @@ <button class="action-btn apply" onclick={handlers.copyEnvVars}> <Icon name={envVarsCopied ? 'check' : 'copy'} size="sm" /> - {envVarsCopied ? 'Copied!' : 'Copy to Clipboard'} + {envVarsCopied + ? $t('furniture.settings_panel.export.copied') + : $t('furniture.settings_panel.export.copy_clipboard')} </button> </div> {/if} @@ -614,14 +623,14 @@ aria-expanded={showExportStyles} > <Icon name={showExportStyles ? 'chevron-up' : 'chevron-down'} size="sm" /> - <span>Export Styles</span> + <span>{$t('furniture.settings_panel.export.styles')}</span> </button> {#if showExportStyles} <div class="env-vars-section" transition:slide={{ duration: 300 }}> <div class="env-header"> - <h4>Custom CSS</h4> + <h4>{$t('furniture.settings_panel.export.custom_css_title')}</h4> </div> - <p class="section-description">Apply your custom CSS to your self-hosted instance by mounting a CSS file.</p> + <p class="section-description">{$t('furniture.settings_panel.export.custom_css_description')}</p> <div class="code-block-section"> <p class="code-label">1. Create a file named <code>custom-styles.css</code></p> @@ -674,7 +683,7 @@ EOF'</code <div class="settings-section settings-links"> <a class="settings-link" href="/settings" onclick={handlers.linkClick}> <Icon name="settings" size="sm" /> - <span>More Settings</span> + <span>{$t('settings.more_settings')}</span> </a> </div> {/if} @@ -949,7 +958,19 @@ EOF'</code grid-template-columns: repeat(auto-fit, minmax(96px, 1fr)); } - .language-option, + .language-option { + &:hover { + background: var(--surface-hover); + color: var(--text-primary); + } + + &.active { + background: color-mix(in srgb, var(--color-primary), transparent 90%); + border-color: var(--color-primary); + color: var(--text-primary); + } + } + .theme-option { &:hover:not(.disabled) { background: var(--surface-hover); diff --git a/src/lib/components/furniture/ShortcutsDialog.svelte b/src/lib/components/furniture/ShortcutsDialog.svelte index b28e83df..a41e8f27 100644 --- a/src/lib/components/furniture/ShortcutsDialog.svelte +++ b/src/lib/components/furniture/ShortcutsDialog.svelte @@ -5,6 +5,7 @@ import { bookmarks } from '$lib/stores/bookmarks'; import { formatShortcut } from '$lib/utils/keyboard'; import SegmentedControl from '$lib/components/global/SegmentedControl.svelte'; + import { t } from '$lib/stores/language'; interface Shortcut { keys: string; @@ -14,16 +15,43 @@ let isOpen = $state(false); - const shortcuts: Shortcut[] = [ - { keys: '^K', description: 'Open search', category: 'Navigation' }, - { keys: '^,', description: 'Open settings', category: 'Navigation' }, - { keys: '^M', description: 'Toggle menu', category: 'Navigation' }, - { keys: '^H', description: 'Go to homepage', category: 'Navigation' }, - // { keys: '^B', description: 'Open bookmarks', category: 'Navigation' }, - { keys: '^/', description: 'Show shortcuts', category: 'Navigation' }, - { keys: '^1-9', description: 'Jump to bookmarked tool', category: 'Bookmarks' }, - { keys: 'Esc', description: 'Close dialogs/clear', category: 'General' }, - ]; + const shortcuts = $derived([ + { + keys: '^K', + description: $t('furniture.shortcuts.actions.open_search'), + category: $t('furniture.shortcuts.categories.navigation'), + }, + { + keys: '^,', + description: $t('furniture.shortcuts.actions.open_settings'), + category: $t('furniture.shortcuts.categories.navigation'), + }, + { + keys: '^M', + description: $t('furniture.shortcuts.actions.toggle_menu'), + category: $t('furniture.shortcuts.categories.navigation'), + }, + { + keys: '^H', + description: $t('furniture.shortcuts.actions.go_home'), + category: $t('furniture.shortcuts.categories.navigation'), + }, + { + keys: '^/', + description: $t('furniture.shortcuts.actions.show_shortcuts'), + category: $t('furniture.shortcuts.categories.navigation'), + }, + { + keys: '^1-9', + description: $t('furniture.shortcuts.actions.jump_bookmark'), + category: $t('furniture.shortcuts.categories.bookmarks'), + }, + { + keys: 'Esc', + description: $t('furniture.shortcuts.actions.close_dialogs'), + category: $t('furniture.shortcuts.categories.general'), + }, + ]); function openDialog() { isOpen = true; @@ -104,15 +132,19 @@ return groups; }); - const viewOptions = [ - { label: 'Keyboard Shortcuts', value: 'keyboard-shortcuts', icon: 'keyboard' }, - { label: 'About', value: 'about', icon: 'info' }, - ]; + const viewOptions = $derived([ + { label: $t('furniture.shortcuts.view_options.shortcuts'), value: 'keyboard-shortcuts', icon: 'keyboard' }, + { label: $t('furniture.shortcuts.view_options.about'), value: 'about', icon: 'info' }, + ]); let activeView = $state('keyboard-shortcuts'); // Compute dialog title based on active view - const dialogTitle = $derived(activeView === 'keyboard-shortcuts' ? 'Command Palette' : 'Networking Toolbox'); + const dialogTitle = $derived( + activeView === 'keyboard-shortcuts' + ? $t('furniture.shortcuts.command_palette') + : $t('furniture.shortcuts.app_title'), + ); // Close dialog when clicking links in About tab function handleLinkClick() { @@ -137,7 +169,7 @@ /> </div> </div> - <button class="close-btn" onclick={close} aria-label="Close shortcuts"> + <button class="close-btn" onclick={close} aria-label={$t('furniture.shortcuts.close')}> <Icon name="x" size="sm" /> </button> </div> @@ -157,9 +189,13 @@ </ul> <!-- Show bookmarked tools if in Bookmarks category --> - {#if category === 'Bookmarks' && $bookmarks.length > 0} + {#if category === $t('furniture.shortcuts.categories.bookmarks') && $bookmarks.length > 0} <details class="bookmarks-details"> - <summary>Your bookmarked tools ({Math.min($bookmarks.length, 9)})</summary> + <summary + >{$t('furniture.shortcuts.bookmarks.your_tools', { + count: Math.min($bookmarks.length, 9), + })}</summary + > <ul class="bookmarks-list"> {#each $bookmarks.slice(0, 10) as bookmark, index (bookmark.href)} <li> @@ -170,21 +206,21 @@ {#if $bookmarks.length <= 9} <li> <kbd>{formatShortcut('^0')}</kbd> - <span>Homepage</span> + <span>{$t('furniture.shortcuts.bookmarks.homepage')}</span> </li> {/if} <li> <kbd>{formatShortcut('^B')}</kbd> - <span>View all Bookmarks</span> + <span>{$t('furniture.shortcuts.bookmarks.view_all')}</span> </li> </ul> </details> {/if} - {#if category === 'Bookmarks' && $bookmarks.length === 0} + {#if category === $t('furniture.shortcuts.categories.bookmarks') && $bookmarks.length === 0} <p class="no-bookmarks-tip"> - <i>You don't have any bookmarks yet.</i> - <span>Right-click on a tool to bookmark it for quick access and offline use.</span> + <i>{$t('furniture.shortcuts.bookmarks.empty')}</i> + <span>{$t('furniture.shortcuts.bookmarks.help')}</span> </p> {/if} </div> @@ -192,27 +228,25 @@ </div> {:else if activeView === 'about'} <div class="about-content"> - <p> - Networking Toolbox is an open-source collection of web-based networking tools designed to make - network-related tasks quicker and easier. - </p> - <p> - With 100+ tools, it's privacy-focused and self-hostable, fully customizable, and includes a free REST API - for automation. - </p> + <p>{$t('furniture.shortcuts.about.description_1')}</p> + <p>{$t('furniture.shortcuts.about.description_2')}</p> <ul> - <li><a href="https://github.com/lissy93/networking-toolbox" onclick={handleLinkClick}>GitHub</a></li> - <li><a href="/sitemap" onclick={handleLinkClick}>Page Listing</a></li> - <li><a href="/settings" onclick={handleLinkClick}>App Settings</a></li> - <li><a href="/about" onclick={handleLinkClick}>Documentation</a></li> - <li><a href="/about/support" onclick={handleLinkClick}>Support</a></li> - <li><a href="/about/legal" onclick={handleLinkClick}>Legal</a></li> + <li> + <a href="https://github.com/lissy93/networking-toolbox" onclick={handleLinkClick} + >{$t('furniture.shortcuts.about.github')}</a + > + </li> + <li><a href="/sitemap" onclick={handleLinkClick}>{$t('furniture.shortcuts.about.page_listing')}</a></li> + <li><a href="/settings" onclick={handleLinkClick}>{$t('furniture.shortcuts.about.app_settings')}</a></li> + <li><a href="/about" onclick={handleLinkClick}>{$t('furniture.shortcuts.about.documentation')}</a></li> + <li><a href="/about/support" onclick={handleLinkClick}>{$t('furniture.shortcuts.about.support')}</a></li> + <li><a href="/about/legal" onclick={handleLinkClick}>{$t('furniture.shortcuts.about.legal')}</a></li> </ul> <p class="sponsor"> <Icon name="heart" size="md" /> <span> - <b>Finding Networking Toolbox useful?</b> - Consider <a href="https://github.com/sponsors/Lissy93">sponsoring us on GitHub</a> to support ongoing development! + <b>{$t('furniture.shortcuts.about.sponsor_heading')}</b> + {$t('furniture.shortcuts.about.sponsor_text')} </span> </p> @@ -221,9 +255,11 @@ href="https://github.com/lissy93/networking-toolbox" target="_blank" rel="noopener" - onclick={handleLinkClick}>Networking Toolbox</a + onclick={handleLinkClick}>{$t('furniture.shortcuts.app_title')}</a > - v{import.meta.env.VITE_APP_VERSION}, licensed under + {$t('furniture.shortcuts.about.version', { version: import.meta.env.VITE_APP_VERSION })}, {$t( + 'furniture.shortcuts.about.license', + )} <a href="https://opensource.org/licenses/MIT" target="_blank" rel="noopener" onclick={handleLinkClick} >MIT</a > @@ -494,10 +530,6 @@ align-items: center; gap: var(--spacing-sm); margin: var(--spacing-md) auto; - a { - color: var(--color-pink); - text-decoration: underline; - } opacity: 0; animation: fadeIn 1s ease-out 10s forwards; } diff --git a/src/lib/components/furniture/TopNav.svelte b/src/lib/components/furniture/TopNav.svelte index fbf72078..fb693459 100644 --- a/src/lib/components/furniture/TopNav.svelte +++ b/src/lib/components/furniture/TopNav.svelte @@ -7,6 +7,7 @@ import { bookmarks } from '$lib/stores/bookmarks'; import { frequentlyUsedTools, toolUsage } from '$lib/stores/toolUsage'; import { onMount } from 'svelte'; + import { t } from '$lib/stores/language'; const currentPath = $derived($page.url?.pathname ?? '/'); @@ -118,7 +119,7 @@ .slice(0, 8) .map((tool) => ({ href: tool.href, - label: tool.label || 'Untitled Tool', + label: tool.label || $t('furniture.navigation.untitled_tool'), icon: tool.icon, description: tool.description, })); @@ -142,7 +143,7 @@ }); </script> -<nav id="navigation" class="top-nav" class:has-dropdowns={hasDropdowns} aria-label="Primary navigation"> +<nav id="navigation" class="top-nav" class:has-dropdowns={hasDropdowns} aria-label={$t('furniture.navigation.primary')}> {#each navigationItems as item (item.href)} <div class="nav-item" diff --git a/src/lib/components/tools/CAABuilder.svelte b/src/lib/components/tools/CAABuilder.svelte index daf320dc..e3383b66 100644 --- a/src/lib/components/tools/CAABuilder.svelte +++ b/src/lib/components/tools/CAABuilder.svelte @@ -2,6 +2,7 @@ import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { SvelteSet } from 'svelte/reactivity'; + import { t } from '$lib/stores/language'; interface CAARecord { flag: number; @@ -34,16 +35,16 @@ { name: 'Cloudflare', value: 'comodoca.com' }, ]; - const tagDescriptions = { - issue: 'Authorize certificate issuance for this domain', - issuewild: 'Authorize wildcard certificate issuance for this domain', - iodef: 'Contact information for certificate abuse reports', - }; + const tagDescriptions = $derived({ + issue: $t('tools/caa-builder.tags.issue'), + issuewild: $t('tools/caa-builder.tags.issuewild'), + iodef: $t('tools/caa-builder.tags.iodef'), + }); - const _flagDescriptions = { - 0: 'Non-critical flag - unknown tags can be ignored', - 128: 'Critical flag - unknown tags must cause rejection', - }; + const _flagDescriptions = $derived({ + 0: $t('tools/caa-builder.flags.descriptions.0'), + 128: $t('tools/caa-builder.flags.descriptions.128'), + }); const caaRecords = $derived.by(() => { return records @@ -71,14 +72,14 @@ // Check domain format if (!domain.trim()) { - errors.push('Domain is required'); + errors.push($t('tools/caa-builder.validation.errors.domainRequired')); } else if (!domain.includes('.')) { - warnings.push('Domain should include TLD (e.g., .com, .org)'); + warnings.push($t('tools/caa-builder.validation.warnings.domainNoTLD')); } // Check if any records are enabled if (enabledRecords.length === 0) { - warnings.push('No CAA records enabled - this will not provide any protection'); + warnings.push($t('tools/caa-builder.validation.warnings.noRecordsEnabled')); } // Check for issue records @@ -86,12 +87,12 @@ const issuewildRecords = enabledRecords.filter((r) => r.tag === 'issuewild'); if (issueRecords.length === 0 && issuewildRecords.length === 0) { - warnings.push('No issue or issuewild records - certificates can be issued by any CA'); + warnings.push($t('tools/caa-builder.validation.warnings.noIssueRecords')); } // Check for wildcard without base issue if (issuewildRecords.length > 0 && issueRecords.length === 0) { - warnings.push('Wildcard authorization without base domain authorization may cause issues'); + warnings.push($t('tools/caa-builder.validation.warnings.wildcardWithoutBase')); } // Check for deny-all configuration @@ -99,7 +100,7 @@ const hasIssuewildNone = issuewildRecords.some((r) => r.value.trim() === ';'); if (hasIssueNone && hasIssuewildNone) { - warnings.push('Both issue and issuewild set to ";" - this will block ALL certificate issuance'); + warnings.push($t('tools/caa-builder.validation.warnings.denyAll')); } // Validate iodef records @@ -110,10 +111,10 @@ if (value.includes('@')) { // Email format if (!value.includes('@') || (!value.startsWith('mailto:') && value.indexOf('@') === -1)) { - errors.push('Invalid iodef email format - use "mailto:user@domain.com" or "user@domain.com"'); + errors.push($t('tools/caa-builder.validation.errors.invalidIodefEmail')); } } else if (!value.startsWith('http://') && !value.startsWith('https://')) { - warnings.push('iodef URL should start with http:// or https://'); + warnings.push($t('tools/caa-builder.validation.warnings.iodefURLFormat')); } } } @@ -131,7 +132,11 @@ } if (duplicateValues.size > 0) { - warnings.push(`Duplicate CAA records found: ${Array.from(duplicateValues).join(', ')}`); + warnings.push( + $t('tools/caa-builder.validation.warnings.duplicateRecords', { + duplicates: Array.from(duplicateValues).join(', '), + }), + ); } return { @@ -189,10 +194,10 @@ showButtonSuccess('export-caa'); } - const exampleConfigurations = [ + const exampleConfigurations = $derived([ { - name: "Let's Encrypt Only", - description: "Allow only Let's Encrypt certificates", + name: $t('tools/caa-builder.examples.letsEncryptOnly.name'), + description: $t('tools/caa-builder.examples.letsEncryptOnly.description'), domain: 'example.com', records: [ { flag: 0, tag: 'issue' as const, value: 'letsencrypt.org', enabled: true }, @@ -200,8 +205,8 @@ ], }, { - name: 'Multiple CAs', - description: 'Allow certificates from multiple providers', + name: $t('tools/caa-builder.examples.multipleCAs.name'), + description: $t('tools/caa-builder.examples.multipleCAs.description'), domain: 'mycompany.com', records: [ { flag: 0, tag: 'issue' as const, value: 'letsencrypt.org', enabled: true }, @@ -211,8 +216,8 @@ ], }, { - name: 'No Certificates', - description: 'Block all certificate issuance', + name: $t('tools/caa-builder.examples.noCertificates.name'), + description: $t('tools/caa-builder.examples.noCertificates.description'), domain: 'secure.example.com', records: [ { flag: 0, tag: 'issue' as const, value: ';', enabled: true }, @@ -220,7 +225,7 @@ { flag: 0, tag: 'iodef' as const, value: 'security@example.com', enabled: true }, ], }, - ]; + ]); function loadExample(example: (typeof exampleConfigurations)[0]): void { domain = example.domain; @@ -244,21 +249,14 @@ showExamples = false; } - const securityTips = [ - 'Start with monitoring: Add iodef records first to receive notifications', - 'Use specific CAs: Only authorize certificate authorities you actually use', - 'Include wildcards: Add issuewild records if you use wildcard certificates', - 'Monitor regularly: Check iodef notifications for unauthorized issuance attempts', - 'Test thoroughly: Verify legitimate certificate renewals still work after deployment', - ]; + const securityTips = $derived($t('tools/caa-builder.securityGuide.tips')); </script> <div class="card"> <div class="card-header"> - <h1>CAA Record Builder</h1> + <h1>{$t('tools/caa-builder.title')}</h1> <p class="card-subtitle"> - Build CAA (Certificate Authority Authorization) records to control which CAs can issue certificates for your - domain. + {$t('tools/caa-builder.description')} </p> </div> @@ -268,13 +266,15 @@ <div class="section-header"> <h3> <Icon name="globe" size="sm" /> - Domain Configuration + {$t('tools/caa-builder.domain.title')} </h3> </div> <div class="input-group"> - <label for="domain" use:tooltip={'Domain to create CAA records for'}> Domain: </label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('tools/caa-builder.domain.tooltip')}> + {$t('tools/caa-builder.domain.label')} + </label> + <input id="domain" type="text" bind:value={domain} placeholder={$t('tools/caa-builder.domain.placeholder')} /> </div> </div> @@ -282,35 +282,35 @@ <div class="section-header"> <h3> <Icon name="shield" size="sm" /> - CAA Records + {$t('tools/caa-builder.records.title')} </h3> <div class="add-buttons"> <button type="button" class="add-btn" onclick={() => addRecord('issue')} - use:tooltip={'Add certificate issuance authorization'} + use:tooltip={$t('tools/caa-builder.records.addButtons.issue.tooltip')} > <Icon name="plus" size="sm" /> - Issue + {$t('tools/caa-builder.records.addButtons.issue.label')} </button> <button type="button" class="add-btn" onclick={() => addRecord('issuewild')} - use:tooltip={'Add wildcard certificate issuance authorization'} + use:tooltip={$t('tools/caa-builder.records.addButtons.issuewild.tooltip')} > <Icon name="plus" size="sm" /> - Wildcard + {$t('tools/caa-builder.records.addButtons.issuewild.label')} </button> <button type="button" class="add-btn" onclick={() => addRecord('iodef')} - use:tooltip={'Add incident reporting contact'} + use:tooltip={$t('tools/caa-builder.records.addButtons.iodef.tooltip')} > <Icon name="plus" size="sm" /> - Contact + {$t('tools/caa-builder.records.addButtons.iodef.label')} </button> </div> </div> @@ -330,8 +330,8 @@ <div class="record-controls"> <div class="flag-select"> <select bind:value={record.flag} disabled={!record.enabled}> - <option value={0}>Flag 0 (Non-critical)</option> - <option value={128}>Flag 128 (Critical)</option> + <option value={0}>{$t('tools/caa-builder.flags.nonCritical')}</option> + <option value={128}>{$t('tools/caa-builder.flags.critical')}</option> </select> </div> @@ -339,7 +339,7 @@ type="button" class="remove-btn" onclick={() => removeRecord(index)} - use:tooltip={'Remove this record'} + use:tooltip={$t('tools/caa-builder.records.removeTooltip')} > <Icon name="x" size="sm" /> </button> @@ -352,16 +352,16 @@ bind:value={record.value} disabled={!record.enabled} placeholder={record.tag === 'issue' - ? 'letsencrypt.org or ; (to deny all)' + ? $t('tools/caa-builder.placeholders.issue') : record.tag === 'issuewild' - ? 'letsencrypt.org or ; (to deny all)' - : 'security@example.com or https://example.com/security'} + ? $t('tools/caa-builder.placeholders.issuewild') + : $t('tools/caa-builder.placeholders.iodef')} class="record-input" /> {#if (record.tag === 'issue' || record.tag === 'issuewild') && record.enabled} <div class="ca-shortcuts"> - <span class="shortcuts-label">Common CAs:</span> + <span class="shortcuts-label">{$t('tools/caa-builder.caShortcuts.label')}</span> <div class="ca-buttons"> {#each commonCAs.slice(0, 4) as ca (ca.name)} <button @@ -377,9 +377,9 @@ type="button" class="ca-btn deny-all" onclick={() => addCA(index, ';')} - use:tooltip={'Deny all certificate issuance'} + use:tooltip={$t('tools/caa-builder.caShortcuts.denyAllTooltip')} > - Deny All + {$t('tools/caa-builder.caShortcuts.denyAll')} </button> </div> </div> @@ -394,29 +394,33 @@ <div class="results-section"> <div class="records-output-section"> <div class="section-header"> - <h3>Generated CAA Records</h3> + <h3>{$t('tools/caa-builder.output.title')}</h3> <div class="actions"> <button type="button" class="copy-btn" class:success={buttonStates['copy-caa']} onclick={() => copyToClipboard(caaRecords.join('\n'), 'copy-caa')} - use:tooltip={'Copy all CAA records to clipboard'} + use:tooltip={$t('tools/caa-builder.output.copyTooltip')} disabled={caaRecords.length === 0} > <Icon name={buttonStates['copy-caa'] ? 'check' : 'copy'} size="sm" /> - {buttonStates['copy-caa'] ? 'Copied!' : 'Copy'} + {buttonStates['copy-caa'] + ? $t('tools/caa-builder.output.copied') + : $t('tools/caa-builder.output.copyButton')} </button> <button type="button" class="export-btn" class:success={buttonStates['export-caa']} onclick={exportAsZoneFile} - use:tooltip={'Download as zone file'} + use:tooltip={$t('tools/caa-builder.output.exportTooltip')} disabled={caaRecords.length === 0} > <Icon name={buttonStates['export-caa'] ? 'check' : 'download'} size="sm" /> - {buttonStates['export-caa'] ? 'Downloaded!' : 'Export'} + {buttonStates['export-caa'] + ? $t('tools/caa-builder.output.downloaded') + : $t('tools/caa-builder.output.exportButton')} </button> </div> </div> @@ -432,7 +436,7 @@ {:else} <div class="no-records"> <Icon name="info" size="sm" /> - <span>Enable and configure CAA records to see output</span> + <span>{$t('tools/caa-builder.output.noRecords')}</span> </div> {/if} </div> @@ -441,19 +445,21 @@ <div class="section-header"> <h3> <Icon name="bar-chart" size="sm" /> - Policy Validation + {$t('tools/caa-builder.validation.title')} </h3> </div> <div class="validation-stats"> <div class="stat-item"> - <span class="stat-label">Active Records:</span> + <span class="stat-label">{$t('tools/caa-builder.validation.activeRecordsLabel')}</span> <span class="stat-value">{validation.recordCount}</span> </div> <div class="stat-item"> - <span class="stat-label">Status:</span> + <span class="stat-label">{$t('tools/caa-builder.validation.statusLabel')}</span> <span class="stat-value" class:success={validation.isValid} class:error={!validation.isValid}> - {validation.isValid ? 'Valid' : 'Invalid'} + {validation.isValid + ? $t('tools/caa-builder.validation.valid') + : $t('tools/caa-builder.validation.invalid')} </span> </div> </div> @@ -483,7 +489,7 @@ {#if validation.isValid && validation.errors.length === 0 && validation.warnings.length === 0} <div class="validation-messages success"> <Icon name="check-circle" size="sm" /> - <div class="message">CAA configuration is valid and ready to deploy!</div> + <div class="message">{$t('tools/caa-builder.validation.success')}</div> </div> {/if} </div> @@ -492,7 +498,7 @@ <div class="section-header"> <h3> <Icon name="info" size="sm" /> - Security Tips + {$t('tools/caa-builder.securityGuide.title')} </h3> </div> @@ -511,7 +517,7 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Example Configurations + {$t('tools/caa-builder.examples.title')} </summary> <div class="examples-grid"> {#each exampleConfigurations as example (example.name)} diff --git a/src/lib/components/tools/CIDRAlignment.svelte b/src/lib/components/tools/CIDRAlignment.svelte index 9be19f9e..c4447162 100644 --- a/src/lib/components/tools/CIDRAlignment.svelte +++ b/src/lib/components/tools/CIDRAlignment.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { checkCIDRAlignment, type AlignmentResult } from '$lib/utils/cidr-alignment.js'; import { tooltip } from '$lib/actions/tooltip.js'; + import { t } from '$lib/stores/language'; import Icon from '$lib/components/global/Icon.svelte'; import '../../../styles/diagnostics-pages.scss'; @@ -14,23 +15,23 @@ let _userModified = $state(false); let validationErrors = $state<string[]>([]); - const examples = [ + const examples = $derived([ { - label: 'Basic IPv4 Alignment', + label: $t('tools/cidr-alignment.examples.basicIPv4'), input: `192.168.1.0/24 192.168.2.0/24 192.168.3.0/24`, targetPrefix: 22, }, { - label: 'Mixed IP Types', + label: $t('tools/cidr-alignment.examples.mixedIPTypes'), input: `10.0.0.0-10.0.0.255 172.16.5.100 192.168.1.0/25`, targetPrefix: 24, }, { - label: 'Subnet Aggregation Check', + label: $t('tools/cidr-alignment.examples.subnetAggregation'), input: `192.168.0.0/26 192.168.0.64/26 192.168.0.128/26 @@ -38,7 +39,7 @@ targetPrefix: 24, }, { - label: 'Network Consolidation', + label: $t('tools/cidr-alignment.examples.networkConsolidation'), input: `10.1.0.0/24 10.1.1.0/24 10.1.2.0/24 @@ -46,7 +47,7 @@ targetPrefix: 22, }, { - label: 'VLAN Alignment Check', + label: $t('tools/cidr-alignment.examples.vlanAlignment'), input: `172.16.10.0/24 172.16.11.0/24 172.16.15.0/24 @@ -54,27 +55,27 @@ targetPrefix: 20, }, { - label: 'Point-to-Point Links', + label: $t('tools/cidr-alignment.examples.pointToPointLinks'), input: `192.168.100.0/30 192.168.100.4/30 192.168.100.8/30 192.168.100.12/30`, targetPrefix: 28, }, - ]; + ]); function validateTargetPrefix(): string[] { const errors: string[] = []; // Check if target prefix is a valid number if (isNaN(targetPrefix) || targetPrefix === null || targetPrefix === undefined) { - errors.push('Target prefix length must be a valid number'); + errors.push($t('tools/cidr-alignment.validation.mustBeValidNumber')); return errors; } // Check if target prefix is within basic bounds if (targetPrefix < 0 || targetPrefix > 128) { - errors.push('Target prefix length must be between 0 and 128'); + errors.push($t('tools/cidr-alignment.validation.outOfRange')); return errors; } @@ -104,16 +105,16 @@ // Validate prefix length based on IP types present if (hasIPv4 && !hasIPv6 && targetPrefix > 32) { - errors.push('Target prefix length cannot exceed 32 for IPv4 addresses'); + errors.push($t('tools/cidr-alignment.validation.ipv4OutOfRange')); } if (hasIPv6 && targetPrefix > 128) { - errors.push('Target prefix length cannot exceed 128 for IPv6 addresses'); + errors.push($t('tools/cidr-alignment.validation.ipv6OutOfRange')); } // Additional practical validation if (targetPrefix === 0) { - errors.push('Target prefix length of 0 is not practical for alignment checking'); + errors.push($t('tools/cidr-alignment.validation.notPractical')); } return errors; @@ -225,8 +226,8 @@ <div class="card"> <header class="card-header"> - <h2>CIDR Boundary Alignment</h2> - <p>Check if IP addresses, ranges, and CIDR blocks align to specific prefix boundaries</p> + <h2>{$t('tools/cidr-alignment.title')}</h2> + <p>{$t('tools/cidr-alignment.description')}</p> </header> <!-- Examples --> @@ -234,7 +235,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-alignment.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -245,7 +246,7 @@ > <div class="example-label">{example.label}</div> <div class="example-preview"> - Target: /{example.targetPrefix} + {$t('tools/cidr-alignment.input.targetPreview', { targetPrefix: example.targetPrefix })} </div> </button> {/each} @@ -255,29 +256,28 @@ <div class="input-section"> <div class="inputs-card"> - <h3 use:tooltip={'Enter IP addresses, CIDR blocks, or ranges to check alignment'}>Network Inputs</h3> + <h3 use:tooltip={$t('tools/cidr-alignment.input.networkInputsTooltip')}> + {$t('tools/cidr-alignment.input.networkInputsTitle')} + </h3> <div class="input-group"> - <label for="inputs" use:tooltip={'Enter one per line: CIDR blocks, IP ranges, or individual IP addresses'}> - IP Addresses, CIDRs, or Ranges + <label for="inputs" use:tooltip={$t('tools/cidr-alignment.input.inputsTooltip')}> + {$t('tools/cidr-alignment.input.inputsLabel')} </label> <textarea id="inputs" bind:value={inputText} oninput={handleInputChange} - placeholder="192.168.1.0/24 10.0.0.0-10.0.0.255 172.16.1.5 2001:db8::/32" + placeholder={$t('tools/cidr-alignment.input.inputsPlaceholder')} rows="8" ></textarea> <div class="input-help"> - Enter one per line: CIDR blocks (192.168.1.0/24), IP ranges (10.0.0.1-10.0.0.100), or single IPs (172.16.1.5) + {$t('tools/cidr-alignment.input.inputHelp')} </div> </div> <div class="input-group"> - <label - for="prefix" - use:tooltip={'The prefix length boundary to check alignment against (e.g., 24 for /24 boundaries)'} - > - Target Prefix Length + <label for="prefix" use:tooltip={$t('tools/cidr-alignment.input.targetPrefixTooltip')}> + {$t('tools/cidr-alignment.input.targetPrefixLabel')} </label> <input id="prefix" @@ -286,10 +286,10 @@ oninput={handleInputChange} min="0" max="128" - placeholder="24" + placeholder={$t('tools/cidr-alignment.input.targetPrefixPlaceholder')} class:error={validationErrors.length > 0} /> - <div class="input-help">Prefix length to check alignment against (0-32 for IPv4, 0-128 for IPv6)</div> + <div class="input-help">{$t('tools/cidr-alignment.input.targetPrefixHelp')}</div> {#if validationErrors.length > 0} <div class="validation-errors"> {#each validationErrors as error (error)} @@ -307,7 +307,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Checking alignment... + {$t('tools/cidr-alignment.loading')} </div> {/if} @@ -315,7 +315,7 @@ <div class="results"> {#if result.errors.length > 0} <div class="errors"> - <h3><Icon name="alert-triangle" /> Errors</h3> + <h3><Icon name="alert-triangle" /> {$t('tools/cidr-alignment.results.errorsTitle')}</h3> {#each result.errors as error (error)} <div class="error-item">{error}</div> {/each} @@ -324,26 +324,32 @@ {#if result.checks.length > 0} <div class="summary"> - <h3 use:tooltip={'Overview of alignment results across all inputs'}>Alignment Summary</h3> + <h3 use:tooltip={$t('tools/cidr-alignment.results.summaryTooltip')}> + {$t('tools/cidr-alignment.results.summaryTitle')} + </h3> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{result.summary.totalInputs}</span> - <span class="stat-label" use:tooltip={'Total number of network inputs processed'}>Total Inputs</span> + <span class="stat-label" use:tooltip={$t('tools/cidr-alignment.results.totalInputsTooltip')} + >{$t('tools/cidr-alignment.results.totalInputsLabel')}</span + > </div> <div class="stat aligned"> <span class="stat-value">{result.summary.alignedInputs}</span> - <span class="stat-label" use:tooltip={'Networks that align to the target prefix boundary'}>Aligned</span> + <span class="stat-label" use:tooltip={$t('tools/cidr-alignment.results.alignedTooltip')} + >{$t('tools/cidr-alignment.results.alignedLabel')}</span + > </div> <div class="stat misaligned"> <span class="stat-value">{result.summary.misalignedInputs}</span> - <span class="stat-label" use:tooltip={'Networks that do not align to the target prefix boundary'} - >Misaligned</span + <span class="stat-label" use:tooltip={$t('tools/cidr-alignment.results.misalignedTooltip')} + >{$t('tools/cidr-alignment.results.misalignedLabel')}</span > </div> <div class="stat"> <span class="stat-value">{result.summary.alignmentRate}%</span> - <span class="stat-label" use:tooltip={'Percentage of inputs that align to the target boundary'} - >Alignment Rate</span + <span class="stat-label" use:tooltip={$t('tools/cidr-alignment.results.alignmentRateTooltip')} + >{$t('tools/cidr-alignment.results.alignmentRateLabel')}</span > </div> </div> @@ -351,15 +357,17 @@ <div class="checks"> <div class="checks-header"> - <h3 use:tooltip={'Detailed results for each network input'}>Alignment Checks</h3> + <h3 use:tooltip={$t('tools/cidr-alignment.results.checksTooltip')}> + {$t('tools/cidr-alignment.results.checksTitle')} + </h3> <div class="export-buttons"> <button onclick={() => exportResults('csv')}> <Icon name="csv-file" /> - Export CSV + {$t('tools/cidr-alignment.results.exportCSV')} </button> <button onclick={() => exportResults('json')}> <Icon name="json-file" /> - Export JSON + {$t('tools/cidr-alignment.results.exportJSON')} </button> </div> </div> @@ -375,20 +383,22 @@ <div class="check-status"> {#if check.isAligned} <Icon name="check-circle" size="sm" /> - <span class="status-text">Aligned to /{check.targetPrefix}</span> + <span class="status-text" + >{$t('tools/cidr-alignment.check.alignedTo', { targetPrefix: check.targetPrefix })}</span + > {:else} <Icon name="x-circle" size="sm" /> - <span class="status-text">Not aligned to /{check.targetPrefix}</span> + <span class="status-text" + >{$t('tools/cidr-alignment.check.notAlignedTo', { targetPrefix: check.targetPrefix })}</span + > {/if} </div> </div> {#if check.alignedCIDR} <div class="aligned-cidr"> - <span - class="aligned-label" - use:tooltip={'The CIDR block that properly aligns to the target prefix boundary'} - >Aligned CIDR:</span + <span class="aligned-label" use:tooltip={$t('tools/cidr-alignment.check.alignedCIDRTooltip')} + >{$t('tools/cidr-alignment.check.alignedCIDRLabel')}</span > <div class="cidr-with-copy"> <code class="aligned-code">{check.alignedCIDR}</code> @@ -396,7 +406,7 @@ type="button" class="copy-button {copiedStates[`cidr-${check.input}`] ? 'copied' : ''}" onclick={() => copyToClipboard(check.alignedCIDR!, `cidr-${check.input}`)} - use:tooltip={'Copy aligned CIDR to clipboard'} + use:tooltip={$t('tools/cidr-alignment.check.copyAlignedCIDR')} > <Icon name={copiedStates[`cidr-${check.input}`] ? 'check' : 'copy'} size="xs" /> </button> @@ -406,8 +416,8 @@ {#if check.reason} <div class="reason"> - <span class="reason-label" use:tooltip={"Explanation of why this input aligns or doesn't align"} - >Reason:</span + <span class="reason-label" use:tooltip={$t('tools/cidr-alignment.check.reasonTooltip')} + >{$t('tools/cidr-alignment.check.reasonLabel')}</span > <span class="reason-text">{check.reason}</span> </div> @@ -415,10 +425,8 @@ {#if check.suggestions.length > 0} <div class="suggestions"> - <span - class="suggestions-label" - use:tooltip={'Alternative CIDR configurations that would align to the target boundary'} - >Suggestions:</span + <span class="suggestions-label" use:tooltip={$t('tools/cidr-alignment.check.suggestionsTooltip')} + >{$t('tools/cidr-alignment.check.suggestionsLabel')}</span > {#each check.suggestions as suggestion (suggestion.type + suggestion.description)} <div class="suggestion"> @@ -440,7 +448,7 @@ type="button" class="copy-button {copiedStates[`suggestion-${check.input}-${idx}`] ? 'copied' : ''}" onclick={() => copyToClipboard(cidr, `suggestion-${check.input}-${idx}`)} - use:tooltip={'Copy suggested CIDR to clipboard'} + use:tooltip={$t('tools/cidr-alignment.check.copySuggestedCIDR')} > <Icon name={copiedStates[`suggestion-${check.input}-${idx}`] ? 'check' : 'copy'} @@ -453,9 +461,9 @@ {#if suggestion.efficiency} <div class="suggestion-efficiency" - use:tooltip={'Address space utilization efficiency of this suggestion'} + use:tooltip={$t('tools/cidr-alignment.check.efficiencyTooltip')} > - Efficiency: {suggestion.efficiency}% + {$t('tools/cidr-alignment.check.efficiency', { efficiency: suggestion.efficiency })} </div> {/if} </div> diff --git a/src/lib/components/tools/CIDRAllocator.svelte b/src/lib/components/tools/CIDRAllocator.svelte index d992e8d2..688f8bb8 100644 --- a/src/lib/components/tools/CIDRAllocator.svelte +++ b/src/lib/components/tools/CIDRAllocator.svelte @@ -3,6 +3,7 @@ import { useClipboard } from '$lib/composables'; import Icon from '$lib/components/global/Icon.svelte'; import { formatNumber } from '$lib/utils/formatters'; + import { t } from '$lib/stores/language'; import '../../../styles/diagnostics-pages.scss'; let pools = $state(`192.168.0.0/16 10.0.0.0/20`); @@ -442,8 +443,8 @@ <div class="card"> <header class="card-header"> - <h2>CIDR Allocator</h2> - <p>Pack requested subnet sizes into pools using intelligent bin-packing algorithms</p> + <h2>{$t('tools/cidr-allocator.title')}</h2> + <p>{$t('tools/cidr-allocator.description')}</p> </header> <!-- Examples --> @@ -451,7 +452,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-allocator.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -462,7 +463,7 @@ > <div class="example-label">{example.label}</div> <div class="example-preview"> - Algorithm: {example.algorithm} + {$t('tools/cidr-allocator.examples.algorithm', { algorithm: example.algorithm })} </div> </button> {/each} @@ -472,8 +473,8 @@ <!-- Algorithm Selection --> <section class="algorithm-section"> - <h4 use:tooltip={'Choose between first-fit (faster) and best-fit (more efficient packing) algorithms'}> - Allocation Algorithm + <h4 use:tooltip={$t('tools/cidr-allocator.algorithm.titleTooltip')}> + {$t('tools/cidr-allocator.algorithm.title')} </h4> <div class="algorithm-options"> <label class="algorithm-option"> @@ -481,10 +482,10 @@ <div class="option-content"> <div class="option-title"> <Icon name="zap" size="sm" /> - First-Fit + {$t('tools/cidr-allocator.algorithm.firstFit.title')} </div> <div class="option-description"> - Fast allocation - uses the first available block that fits (good for speed) + {$t('tools/cidr-allocator.algorithm.firstFit.description')} </div> </div> </label> @@ -494,10 +495,10 @@ <div class="option-content"> <div class="option-title"> <Icon name="target" size="sm" /> - Best-Fit + {$t('tools/cidr-allocator.algorithm.bestFit.title')} </div> <div class="option-description"> - Optimal packing - uses the smallest available block that fits (reduces fragmentation) + {$t('tools/cidr-allocator.algorithm.bestFit.description')} </div> </div> </label> @@ -508,30 +509,30 @@ <section class="input-section"> <div class="input-grid"> <div class="input-group"> - <label for="pools" use:tooltip={'Available network pools - one CIDR block per line'}> + <label for="pools" use:tooltip={$t('tools/cidr-allocator.input.poolsTooltip')}> <Icon name="database" size="sm" /> - Available Pools + {$t('tools/cidr-allocator.input.poolsLabel')} </label> <textarea id="pools" bind:value={pools} oninput={handleInputChange} - placeholder="192.168.0.0/16 10.0.0.0/20" + placeholder={$t('tools/cidr-allocator.input.poolsPlaceholder')} rows="6" required ></textarea> </div> <div class="input-group"> - <label for="requests" use:tooltip={"Subnet requests in format '/24 - Description' - one per line"}> + <label for="requests" use:tooltip={$t('tools/cidr-allocator.input.requestsTooltip')}> <Icon name="list-check" size="sm" /> - Subnet Requests + {$t('tools/cidr-allocator.input.requestsLabel')} </label> <textarea id="requests" bind:value={requests} oninput={handleInputChange} - placeholder="/24 - Main Office /26 - Servers /28 - Management" + placeholder={$t('tools/cidr-allocator.input.requestsPlaceholder')} rows="6" required ></textarea> @@ -546,14 +547,18 @@ <!-- Summary --> <div class="allocation-summary"> <div class="summary-header"> - <h3 use:tooltip={'Summary of subnet allocation requests and pool utilization'}>Allocation Results</h3> + <h3 use:tooltip={$t('tools/cidr-allocator.results.titleTooltip')}> + {$t('tools/cidr-allocator.results.title')} + </h3> {#if result.summary.successfulAllocations > 0} <button class="copy-all-button {clipboard.isCopied('all-allocations') ? 'copied' : ''}" onclick={copyAllAllocations} > <Icon name={clipboard.isCopied('all-allocations') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('all-allocations') ? 'Copied!' : 'Copy All'} + {clipboard.isCopied('all-allocations') + ? $t('tools/cidr-allocator.results.copied') + : $t('tools/cidr-allocator.results.copyAll')} </button> {/if} </div> @@ -565,7 +570,7 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.successfulAllocations}</div> - <div class="summary-label">Allocated</div> + <div class="summary-label">{$t('tools/cidr-allocator.summary.allocated')}</div> </div> </div> @@ -575,7 +580,7 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.failedAllocations}</div> - <div class="summary-label">Failed</div> + <div class="summary-label">{$t('tools/cidr-allocator.summary.failed')}</div> </div> </div> @@ -585,28 +590,30 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.efficiency.toFixed(1)}%</div> - <div class="summary-label">Efficiency</div> + <div class="summary-label">{$t('tools/cidr-allocator.summary.efficiency')}</div> </div> </div> </div> <div class="space-breakdown"> <div class="breakdown-item"> - <span class="breakdown-label">Total Pool Space:</span> + <span class="breakdown-label">{$t('tools/cidr-allocator.summary.totalPoolSpace')}</span> <span class="breakdown-value"> - {formatNumber(result.summary.totalPoolSpace)} addresses + {$t('tools/cidr-allocator.summary.addresses', { count: formatNumber(result.summary.totalPoolSpace) })} </span> </div> <div class="breakdown-item"> - <span class="breakdown-label">Allocated:</span> + <span class="breakdown-label">{$t('tools/cidr-allocator.summary.allocatedSpace')}</span> <span class="breakdown-value allocated"> - {formatNumber(result.summary.allocatedSpace)} addresses + {$t('tools/cidr-allocator.summary.addresses', { count: formatNumber(result.summary.allocatedSpace) })} </span> </div> <div class="breakdown-item"> - <span class="breakdown-label">Remaining:</span> + <span class="breakdown-label">{$t('tools/cidr-allocator.summary.remaining')}</span> <span class="breakdown-value"> - {formatNumber(result.summary.totalPoolSpace - result.summary.allocatedSpace)} addresses + {$t('tools/cidr-allocator.summary.addresses', { + count: formatNumber(result.summary.totalPoolSpace - result.summary.allocatedSpace), + })} </span> </div> </div> @@ -614,7 +621,9 @@ <!-- Allocations --> <div class="allocations-section"> - <h4 use:tooltip={'Individual subnet allocation results with assigned CIDR blocks'}>Subnet Allocations</h4> + <h4 use:tooltip={$t('tools/cidr-allocator.allocations.titleTooltip')}> + {$t('tools/cidr-allocator.allocations.title')} + </h4> <div class="allocations-list"> {#each result.allocations as allocation (allocation.request)} <div class="allocation-item {allocation.allocated ? 'success' : 'failed'}"> @@ -626,10 +635,10 @@ <div class="allocation-status"> {#if allocation.allocated} <Icon name="check-circle" size="sm" /> - <span class="status-text success">Allocated</span> + <span class="status-text success">{$t('tools/cidr-allocator.allocations.allocated')}</span> {:else} <Icon name="x-circle" size="sm" /> - <span class="status-text failed">Failed</span> + <span class="status-text failed">{$t('tools/cidr-allocator.allocations.failed')}</span> {/if} </div> </div> @@ -638,9 +647,9 @@ <div class="allocation-result"> <div class="result-info"> <code class="result-cidr">{allocation.cidr}</code> - <span class="result-pool">in {allocation.pool}</span> + <span class="result-pool">{$t('tools/cidr-allocator.allocations.in')} {allocation.pool}</span> <span class="result-size"> - ({formatNumber(allocation.size)} addresses) + ({$t('tools/cidr-allocator.summary.addresses', { count: formatNumber(allocation.size) })}) </span> </div> <button @@ -653,7 +662,7 @@ {:else if allocation.reason} <div class="allocation-reason failed"> <Icon name="alert-triangle" size="xs" /> - {allocation.reason} + {$t('tools/cidr-allocator.allocations.noSuitableBlock')} </div> {/if} </div> @@ -663,7 +672,7 @@ <!-- Pool Utilization --> <div class="pools-section"> - <h4 use:tooltip={'Detailed breakdown of how address space was used in each pool'}>Pool Utilization</h4> + <h4 use:tooltip={$t('tools/cidr-allocator.pools.titleTooltip')}>{$t('tools/cidr-allocator.pools.title')}</h4> <div class="pools-grid"> {#each result.pools as pool (pool.original)} <div class="pool-card"> @@ -676,7 +685,7 @@ class:medium={pool.utilization >= 50 && pool.utilization < 80} class:low={pool.utilization < 50} > - {pool.utilization.toFixed(1)}% used + {$t('tools/cidr-allocator.pools.used', { percent: pool.utilization.toFixed(1) })} </span> </div> </div> @@ -689,7 +698,7 @@ <!-- Allocated Subnets --> {#if pool.allocated.length > 0} <div class="pool-allocations"> - <h5>Allocated Subnets</h5> + <h5>{$t('tools/cidr-allocator.pools.allocatedSubnets')}</h5> <div class="allocated-list"> {#each pool.allocated as subnet (subnet.cidr)} <div class="allocated-subnet"> @@ -704,12 +713,12 @@ <!-- Remaining Space --> {#if pool.remaining.length > 0} <div class="pool-remaining"> - <h5>Available Space</h5> + <h5>{$t('tools/cidr-allocator.pools.availableSpace')}</h5> <div class="remaining-list"> {#each pool.remaining.slice(0, 5) as remaining (remaining.cidr)} <div class="remaining-block"> <code class="remaining-size"> - {formatNumber(remaining.size)} addresses + {$t('tools/cidr-allocator.summary.addresses', { count: formatNumber(remaining.size) })} </code> <span class="remaining-range"> {ipToString(remaining.start)} - {ipToString(remaining.start + remaining.size - 1)} @@ -718,7 +727,7 @@ {/each} {#if pool.remaining.length > 5} <div class="remaining-more"> - +{pool.remaining.length - 5} more blocks + {$t('tools/cidr-allocator.pools.moreBlocks', { count: pool.remaining.length - 5 })} </div> {/if} </div> @@ -731,8 +740,8 @@ {:else} <div class="error-message"> <Icon name="alert-triangle" /> - <h4>Allocation Error</h4> - <p>{result.error || 'Unknown error occurred'}</p> + <h4>{$t('tools/cidr-allocator.errors.title')}</h4> + <p>{result.error || $t('tools/cidr-allocator.errors.unknown')}</p> </div> {/if} </section> diff --git a/src/lib/components/tools/CIDRCompare.svelte b/src/lib/components/tools/CIDRCompare.svelte index 69164536..3c722d84 100644 --- a/src/lib/components/tools/CIDRCompare.svelte +++ b/src/lib/components/tools/CIDRCompare.svelte @@ -4,6 +4,7 @@ import Icon from '$lib/components/global/Icon.svelte'; import { SvelteSet } from 'svelte/reactivity'; import '../../../styles/diagnostics-pages.scss'; + import { t } from '$lib/stores/language'; let listA = $state(`192.168.0.0/16 10.0.0.0/8 @@ -33,65 +34,65 @@ let selectedExampleIndex = $state<number | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Network Addition', + label: $t('tools/cidr-compare.examples.networkAddition'), listA: `192.168.1.0/24 10.0.0.0/16`, listB: `192.168.1.0/24 10.0.0.0/16 172.16.0.0/24`, - description: 'Added 172.16.0.0/24', + description: $t('tools/cidr-compare.examples.networkAdditionDesc'), }, { - label: 'Network Removal', + label: $t('tools/cidr-compare.examples.networkRemoval'), listA: `192.168.0.0/16 10.0.0.0/8 172.16.0.0/12`, listB: `192.168.0.0/16 10.0.0.0/8`, - description: 'Removed 172.16.0.0/12', + description: $t('tools/cidr-compare.examples.networkRemovalDesc'), }, { - label: 'Mixed Changes', + label: $t('tools/cidr-compare.examples.mixedChanges'), listA: `192.168.1.0/24 10.0.0.0/16 172.16.1.0/24`, listB: `192.168.1.0/24 10.0.1.0/24 172.16.2.0/24`, - description: 'Swapped subnets', + description: $t('tools/cidr-compare.examples.mixedChangesDesc'), }, { - label: 'VLAN Reconfiguration', + label: $t('tools/cidr-compare.examples.vlanReconfig'), listA: `192.168.10.0/24 192.168.20.0/24 192.168.30.0/24`, listB: `192.168.10.0/24 192.168.25.0/24 192.168.35.0/24`, - description: 'Replaced VLANs 20,30 with 25,35', + description: $t('tools/cidr-compare.examples.vlanReconfigDesc'), }, { - label: 'Network Consolidation', + label: $t('tools/cidr-compare.examples.networkConsolidation'), listA: `10.1.0.0/24 10.1.1.0/24 10.1.2.0/24 10.1.3.0/24`, listB: `10.1.0.0/22`, - description: 'Merged 4 /24s into 1 /22', + description: $t('tools/cidr-compare.examples.networkConsolidationDesc'), }, { - label: 'Branch Office Migration', + label: $t('tools/cidr-compare.examples.branchOfficeMigration'), listA: `172.16.1.0/24 172.16.2.0/24 192.168.100.0/24`, listB: `10.10.1.0/24 10.10.2.0/24 192.168.100.0/24`, - description: 'Migrated 172.16.x.x to 10.10.x.x', + description: $t('tools/cidr-compare.examples.branchOfficeMigrationDesc'), }, - ]; + ]); function loadExample(example: (typeof examples)[0], index: number) { listA = example.listA; @@ -278,8 +279,8 @@ <div class="card"> <header class="card-header"> - <h2>CIDR Compare</h2> - <p>Compare two lists of networks to identify changes for auditing</p> + <h2>{$t('tools/cidr-compare.title')}</h2> + <p>{$t('tools/cidr-compare.description')}</p> </header> <!-- Examples --> @@ -287,7 +288,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-compare.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -307,40 +308,40 @@ <!-- Input Section --> <section class="input-section"> <div class="input-header"> - <h3 use:tooltip={'Compare two network lists to identify additions, removals, and unchanged items'}> - Network Lists + <h3 use:tooltip={$t('tools/cidr-compare.input.networkListsTooltip')}> + {$t('tools/cidr-compare.input.networkListsHeading')} </h3> - <button class="swap-button" onclick={swapLists} use:tooltip={'Swap List A and List B'}> + <button class="swap-button" onclick={swapLists} use:tooltip={$t('tools/cidr-compare.input.swapTooltip')}> <Icon name="swap" size="sm" /> - Swap + {$t('tools/cidr-compare.input.swapButton')} </button> </div> <div class="input-grid"> <div class="input-group"> - <label for="list-a" use:tooltip={"Original or 'before' state - CIDR blocks, IP ranges, or individual IPs"}> + <label for="list-a" use:tooltip={$t('tools/cidr-compare.input.listATooltip')}> <Icon name="list" size="sm" /> - List A (Before) + {$t('tools/cidr-compare.input.listALabel')} </label> <textarea id="list-a" bind:value={listA} oninput={handleInputChange} - placeholder="192.168.0.0/16 10.0.0.0/8 172.16.1.0-172.16.1.255" + placeholder={$t('tools/cidr-compare.input.listAPlaceholder')} rows="8" ></textarea> </div> <div class="input-group"> - <label for="list-b" use:tooltip={"Updated or 'after' state - CIDR blocks, IP ranges, or individual IPs"}> + <label for="list-b" use:tooltip={$t('tools/cidr-compare.input.listBTooltip')}> <Icon name="list-check" size="sm" /> - List B (After) + {$t('tools/cidr-compare.input.listBLabel')} </label> <textarea id="list-b" bind:value={listB} oninput={handleInputChange} - placeholder="192.168.0.0/16 10.0.0.0/8 192.168.100.0/24" + placeholder={$t('tools/cidr-compare.input.listBPlaceholder')} rows="8" ></textarea> </div> @@ -353,7 +354,7 @@ {#if result.success} <!-- Summary --> <div class="comparison-summary"> - <h3 use:tooltip={'Overview of changes between the two network lists'}>Comparison Summary</h3> + <h3 use:tooltip={$t('tools/cidr-compare.summary.titleTooltip')}>{$t('tools/cidr-compare.summary.title')}</h3> <div class="summary-grid"> <div class="summary-card"> <div class="summary-icon added"> @@ -361,7 +362,7 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.addedCount}</div> - <div class="summary-label">Added</div> + <div class="summary-label">{$t('tools/cidr-compare.summary.addedLabel')}</div> </div> </div> @@ -371,7 +372,7 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.removedCount}</div> - <div class="summary-label">Removed</div> + <div class="summary-label">{$t('tools/cidr-compare.summary.removedLabel')}</div> </div> </div> @@ -381,17 +382,17 @@ </div> <div class="summary-content"> <div class="summary-number">{result.summary.unchangedCount}</div> - <div class="summary-label">Unchanged</div> + <div class="summary-label">{$t('tools/cidr-compare.summary.unchangedLabel')}</div> </div> </div> </div> <div class="list-totals"> <span class="total-item"> - List A: {result.summary.totalA} items + {$t('tools/cidr-compare.summary.listATotal', { count: result.summary.totalA })} </span> <span class="total-item"> - List B: {result.summary.totalB} items + {$t('tools/cidr-compare.summary.listBTotal', { count: result.summary.totalB })} </span> </div> </div> @@ -401,9 +402,9 @@ <!-- Added --> <div class="change-category added"> <div class="category-header"> - <h4 use:tooltip={'Networks present in List B but not in List A'}> + <h4 use:tooltip={$t('tools/cidr-compare.categories.addedTooltip')}> <Icon name="plus-circle" size="sm" /> - Added Networks ({result.added.length}) + {$t('tools/cidr-compare.categories.addedTitle', { count: result.added.length })} </h4> {#if result.added.length > 0} <button @@ -432,7 +433,7 @@ {:else} <div class="empty-category"> <Icon name="check" /> - <span>No networks added</span> + <span>{$t('tools/cidr-compare.empty.noAdded')}</span> </div> {/if} </div> @@ -440,9 +441,9 @@ <!-- Removed --> <div class="change-category removed"> <div class="category-header"> - <h4 use:tooltip={'Networks present in List A but not in List B'}> + <h4 use:tooltip={$t('tools/cidr-compare.categories.removedTooltip')}> <Icon name="minus-circle" size="sm" /> - Removed Networks ({result.removed.length}) + {$t('tools/cidr-compare.categories.removedTitle', { count: result.removed.length })} </h4> {#if result.removed.length > 0} <button @@ -471,7 +472,7 @@ {:else} <div class="empty-category"> <Icon name="check" /> - <span>No networks removed</span> + <span>{$t('tools/cidr-compare.empty.noRemoved')}</span> </div> {/if} </div> @@ -479,9 +480,9 @@ <!-- Unchanged --> <div class="change-category unchanged"> <div class="category-header"> - <h4 use:tooltip={'Networks present in both List A and List B'}> + <h4 use:tooltip={$t('tools/cidr-compare.categories.unchangedTooltip')}> <Icon name="check-circle" size="sm" /> - Unchanged Networks ({result.unchanged.length}) + {$t('tools/cidr-compare.categories.unchangedTitle', { count: result.unchanged.length })} </h4> {#if result.unchanged.length > 0} <button @@ -510,7 +511,7 @@ {:else} <div class="empty-category"> <Icon name="alert-circle" /> - <span>No networks remained unchanged</span> + <span>{$t('tools/cidr-compare.empty.noUnchanged')}</span> </div> {/if} </div> @@ -518,8 +519,8 @@ {:else} <div class="error-message"> <Icon name="alert-triangle" /> - <h4>Comparison Error</h4> - <p>{result.error || 'Unknown error occurred'}</p> + <h4>{$t('tools/cidr-compare.error.title')}</h4> + <p>{result.error || $t('tools/cidr-compare.error.unknown')}</p> </div> {/if} </section> diff --git a/src/lib/components/tools/CIDRContains.svelte b/src/lib/components/tools/CIDRContains.svelte index d90bb8a9..d6fe8e4b 100644 --- a/src/lib/components/tools/CIDRContains.svelte +++ b/src/lib/components/tools/CIDRContains.svelte @@ -5,6 +5,14 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let setA = $state(`192.168.0.0/16 10.0.0.0/8`); @@ -18,15 +26,15 @@ let selectedExampleIndex = $state<number | null>(null); let userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Basic Containment', + label: $t('tools.cidr_contains.examples.basicContainment'), setA: '192.168.0.0/16', setB: `192.168.1.0/24 192.168.2.0/24`, }, { - label: 'Mixed Results', + label: $t('tools.cidr_contains.examples.mixedResults'), setA: `192.168.1.0/24 10.0.0.0/16`, setB: `192.168.1.100/32 @@ -34,17 +42,17 @@ 172.16.0.0/24`, }, { - label: 'Partial Overlap', + label: $t('tools.cidr_contains.examples.partialOverlap'), setA: '192.168.1.0/25', setB: '192.168.1.0/24', }, { - label: 'IPv6 Containment', + label: $t('tools.cidr_contains.examples.ipv6Containment'), setA: '2001:db8::/32', setB: `2001:db8:1::/48 2001:db8:2::/64`, }, - ]; + ]); /* Set example */ function setExample(example: (typeof examples)[0], index: number) { @@ -111,7 +119,7 @@ checks: [], stats: { setA: { count: 0, addresses: '0' }, totalChecked: 0, inside: 0, equal: 0, partial: 0, outside: 0 }, visualization: [], - errors: [error instanceof Error ? error.message : 'Unknown error'], + errors: [error instanceof Error ? error.message : $t('tools.cidr_contains.errors.unknownError')], }; } } @@ -120,13 +128,13 @@ function getStatusInfo(status: ContainmentStatus) { switch (status) { case 'inside': - return { icon: 'check-circle', color: 'var(--color-success)', label: 'Inside' }; + return { icon: 'check-circle', color: 'var(--color-success)', label: $t('tools.cidr_contains.status.inside') }; case 'equal': - return { icon: 'equals', color: 'var(--color-info)', label: 'Equal' }; + return { icon: 'equals', color: 'var(--color-info)', label: $t('tools.cidr_contains.status.equal') }; case 'partial': - return { icon: 'alert-circle', color: 'var(--color-warning)', label: 'Partial' }; + return { icon: 'alert-circle', color: 'var(--color-warning)', label: $t('tools.cidr_contains.status.partial') }; case 'outside': - return { icon: 'x-circle', color: 'var(--color-error)', label: 'Outside' }; + return { icon: 'x-circle', color: 'var(--color-error)', label: $t('tools.cidr_contains.status.outside') }; } } @@ -150,9 +158,14 @@ type: 'candidate' | 'container' | 'gap', ): string { const size = range.end - range.start + 1n; - const label = type === 'candidate' ? 'Candidate' : type === 'container' ? 'Container' : 'Gap'; - - return `${label}${range.label ? ` (${range.label})` : ''}\nSize: ${formatNumber(Number(size))}${range.cidr ? `\nCIDR: ${range.cidr}` : ''}`; + const labelPart = range.label ? ` (${range.label})` : ''; + const cidrPart = range.cidr ? `\nCIDR: ${range.cidr}` : ''; + + return $t(`tools.cidr_contains.visualization.${type}Tooltip`, { + label: labelPart, + size: formatNumber(Number(size)), + cidr: cidrPart, + }); } // Reactive computation @@ -165,13 +178,13 @@ <!-- Options --> <div class="options-section"> - <h3>Options</h3> + <h3>{$t('tools.cidr_contains.options.title')}</h3> <div class="options-grid"> <label class="checkbox-label"> <input type="checkbox" bind:checked={mergeContainers} /> <span class="checkbox-text"> - Merge/normalize containers first - <Tooltip text="Combine overlapping ranges in set A before checking containment"> + {$t('tools.cidr_contains.options.mergeContainers')} + <Tooltip text={$t('tools.cidr_contains.options.mergeContainersTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </span> @@ -179,8 +192,8 @@ <label class="checkbox-label"> <input type="checkbox" bind:checked={strictEquality} /> <span class="checkbox-text"> - Strict equality counts as contain - <Tooltip text="Treat exact matches as 'equal' instead of 'inside'"> + {$t('tools.cidr_contains.options.strictEquality')} + <Tooltip text={$t('tools.cidr_contains.options.strictEqualityTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </span> @@ -194,15 +207,15 @@ <!-- Set A --> <div class="input-group"> <h3> - Set A (Containers) - <Tooltip text="The containing set - these ranges may contain items from Set B"> + {$t('tools.cidr_contains.input.setALabel')} + <Tooltip text={$t('tools.cidr_contains.input.setATooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h3> <div class="input-wrapper"> <textarea bind:value={setA} - placeholder="192.168.0.0/16 10.0.0.0/8" + placeholder={$t('tools.cidr_contains.input.setAPlaceholder')} class="input-textarea set-a" rows="6" oninput={handleInputChange} @@ -213,15 +226,15 @@ <!-- Set B --> <div class="input-group"> <h3> - Set B (Candidates) - <Tooltip text="Items to check for containment within Set A"> + {$t('tools/cidr-contains.input.setBLabel')} + <Tooltip text={$t('tools/cidr-contains.input.setBTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h3> <div class="input-wrapper"> <textarea bind:value={setB} - placeholder="192.168.1.0/24 172.16.0.0/24" + placeholder={$t('tools/cidr-contains.input.setBPlaceholder')} class="input-textarea set-b" rows="6" oninput={handleInputChange} @@ -233,7 +246,7 @@ <div class="input-actions"> <button type="button" class="btn btn-secondary btn-sm" onclick={clearInputs}> <Icon name="trash" size="sm" /> - Clear All + {$t('tools/cidr-contains.input.clearAll')} </button> </div> @@ -242,7 +255,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-contains.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -267,7 +280,7 @@ <div class="results-section"> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Parse Errors</h3> + <h3>{$t('tools/cidr-contains.results.errorsTitle')}</h3> <ul class="error-list"> {#each result.errors as error (error)} <li>{error}</li> @@ -280,7 +293,7 @@ <!-- Summary Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Containment Analysis</h3> + <h3>{$t('tools/cidr-contains.results.analysisTitle')}</h3> <div class="export-buttons"> <button type="button" @@ -289,7 +302,7 @@ onclick={exportAsCSV} > <Icon name={clipboard.isCopied('csv-export') ? 'check' : 'csv-file'} size="sm" /> - CSV + {$t('tools/cidr-contains.results.exportCSV')} </button> <button type="button" @@ -298,55 +311,61 @@ onclick={exportAsJSON} > <Icon name={clipboard.isCopied('json-export') ? 'check' : 'json-file'} size="sm" /> - JSON + {$t('tools/cidr-contains.results.exportJSON')} </button> </div> </div> <div class="stats-grid"> <div class="stat-card containers"> - <span class="stat-label">Containers (A)</span> - <span class="stat-value">{result.stats.setA.count} items</span> - <span class="stat-detail">{result.stats.setA.addresses} addresses</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.containersLabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-contains.stats.itemsCount', { count: result.stats.setA.count })}</span + > + <span class="stat-detail" + >{$t('tools/cidr-contains.stats.addressesCount', { count: result.stats.setA.addresses })}</span + > </div> <div class="stat-card candidates"> - <span class="stat-label">Candidates (B)</span> - <span class="stat-value">{result.stats.totalChecked} items</span> - <span class="stat-detail">checked for containment</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.candidatesLabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-contains.stats.itemsCount', { count: result.stats.totalChecked })}</span + > + <span class="stat-detail">{$t('tools/cidr-contains.stats.checkedForContainment')}</span> </div> <div class="stat-card inside"> - <span class="stat-label">Inside</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.insideLabel')}</span> <span class="stat-value">{result.stats.inside}</span> - <span class="stat-detail">fully contained</span> + <span class="stat-detail">{$t('tools/cidr-contains.stats.fullyContained')}</span> </div> <div class="stat-card equal"> - <span class="stat-label">Equal</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.equalLabel')}</span> <span class="stat-value">{result.stats.equal}</span> - <span class="stat-detail">exact matches</span> + <span class="stat-detail">{$t('tools/cidr-contains.stats.exactMatches')}</span> </div> <div class="stat-card partial"> - <span class="stat-label">Partial</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.partialLabel')}</span> <span class="stat-value">{result.stats.partial}</span> - <span class="stat-detail">partial overlap</span> + <span class="stat-detail">{$t('tools/cidr-contains.stats.partialOverlap')}</span> </div> <div class="stat-card outside"> - <span class="stat-label">Outside</span> + <span class="stat-label">{$t('tools/cidr-contains.stats.outsideLabel')}</span> <span class="stat-value">{result.stats.outside}</span> - <span class="stat-detail">no overlap</span> + <span class="stat-detail">{$t('tools/cidr-contains.stats.noOverlap')}</span> </div> </div> </div> <!-- Containment Results Table --> <div class="table-section"> - <h4>Containment Results</h4> + <h4>{$t('tools/cidr-contains.results.resultsTitle')}</h4> <div class="results-table"> <div class="table-header"> - <div class="col-input">Candidate</div> - <div class="col-status">Status</div> - <div class="col-coverage">Coverage</div> - <div class="col-containers">Containers</div> - <div class="col-gaps">Gaps</div> + <div class="col-input">{$t('tools/cidr-contains.table.candidateColumn')}</div> + <div class="col-status">{$t('tools/cidr-contains.table.statusColumn')}</div> + <div class="col-coverage">{$t('tools/cidr-contains.table.coverageColumn')}</div> + <div class="col-containers">{$t('tools/cidr-contains.table.containersColumn')}</div> + <div class="col-gaps">{$t('tools/cidr-contains.table.gapsColumn')}</div> </div> {#each result.checks as check, index (`${check.input}-${index}`)} @@ -378,7 +397,7 @@ {/each} </div> {:else} - <span class="no-containers">-</span> + <span class="no-containers">{$t('tools/cidr-contains.table.noDash')}</span> {/if} </div> <div class="col-gaps"> @@ -397,7 +416,7 @@ <Icon name={clipboard.isCopied(`gaps-${check.input}`) ? 'check' : 'copy'} size="xs" /> </button> {:else} - <span class="no-gaps">-</span> + <span class="no-gaps">{$t('tools/cidr-contains.table.noDash')}</span> {/if} </div> </div> @@ -408,19 +427,19 @@ <!-- Visualization --> {#if result.visualization.length > 0} <div class="visualization-section"> - <h4>Containment Visualization</h4> + <h4>{$t('tools/cidr-contains.visualization.title')}</h4> <div class="viz-legend"> <div class="legend-item"> <div class="legend-color candidate-color"></div> - <span>Candidate Range</span> + <span>{$t('tools/cidr-contains.visualization.candidateRange')}</span> </div> <div class="legend-item"> <div class="legend-color container-color"></div> - <span>Container Coverage</span> + <span>{$t('tools/cidr-contains.visualization.containerCoverage')}</span> </div> <div class="legend-item"> <div class="legend-color gap-color"></div> - <span>Uncovered Gaps</span> + <span>{$t('tools/cidr-contains.visualization.uncoveredGaps')}</span> </div> </div> diff --git a/src/lib/components/tools/CIDRDeaggregate.svelte b/src/lib/components/tools/CIDRDeaggregate.svelte index 6d1349fc..c8a8d5be 100644 --- a/src/lib/components/tools/CIDRDeaggregate.svelte +++ b/src/lib/components/tools/CIDRDeaggregate.svelte @@ -5,6 +5,7 @@ import { cidrDeaggregate, getSubnetSize, type DeaggregateResult } from '$lib/utils/cidr-deaggregate.js'; import { formatNumber } from '$lib/utils/formatters'; import '../../../styles/diagnostics-pages.scss'; + import { t } from '$lib/stores/language'; let input = $state(`192.168.0.0/22 10.0.0.0-10.0.0.255`); @@ -15,43 +16,43 @@ let selectedExampleIndex = $state<number | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Break /22 into /24s', + label: $t('tools/cidr-deaggregate.examples.break22to24'), input: '192.168.0.0/22', targetPrefix: 24, }, { - label: 'Decompose Range to /28s', + label: $t('tools/cidr-deaggregate.examples.decomposeRangeTo28'), input: '10.0.0.0-10.0.0.255', targetPrefix: 28, }, { - label: 'Multiple Blocks to /26s', + label: $t('tools/cidr-deaggregate.examples.multipleBlocksTo26'), input: `172.16.0.0/24 172.16.2.0/25`, targetPrefix: 26, }, { - label: 'Enterprise Campus to /25s', + label: $t('tools/cidr-deaggregate.examples.enterpriseCampusTo25'), input: `10.10.0.0/16 10.20.0.0/17`, targetPrefix: 25, }, { - label: 'Data Center Racks to /29s', + label: $t('tools/cidr-deaggregate.examples.dataCenterRacksTo29'), input: `192.168.100.0/24 192.168.101.0-192.168.101.127`, targetPrefix: 29, }, { - label: 'Service Provider to /30s', + label: $t('tools/cidr-deaggregate.examples.serviceProviderTo30'), input: `203.0.113.0/26 198.51.100.64/27 198.51.100.96/28`, targetPrefix: 30, }, - ]; + ]); function loadExample(example: (typeof examples)[0], index: number) { input = example.input; @@ -91,8 +92,8 @@ <div class="card"> <header class="card-header"> - <h2>CIDR Deaggregate</h2> - <p>Decompose CIDR blocks and ranges into uniform target prefix subnets</p> + <h2>{$t('tools/cidr-deaggregate.title')}</h2> + <p>{$t('tools/cidr-deaggregate.description')}</p> </header> <!-- Examples --> @@ -100,7 +101,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-deaggregate.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -111,7 +112,7 @@ > <div class="example-label">{example.label}</div> <div class="example-preview"> - Target: /{example.targetPrefix} + {$t('tools/cidr-deaggregate.examples.targetPreview', { targetPrefix: example.targetPrefix })} </div> </button> {/each} @@ -123,25 +124,22 @@ <section class="input-section"> <div class="input-grid"> <div class="input-group"> - <label for="input" use:tooltip={'Enter CIDR blocks, IP ranges, or individual IPs - one per line'}> - Input Networks/Ranges + <label for="input" use:tooltip={$t('tools/cidr-deaggregate.input.networksTooltip')}> + {$t('tools/cidr-deaggregate.input.networksLabel')} </label> <textarea id="input" bind:value={input} oninput={handleInputChange} - placeholder="192.168.0.0/22 10.0.0.0-10.0.255.255 172.16.1.1" + placeholder={$t('tools/cidr-deaggregate.input.networksPlaceholder')} rows="6" required ></textarea> </div> <div class="input-group"> - <label - for="target-prefix" - use:tooltip={'Target prefix length for uniform decomposition (e.g., 24 for /24 subnets)'} - > - Target Prefix Length + <label for="target-prefix" use:tooltip={$t('tools/cidr-deaggregate.input.targetPrefixTooltip')}> + {$t('tools/cidr-deaggregate.input.targetPrefixLabel')} </label> <div class="prefix-input-wrapper"> <input @@ -153,12 +151,12 @@ max="32" required /> - <span class="prefix-hint">/{targetPrefix}</span> + <span class="prefix-hint">{$t('tools/cidr-deaggregate.input.prefixHint', { prefix: targetPrefix })}</span> </div> <div class="prefix-info"> {#if targetPrefix} {@const addresses = getSubnetSize(targetPrefix)} - Each /{targetPrefix} subnet = {formatNumber(addresses)} addresses + {$t('tools/cidr-deaggregate.input.subnetSize', { prefix: targetPrefix, count: formatNumber(addresses) })} {/if} </div> </div> @@ -170,16 +168,18 @@ <section class="results-section"> {#if result.success} <div class="results-header"> - <h3 use:tooltip={'Generated uniform subnets from input networks and ranges'}>Deaggregated Subnets</h3> + <h3 use:tooltip={$t('tools/cidr-deaggregate.results.titleTooltip')}> + {$t('tools/cidr-deaggregate.results.title')} + </h3> <div class="results-actions"> <div class="results-summary"> <span class="metric"> <Icon name="network" size="sm" /> - {result.totalSubnets} subnets + {$t('tools/cidr-deaggregate.results.subnetsCount', { count: result.totalSubnets })} </span> <span class="metric"> <Icon name="database" size="sm" /> - {formatNumber(result.totalAddresses)} addresses + {$t('tools/cidr-deaggregate.results.addressesCount', { count: formatNumber(result.totalAddresses) })} </span> </div> {#if result.subnets.length > 0} @@ -188,7 +188,9 @@ onclick={copyAllSubnets} > <Icon name={clipboard.isCopied('all-subnets') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('all-subnets') ? 'Copied!' : 'Copy All'} + {clipboard.isCopied('all-subnets') + ? $t('tools/cidr-deaggregate.results.copied') + : $t('tools/cidr-deaggregate.results.copyAll')} </button> {/if} </div> @@ -197,26 +199,43 @@ <!-- Input Summary --> <div class="input-summary"> <div class="summary-item"> - <span class="summary-label" use:tooltip={'Original networks, ranges, and addresses provided'}>Input:</span> + <span class="summary-label" use:tooltip={$t('tools/cidr-deaggregate.summary.inputTooltip')} + >{$t('tools/cidr-deaggregate.summary.inputLabel')}</span + > <span class="summary-value"> - {result.inputSummary.totalInputs} items, {formatNumber(result.inputSummary.totalInputAddresses)} addresses + {$t('tools/cidr-deaggregate.summary.inputValue', { + count: result.inputSummary.totalInputs, + addresses: formatNumber(result.inputSummary.totalInputAddresses), + })} </span> </div> <div class="summary-item"> - <span class="summary-label" use:tooltip={'Uniform subnets generated from input'}>Output:</span> + <span class="summary-label" use:tooltip={$t('tools/cidr-deaggregate.summary.outputTooltip')} + >{$t('tools/cidr-deaggregate.summary.outputLabel')}</span + > <span class="summary-value"> - {result.totalSubnets} /{targetPrefix} subnets, {formatNumber(result.totalAddresses)} addresses + {$t('tools/cidr-deaggregate.summary.outputValue', { + count: result.totalSubnets, + prefix: targetPrefix, + addresses: formatNumber(result.totalAddresses), + })} </span> </div> {#if result.totalAddresses !== result.inputSummary.totalInputAddresses} <div class="summary-item"> - <span class="summary-label" use:tooltip={'Address count difference due to subnet boundary alignment'} - >Note:</span + <span class="summary-label" use:tooltip={$t('tools/cidr-deaggregate.summary.noteTooltip')} + >{$t('tools/cidr-deaggregate.summary.noteLabel')}</span > <span class="summary-value address-diff"> - {result.totalAddresses > result.inputSummary.totalInputAddresses ? 'Expanded' : 'Reduced'} by {formatNumber( - Math.abs(result.totalAddresses - result.inputSummary.totalInputAddresses), - )} addresses (due to alignment to /{targetPrefix} boundaries) + {result.totalAddresses > result.inputSummary.totalInputAddresses + ? $t('tools/cidr-deaggregate.summary.expanded', { + count: formatNumber(Math.abs(result.totalAddresses - result.inputSummary.totalInputAddresses)), + prefix: targetPrefix, + }) + : $t('tools/cidr-deaggregate.summary.reduced', { + count: formatNumber(Math.abs(result.totalAddresses - result.inputSummary.totalInputAddresses)), + prefix: targetPrefix, + })} </span> </div> {/if} @@ -232,21 +251,21 @@ <button class="copy-button {clipboard.isCopied(`subnet-${index}`) ? 'copied' : ''}" onclick={() => clipboard.copy(subnet, `subnet-${index}`)} - aria-label="Copy CIDR block" + aria-label={$t('tools/cidr-deaggregate.subnet.copyAria')} > <Icon name={clipboard.isCopied(`subnet-${index}`) ? 'check' : 'copy'} size="xs" /> </button> </div> <div class="subnet-info"> <span class="address-count"> - {formatNumber(subnetSize)} addresses + {$t('tools/cidr-deaggregate.subnet.addressesCount', { count: formatNumber(subnetSize) })} </span> {#if subnetSize >= 256} <span class="subnet-size"> {subnetSize >= 65536 - ? `${(subnetSize / 65536).toFixed(0)}Γ—/16` + ? $t('tools/cidr-deaggregate.subnet.sizeClass', { count: (subnetSize / 65536).toFixed(0) }) : subnetSize >= 256 - ? `${(subnetSize / 256).toFixed(0)}Γ—/24` + ? $t('tools/cidr-deaggregate.subnet.sizeClassSmall', { count: (subnetSize / 256).toFixed(0) }) : ''} </span> {/if} @@ -257,15 +276,15 @@ {:else} <div class="no-subnets"> <Icon name="alert-circle" /> - <h4>No Subnets Generated</h4> - <p>The target prefix length may be too large for the input networks, or the input is empty.</p> + <h4>{$t('tools/cidr-deaggregate.empty.title')}</h4> + <p>{$t('tools/cidr-deaggregate.empty.message')}</p> </div> {/if} {:else} <div class="error-message"> <Icon name="alert-triangle" /> - <h4>Deaggregation Error</h4> - <p>{result.error || 'Unknown error occurred'}</p> + <h4>{$t('tools/cidr-deaggregate.error.title')}</h4> + <p>{result.error || $t('tools/cidr-deaggregate.error.unknown')}</p> </div> {/if} </section> diff --git a/src/lib/components/tools/CIDRDiff.svelte b/src/lib/components/tools/CIDRDiff.svelte index e3224306..3ff8d225 100644 --- a/src/lib/components/tools/CIDRDiff.svelte +++ b/src/lib/components/tools/CIDRDiff.svelte @@ -5,6 +5,7 @@ import Tooltip from '$lib/components/global/Tooltip.svelte'; import Icon from '$lib/components/global/Icon.svelte'; import { formatNumber } from '$lib/utils/formatters'; + import { t } from '$lib/stores/language'; let setA = $state(`192.168.1.0/24 192.168.2.0/24`); @@ -17,45 +18,45 @@ let selectedExample = $state<string | null>(null); let userModified = $state(false); - const alignmentModes = [ + const alignmentModes = $derived([ { value: 'minimal' as const, - label: 'Minimal', - description: 'Generate the most efficient CIDR blocks', + label: $t('tools/cidr-diff.alignmentMode.minimal.label'), + description: $t('tools/cidr-diff.alignmentMode.minimal.description'), }, { value: 'constrained' as const, - label: 'Constrained', - description: 'Align to specific prefix boundaries', + label: $t('tools/cidr-diff.alignmentMode.constrained.label'), + description: $t('tools/cidr-diff.alignmentMode.constrained.description'), }, - ]; + ]); - const examples = [ + const examples = $derived([ { - label: 'Basic IPv4 Subtraction', + label: $t('tools/cidr-diff.examples.basicIPv4'), setA: '192.168.1.0/24', setB: '192.168.1.128/25', }, { - label: 'Multiple Ranges', + label: $t('tools/cidr-diff.examples.multipleRanges'), setA: `10.0.0.0/16 172.16.0.0/16`, setB: `10.0.1.0/24 172.16.50.0/24`, }, { - label: 'IPv6 Example', + label: $t('tools/cidr-diff.examples.ipv6Example'), setA: '2001:db8::/48', setB: '2001:db8:1::/64', }, { - label: 'Mixed Operations', + label: $t('tools/cidr-diff.examples.mixedOperations'), setA: `192.168.0.0/16 2001:db8::/32`, setB: `192.168.100.0/24 2001:db8:abcd::/48`, }, - ]; + ]); /* Set example */ function setExample(example: (typeof examples)[0]) { @@ -171,8 +172,15 @@ ].join('.') : 'IPv6'; const size = range.end - range.start + 1n; - - return `Set ${type}\nRange: ${startIP} - ${endIP}\nSize: ${formatNumber(Number(size))}${range.cidr ? `\nCIDR: ${range.cidr}` : ''}`; + const cidrPart = range.cidr ? `\nCIDR: ${range.cidr}` : ''; + + const tooltipKey = type === 'A' ? 'setATooltip' : type === 'B' ? 'setBTooltip' : 'resultTooltip'; + return $t(`tools/cidr-diff.visualization.${tooltipKey}`, { + startIP, + endIP, + size: formatNumber(Number(size)), + cidr: cidrPart, + }); } // Track user modifications @@ -192,7 +200,7 @@ <!-- Alignment Mode --> <div class="mode-section"> - <h3>Alignment Mode</h3> + <h3>{$t('tools/cidr-diff.alignmentMode.title')}</h3> <div class="tabs-container"> <div class="tabs"> {#each alignmentModes as mode (mode.value)} @@ -212,9 +220,9 @@ <div class="constraint-input"> <label for="constrained-prefix" - use:tooltip={{ text: 'Force alignment to this prefix boundary', position: 'top' }} + use:tooltip={{ text: $t('tools/cidr-diff.alignmentMode.constrained.prefixTooltip'), position: 'top' }} > - Constrained prefix length + {$t('tools/cidr-diff.alignmentMode.constrained.prefixLabel')} </label> <input id="constrained-prefix" @@ -235,14 +243,14 @@ <div class="input-grid"> <!-- Set A --> <div class="input-group"> - <h3 use:tooltip={{ text: 'The base set of IP addresses, CIDR blocks, or ranges', position: 'top' }}> - Set A (Base) + <h3 use:tooltip={{ text: $t('tools/cidr-diff.input.setATooltip'), position: 'top' }}> + {$t('tools/cidr-diff.input.setALabel')} </h3> <div class="input-wrapper"> <textarea bind:value={setA} oninput={() => (userModified = true)} - placeholder="192.168.1.0/24 10.0.0.0-10.0.0.100" + placeholder={$t('tools/cidr-diff.input.setAPlaceholder')} class="input-textarea set-a" rows="6" ></textarea> @@ -251,12 +259,14 @@ <!-- Set B --> <div class="input-group"> - <h3 use:tooltip={{ text: 'The set to subtract from Set A (can be empty)', position: 'top' }}>Set B (Subtract)</h3> + <h3 use:tooltip={{ text: $t('tools/cidr-diff.input.setBTooltip'), position: 'top' }}> + {$t('tools/cidr-diff.input.setBLabel')} + </h3> <div class="input-wrapper"> <textarea bind:value={setB} oninput={() => (userModified = true)} - placeholder="192.168.1.128/25 10.0.0.50-10.0.0.75" + placeholder={$t('tools/cidr-diff.input.setBPlaceholder')} class="input-textarea set-b" rows="6" ></textarea> @@ -269,7 +279,7 @@ type="button" class="btn btn-secondary btn-sm" onclick={clearInputs} - use:tooltip={{ text: 'Clear both input sets', position: 'top' }} + use:tooltip={{ text: $t('tools/cidr-diff.input.clearTooltip'), position: 'top' }} > <Icon name="trash" size="sm" /> </button> @@ -278,8 +288,8 @@ <!-- Examples --> <div class="examples-section"> <h4> - Quick Examples - <Tooltip text="Click any example to load it into the input fields"> + {$t('tools/cidr-diff.examples.title')} + <Tooltip text={$t('tools/cidr-diff.examples.tooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h4> @@ -303,7 +313,7 @@ <div class="results-section"> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Parse Errors</h3> + <h3>{$t('tools/cidr-diff.results.errorsTitle')}</h3> <ul class="error-list"> {#each result.errors as error (error)} <li>{error}</li> @@ -316,7 +326,7 @@ <!-- Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Difference Results (A - B)</h3> + <h3>{$t('tools/cidr-diff.results.title')}</h3> <div class="export-buttons"> <button type="button" @@ -325,7 +335,7 @@ onclick={() => copyAllResults('text')} > <Icon name={clipboard.isCopied('all-text') ? 'check' : 'copy'} size="sm" /> - Text + {$t('tools/cidr-diff.results.exportText')} </button> <button type="button" @@ -334,34 +344,50 @@ onclick={() => copyAllResults('json')} > <Icon name={clipboard.isCopied('all-json') ? 'check' : 'download'} size="sm" /> - JSON + {$t('tools/cidr-diff.results.exportJSON')} </button> </div> </div> <div class="stats-grid"> <div class="stat-card input-a"> - <span class="stat-label">Set A (Input)</span> - <span class="stat-value">{result.stats.inputA.count} items</span> - <span class="stat-detail">{result.stats.inputA.addresses} addresses</span> + <span class="stat-label">{$t('tools/cidr-diff.stats.setALabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-diff.stats.itemsCount', { count: result.stats.inputA.count })}</span + > + <span class="stat-detail" + >{$t('tools/cidr-diff.stats.addressesCount', { count: result.stats.inputA.addresses })}</span + > </div> <div class="stat-card input-b"> - <span class="stat-label">Set B (Subtract)</span> - <span class="stat-value">{result.stats.inputB.count} items</span> - <span class="stat-detail">{result.stats.inputB.addresses} addresses</span> + <span class="stat-label">{$t('tools/cidr-diff.stats.setBLabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-diff.stats.itemsCount', { count: result.stats.inputB.count })}</span + > + <span class="stat-detail" + >{$t('tools/cidr-diff.stats.addressesCount', { count: result.stats.inputB.addresses })}</span + > </div> <div class="stat-card result"> - <span class="stat-label">Result (A - B)</span> - <span class="stat-value">{result.stats.output.count} CIDRs</span> - <span class="stat-detail">{result.stats.output.addresses} addresses</span> + <span class="stat-label">{$t('tools/cidr-diff.stats.resultLabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-diff.stats.cidrsCount', { count: result.stats.output.count })}</span + > + <span class="stat-detail" + >{$t('tools/cidr-diff.stats.addressesCount', { count: result.stats.output.addresses })}</span + > </div> <div class="stat-card efficiency" data-efficiency={result.stats.efficiency >= 80 ? 'high' : result.stats.efficiency >= 50 ? 'medium' : 'low'} > - <span class="stat-label">Efficiency</span> - <span class="stat-value">{result.stats.efficiency}%</span> - <span class="stat-detail">{result.stats.removed.addresses} removed</span> + <span class="stat-label">{$t('tools/cidr-diff.stats.efficiencyLabel')}</span> + <span class="stat-value" + >{$t('tools/cidr-diff.stats.efficiencyValue', { percent: result.stats.efficiency })}</span + > + <span class="stat-detail" + >{$t('tools/cidr-diff.stats.removedCount', { count: result.stats.removed.addresses })}</span + > </div> </div> </div> @@ -370,23 +396,23 @@ {#if result.visualization.setA.length > 0} <div class="visualization-section"> <h4> - Set Operation Visualization - <Tooltip text="Visual representation showing the relationship between sets A, B, and the result"> + {$t('tools/cidr-diff.visualization.title')} + <Tooltip text={$t('tools/cidr-diff.visualization.tooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h4> <div class="viz-legend"> <div class="legend-item"> <div class="legend-color set-a-color"></div> - <span>Set A (Base)</span> + <span>{$t('tools/cidr-diff.visualization.setALegend')}</span> </div> <div class="legend-item"> <div class="legend-color set-b-color"></div> - <span>Set B (Subtract)</span> + <span>{$t('tools/cidr-diff.visualization.setBLegend')}</span> </div> <div class="legend-item"> <div class="legend-color result-color"></div> - <span>Result (A - B)</span> + <span>{$t('tools/cidr-diff.visualization.resultLegend')}</span> </div> </div> @@ -427,7 +453,7 @@ {#if result.ipv4.length > 0} <div class="result-panel ipv4"> <div class="panel-header"> - <h4>IPv4 Results ({result.ipv4.length})</h4> + <h4>{$t('tools/cidr-diff.output.ipv4Title', { count: result.ipv4.length })}</h4> <button type="button" class="btn btn-icon" @@ -459,7 +485,7 @@ {#if result.ipv6.length > 0} <div class="result-panel ipv6"> <div class="panel-header"> - <h4>IPv6 Results ({result.ipv6.length})</h4> + <h4>{$t('tools/cidr-diff.output.ipv6Title', { count: result.ipv6.length })}</h4> <button type="button" class="btn btn-icon" @@ -489,8 +515,8 @@ </div> {:else} <div class="info-panel info"> - <h3>No Results</h3> - <p>The difference A - B resulted in an empty set. Set B completely contains or covers Set A.</p> + <h3>{$t('tools/cidr-diff.results.noResultsTitle')}</h3> + <p>{$t('tools/cidr-diff.results.noResultsMessage')}</p> </div> {/if} </div> diff --git a/src/lib/components/tools/CIDROverlap.svelte b/src/lib/components/tools/CIDROverlap.svelte index 155aa28d..6e80573d 100644 --- a/src/lib/components/tools/CIDROverlap.svelte +++ b/src/lib/components/tools/CIDROverlap.svelte @@ -1,6 +1,9 @@ <script lang="ts"> import { computeCIDROverlap, type OverlapResult } from '$lib/utils/cidr-overlap.js'; import { tooltip } from '$lib/actions/tooltip.js'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import Tooltip from '$lib/components/global/Tooltip.svelte'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; @@ -17,30 +20,35 @@ let selectedExample = $state<string | null>(null); let userModified = $state(false); - const examples = [ + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); + + const examples = $derived([ { - label: 'Basic Overlap', + label: $t('tools.cidr_overlap.examples.basicOverlap'), setA: '192.168.1.0/24', setB: '192.168.1.128/25', }, { - label: 'No Overlap', + label: $t('tools.cidr_overlap.examples.noOverlap'), setA: '192.168.1.0/24', setB: '192.168.2.0/24', }, { - label: 'Partial Overlap', + label: $t('tools.cidr_overlap.examples.partialOverlap'), setA: `192.168.1.0/25 192.168.2.0/24`, setB: `192.168.1.64/26 192.168.3.0/24`, }, { - label: 'IPv6 Overlap', + label: $t('tools.cidr_overlap.examples.ipv6Overlap'), setA: '2001:db8::/48', setB: '2001:db8:1::/64', }, - ]; + ]); /* Set example */ function setExample(example: (typeof examples)[0]) { @@ -178,13 +186,13 @@ <!-- Options --> <div class="options-section"> - <h3>Options</h3> + <h3>{$t('tools.cidr_overlap.options.title')}</h3> <div class="options-grid"> <label class="checkbox-label"> <input type="checkbox" bind:checked={mergeInputs} onchange={() => (userModified = true)} /> <span class="checkbox-text"> - Merge overlapping inputs first - <Tooltip text="Combine overlapping ranges within each set before comparison"> + {$t('tools.cidr_overlap.options.mergeInputs')} + <Tooltip text={$t('tools.cidr_overlap.options.mergeInputsTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </span> @@ -192,8 +200,8 @@ <label class="checkbox-label"> <input type="checkbox" bind:checked={showOnlyBoolean} onchange={() => (userModified = true)} /> <span class="checkbox-text"> - Show only boolean result - <Tooltip text="Display just yes/no overlap instead of detailed intersection blocks"> + {$t('tools.cidr_overlap.options.showOnlyBoolean')} + <Tooltip text={$t('tools.cidr_overlap.options.showOnlyBooleanTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </span> @@ -207,8 +215,8 @@ <!-- Set A --> <div class="input-group"> <h3> - Set A - <Tooltip text="First set of IP addresses, CIDR blocks, or ranges"> + {$t('tools.cidr_overlap.input.setALabel')} + <Tooltip text={$t('tools.cidr_overlap.input.setATooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h3> @@ -216,7 +224,7 @@ <textarea bind:value={setA} oninput={() => (userModified = true)} - placeholder="192.168.1.0/24 10.0.0.0-10.0.0.100" + placeholder={$t('tools.cidr_overlap.input.setAPlaceholder')} class="input-textarea set-a" rows="6" ></textarea> @@ -226,8 +234,8 @@ <!-- Set B --> <div class="input-group"> <h3> - Set B - <Tooltip text="Second set of IP addresses, CIDR blocks, or ranges"> + {$t('tools.cidr_overlap.input.setBLabel')} + <Tooltip text={$t('tools.cidr_overlap.input.setBTooltip')}> <Icon name="help" size="sm" /> </Tooltip> </h3> @@ -235,7 +243,7 @@ <textarea bind:value={setB} oninput={() => (userModified = true)} - placeholder="192.168.1.128/25 10.0.0.50-10.0.0.150" + placeholder={$t('tools.cidr_overlap.input.setBPlaceholder')} class="input-textarea set-b" rows="6" ></textarea> @@ -246,13 +254,13 @@ <div class="input-actions"> <button type="button" class="btn btn-secondary btn-sm" onclick={clearInputs}> <Icon name="trash" size="sm" /> - Clear All + {$t('tools.cidr_overlap.input.clearAll')} </button> </div> <!-- Examples --> <div class="examples-section"> - <h4>Quick Examples</h4> + <h4>{$t('tools.cidr_overlap.examples.title')}</h4> <div class="examples-grid"> {#each examples as example (example.label)} <button @@ -273,7 +281,7 @@ <div class="results-section"> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Parse Errors</h3> + <h3>{$t('tools.cidr_overlap.status.errorTitle')}</h3> <ul class="error-list"> {#each result.errors as error (error)} <li>{error}</li> @@ -289,11 +297,15 @@ <Icon name={result.hasOverlap ? 'check-circle' : 'x-circle'} size="lg" /> </div> <div class="status-content"> - <h3>{result.hasOverlap ? 'Overlap Detected' : 'No Overlap'}</h3> + <h3> + {result.hasOverlap + ? $t('tools.cidr_overlap.status.overlapDetected') + : $t('tools.cidr_overlap.status.noOverlap')} + </h3> <p> {result.hasOverlap - ? `Sets A and B have overlapping address ranges (${result.stats.overlapPercent}% of smaller set)` - : 'Sets A and B do not share any common address ranges'} + ? $t('tools.cidr_overlap.status.overlapMessage', { percent: result.stats.overlapPercent }) + : $t('tools.cidr_overlap.status.noOverlapMessage')} </p> </div> </div> @@ -303,7 +315,7 @@ <!-- Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Intersection Results (A ∩ B)</h3> + <h3>{$t('tools.cidr_overlap.results.title')}</h3> <div class="export-buttons"> <button type="button" @@ -312,7 +324,7 @@ onclick={() => copyAllResults('text')} > <Icon name={clipboard.isCopied('all-text') ? 'check' : 'copy'} size="sm" /> - Text + {$t('tools.cidr_overlap.results.copyText')} </button> <button type="button" @@ -321,31 +333,43 @@ onclick={() => copyAllResults('json')} > <Icon name={clipboard.isCopied('all-json') ? 'check' : 'download'} size="sm" /> - JSON + {$t('tools.cidr_overlap.results.copyJSON')} </button> </div> </div> <div class="stats-grid"> <div class="stat-card set-a"> - <span class="stat-label">Set A</span> - <span class="stat-value">{result.stats.setA.count} items</span> - <span class="stat-detail">{result.stats.setA.addresses} addresses</span> + <span class="stat-label">{$t('tools.cidr_overlap.results.setALabel')}</span> + <span class="stat-value" + >{$t('tools.cidr_overlap.results.itemsCount', { count: result.stats.setA.count })}</span + > + <span class="stat-detail" + >{$t('tools.cidr_overlap.results.addressesCount', { count: result.stats.setA.addresses })}</span + > </div> <div class="stat-card set-b"> - <span class="stat-label">Set B</span> - <span class="stat-value">{result.stats.setB.count} items</span> - <span class="stat-detail">{result.stats.setB.addresses} addresses</span> + <span class="stat-label">{$t('tools.cidr_overlap.results.setBLabel')}</span> + <span class="stat-value" + >{$t('tools.cidr_overlap.results.itemsCount', { count: result.stats.setB.count })}</span + > + <span class="stat-detail" + >{$t('tools.cidr_overlap.results.addressesCount', { count: result.stats.setB.addresses })}</span + > </div> <div class="stat-card intersection"> - <span class="stat-label">Intersection</span> - <span class="stat-value">{result.stats.intersection.count} CIDRs</span> - <span class="stat-detail">{result.stats.intersection.addresses} addresses</span> + <span class="stat-label">{$t('tools.cidr_overlap.results.intersectionLabel')}</span> + <span class="stat-value" + >{$t('tools.cidr_overlap.results.cidrsCount', { count: result.stats.intersection.count })}</span + > + <span class="stat-detail" + >{$t('tools.cidr_overlap.results.addressesCount', { count: result.stats.intersection.addresses })}</span + > </div> <div class="stat-card overlap-percent"> - <span class="stat-label">Overlap</span> + <span class="stat-label">{$t('tools.cidr_overlap.results.overlapLabel')}</span> <span class="stat-value">{result.stats.overlapPercent}%</span> - <span class="stat-detail">of smaller set</span> + <span class="stat-detail">{$t('tools.cidr_overlap.results.ofSmallerSet')}</span> </div> </div> </div> @@ -353,26 +377,26 @@ <!-- Visualization --> {#if result.visualization.setA.length > 0 || result.visualization.setB.length > 0} <div class="visualization-section"> - <h4>Overlap Visualization</h4> + <h4>{$t('tools.cidr_overlap.visualization.title')}</h4> <div class="viz-legend"> <div class="legend-item"> <div class="legend-color set-a-color"></div> - <span>Set A</span> + <span>{$t('tools.cidr_overlap.visualization.setALegend')}</span> </div> <div class="legend-item"> <div class="legend-color set-b-color"></div> - <span>Set B</span> + <span>{$t('tools.cidr_overlap.visualization.setBLegend')}</span> </div> <div class="legend-item"> <div class="legend-color intersection-color"></div> - <span>Intersection (A ∩ B)</span> + <span>{$t('tools.cidr_overlap.visualization.intersectionLegend')}</span> </div> </div> <div class="visualization-stack"> <!-- Set A Bar --> <div class="viz-bar set-a-bar"> - <div class="bar-label">Set A</div> + <div class="bar-label">{$t('tools.cidr_overlap.visualization.setALabel')}</div> <div class="bar-segments"> {#each result.visualization.setA as range (`${range.start}-${range.end}`)} <div @@ -386,7 +410,7 @@ <!-- Set B Bar --> <div class="viz-bar set-b-bar"> - <div class="bar-label">Set B</div> + <div class="bar-label">{$t('tools.cidr_overlap.visualization.setBLabel')}</div> <div class="bar-segments"> {#each result.visualization.setB as range (`${range.start}-${range.end}`)} <div @@ -400,7 +424,7 @@ <!-- Intersection Highlights --> <div class="viz-bar intersection-bar"> - <div class="bar-label">A ∩ B</div> + <div class="bar-label">{$t('tools.cidr_overlap.visualization.intersectionLabel')}</div> <div class="bar-segments"> {#each result.visualization.intersection as range (`${range.start}-${range.end}`)} <div @@ -421,7 +445,7 @@ {#if result.ipv4.length > 0} <div class="result-panel ipv4"> <div class="panel-header"> - <h4>IPv4 Intersection ({result.ipv4.length})</h4> + <h4>{$t('tools.cidr_overlap.intersection.ipv4Title', { count: result.ipv4.length })}</h4> <button type="button" class="btn btn-icon" @@ -453,7 +477,7 @@ {#if result.ipv6.length > 0} <div class="result-panel ipv6"> <div class="panel-header"> - <h4>IPv6 Intersection ({result.ipv6.length})</h4> + <h4>{$t('tools.cidr_overlap.intersection.ipv6Title', { count: result.ipv6.length })}</h4> <button type="button" class="btn btn-icon" diff --git a/src/lib/components/tools/CIDRSplitter.svelte b/src/lib/components/tools/CIDRSplitter.svelte index 23cccaf1..780297c6 100644 --- a/src/lib/components/tools/CIDRSplitter.svelte +++ b/src/lib/components/tools/CIDRSplitter.svelte @@ -2,6 +2,7 @@ import { splitCIDRByCount, splitCIDRByPrefix, type SplitResult } from '$lib/utils/cidr-split.js'; import { tooltip } from '$lib/actions/tooltip.js'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import Icon from '$lib/components/global/Icon.svelte'; import '../../../styles/diagnostics-pages.scss'; @@ -13,45 +14,45 @@ const clipboard = useClipboard(); let selectedExampleIndex = $state<number | null>(null); - const modes = [ + const modes = $derived([ { value: 'count' as const, - label: 'By Count', - description: 'Split into N equal subnets', + label: $t('tools/cidr-splitter.modes.byCount.label'), + description: $t('tools/cidr-splitter.modes.byCount.description'), }, { value: 'prefix' as const, - label: 'By Prefix', - description: 'Split to target prefix length', + label: $t('tools/cidr-splitter.modes.byPrefix.label'), + description: $t('tools/cidr-splitter.modes.byPrefix.description'), }, - ]; + ]); - const examples = [ + const examples = $derived([ { - label: 'Split /24 β†’ 4 subnets', + label: $t('tools/cidr-splitter.examples.split24To4'), cidr: '192.168.1.0/24', mode: 'count' as const, count: 4, }, { - label: 'Split /16 β†’ /20', + label: $t('tools/cidr-splitter.examples.split16To20'), cidr: '10.0.0.0/16', mode: 'prefix' as const, prefix: 20, }, { - label: 'IPv6 /48 β†’ 16 subnets', + label: $t('tools/cidr-splitter.examples.splitIPv6_48To16'), cidr: '2001:db8::/48', mode: 'count' as const, count: 16, }, { - label: 'IPv6 /32 β†’ /40', + label: $t('tools/cidr-splitter.examples.splitIPv6_32To40'), cidr: '2001:db8::/32', mode: 'prefix' as const, prefix: 40, }, - ]; + ]); /* Set example */ function setExample(example: (typeof examples)[0], index: number) { @@ -138,7 +139,12 @@ const subnet = result.subnets.find((s) => s.cidr === childRange.cidr); if (!subnet) return childRange.cidr; - return `${subnet.cidr}\nRange: ${subnet.network} - ${subnet.broadcast}\nHosts: ${subnet.totalHosts}`; + return $t('tools/cidr-splitter.visualization.subnetTooltip', { + cidr: subnet.cidr, + network: subnet.network, + broadcast: subnet.broadcast, + totalHosts: subnet.totalHosts, + }); } // Reactive split and example selection tracking @@ -165,13 +171,13 @@ <div class="card"> <header class="card-header"> - <h2>CIDR Subnet Splitter</h2> - <p>Split a network into equal child subnets by count or target prefix length.</p> + <h2>{$t('tools/cidr-splitter.title')}</h2> + <p>{$t('tools/cidr-splitter.description')}</p> </header> <!-- Mode Selection --> <div class="mode-section"> - <h3>Split Mode</h3> + <h3>{$t('tools/cidr-splitter.modes.title')}</h3> <div class="tabs"> {#each modes as modeOption (modeOption.value)} <button @@ -189,18 +195,24 @@ <!-- Input Section --> <div class="input-section"> - <h3>Parent Network</h3> + <h3>{$t('tools/cidr-splitter.input.parentNetworkTitle')}</h3> <div class="form-group"> - <label for="input-cidr" use:tooltip={'Enter IPv4 or IPv6 network in CIDR notation (e.g., 192.168.1.0/24)'}> - Parent CIDR block + <label for="input-cidr" use:tooltip={$t('tools/cidr-splitter.input.parentCIDRTooltip')}> + {$t('tools/cidr-splitter.input.parentCIDRLabel')} </label> <div class="input-wrapper"> - <input id="input-cidr" type="text" bind:value={inputCIDR} placeholder="192.168.1.0/24" class="input-field" /> + <input + id="input-cidr" + type="text" + bind:value={inputCIDR} + placeholder={$t('tools/cidr-splitter.input.parentCIDRPlaceholder')} + class="input-field" + /> <button type="button" class="btn btn-secondary btn-sm clear-btn" onclick={clearInput} - use:tooltip={'Clear input'} + use:tooltip={$t('tools/cidr-splitter.input.clearInputTooltip')} > <Icon name="trash" size="sm" /> </button> @@ -210,19 +222,13 @@ <!-- Split Parameters --> <div class="form-group"> {#if splitMode === 'count'} - <label - for="subnet-count" - use:tooltip={'How many equal subnets to create (will be rounded to nearest power of 2)'} - > - Number of subnets + <label for="subnet-count" use:tooltip={$t('tools/cidr-splitter.input.subnetCountTooltip')}> + {$t('tools/cidr-splitter.input.subnetCountLabel')} </label> <input id="subnet-count" type="number" bind:value={subnetCount} min="1" max="1024" class="input-field" /> {:else} - <label - for="target-prefix" - use:tooltip={'The prefix length for child subnets (must be larger than parent prefix)'} - > - Target prefix length + <label for="target-prefix" use:tooltip={$t('tools/cidr-splitter.input.targetPrefixTooltip')}> + {$t('tools/cidr-splitter.input.targetPrefixLabel')} </label> <input id="target-prefix" type="number" bind:value={targetPrefix} min="1" max="128" class="input-field" /> {/if} @@ -234,7 +240,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-splitter.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -242,7 +248,9 @@ class="example-card" class:selected={selectedExampleIndex === i} onclick={() => setExample(example, i)} - use:tooltip={`${example.mode === 'count' ? 'Split into ' + example.count + ' subnets' : 'Split to /' + example.prefix + ' prefix'}`} + use:tooltip={example.mode === 'count' + ? $t('tools/cidr-splitter.examples.splitToCount', { count: example.count }) + : $t('tools/cidr-splitter.examples.splitToPrefix', { prefix: example.prefix })} > <h5>{example.cidr}</h5> <p>{example.label}</p> @@ -257,14 +265,14 @@ <div class="results-section"> {#if result.error} <div class="info-panel error"> - <h3>Split Error</h3> + <h3>{$t('tools/cidr-splitter.results.errorTitle')}</h3> <p>{result.error}</p> </div> {:else if result.subnets.length > 0} <!-- Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Split Results</h3> + <h3>{$t('tools/cidr-splitter.results.title')}</h3> <button type="button" class="btn btn-primary btn-sm" @@ -272,32 +280,39 @@ onclick={copyAllSubnets} > <Icon name={clipboard.isCopied('all-subnets') ? 'check' : 'copy'} size="sm" /> - Copy All CIDRs + {$t('tools/cidr-splitter.results.copyAllCIDRs')} </button> </div> <div class="stats-grid"> <div class="stat-card"> - <span class="stat-label" use:tooltip={'The original network that was split'}>Parent Network</span> + <span class="stat-label" use:tooltip={$t('tools/cidr-splitter.results.parentNetworkTooltip')} + >{$t('tools/cidr-splitter.results.parentNetworkLabel')}</span + > <span class="stat-value">{result.stats.parentCIDR}</span> </div> <div class="stat-card"> - <span class="stat-label" use:tooltip={'Number of child subnets created'}>Child Subnets</span> + <span class="stat-label" use:tooltip={$t('tools/cidr-splitter.results.childSubnetsTooltip')} + >{$t('tools/cidr-splitter.results.childSubnetsLabel')}</span + > <span class="stat-value">{result.stats.childCount}</span> </div> <div class="stat-card"> - <span class="stat-label" use:tooltip={'Prefix length of each child subnet'}>Child Prefix</span> + <span class="stat-label" use:tooltip={$t('tools/cidr-splitter.results.childPrefixTooltip')} + >{$t('tools/cidr-splitter.results.childPrefixLabel')}</span + > <span class="stat-value">/{result.stats.childPrefix}</span> </div> <div class="stat-card"> - <span class="stat-label" use:tooltip={'Total IP addresses in each child subnet'}>Addresses per Child</span + <span class="stat-label" use:tooltip={$t('tools/cidr-splitter.results.addressesPerChildTooltip')} + >{$t('tools/cidr-splitter.results.addressesPerChildLabel')}</span > <span class="stat-value">{result.stats.addressesPerChild}</span> </div> {#if result.stats.utilizationPercent < 100} <div class="stat-card"> - <span class="stat-label" use:tooltip={"Percentage of parent network's address space used"} - >Utilization</span + <span class="stat-label" use:tooltip={$t('tools/cidr-splitter.results.utilizationTooltip')} + >{$t('tools/cidr-splitter.results.utilizationLabel')}</span > <span class="stat-value">{result.stats.utilizationPercent}%</span> </div> @@ -307,7 +322,7 @@ <!-- Visualization --> <div class="visualization-section"> - <h4>Address Space Visualization</h4> + <h4>{$t('tools/cidr-splitter.visualization.title')}</h4> <div class="address-bar"> {#each result.visualization.childRanges as childRange (childRange.cidr)} <div @@ -321,7 +336,7 @@ <!-- Subnet List --> <div class="subnets-section"> - <h4>Child Subnets</h4> + <h4>{$t('tools/cidr-splitter.subnets.title')}</h4> <div class="subnets-grid"> {#each result.subnets as subnet (subnet.cidr)} <div class="subnet-card"> @@ -338,19 +353,19 @@ </div> <div class="subnet-details"> <div class="detail-row"> - <span class="detail-label">Network:</span> + <span class="detail-label">{$t('tools/cidr-splitter.subnets.networkLabel')}</span> <span class="detail-value">{subnet.network}</span> </div> <div class="detail-row"> - <span class="detail-label">Broadcast:</span> + <span class="detail-label">{$t('tools/cidr-splitter.subnets.broadcastLabel')}</span> <span class="detail-value">{subnet.broadcast}</span> </div> <div class="detail-row"> - <span class="detail-label">Usable:</span> + <span class="detail-label">{$t('tools/cidr-splitter.subnets.usableLabel')}</span> <span class="detail-value">{subnet.firstHost} - {subnet.lastHost}</span> </div> <div class="detail-row"> - <span class="detail-label">Hosts:</span> + <span class="detail-label">{$t('tools/cidr-splitter.subnets.hostsLabel')}</span> <span class="detail-value">{subnet.usableHosts}</span> </div> </div> diff --git a/src/lib/components/tools/CIDRSummarizer.svelte b/src/lib/components/tools/CIDRSummarizer.svelte index 098b25b6..df447735 100644 --- a/src/lib/components/tools/CIDRSummarizer.svelte +++ b/src/lib/components/tools/CIDRSummarizer.svelte @@ -4,6 +4,7 @@ import Tooltip from '$lib/components/global/Tooltip.svelte'; import Icon from '$lib/components/global/Icon.svelte'; import SvgIcon from '$lib/components/global/SvgIcon.svelte'; + import { t } from '$lib/stores/language'; let inputText = $state(`192.168.1.1 192.168.1.0/24 @@ -15,42 +16,42 @@ const clipboard = useClipboard(); let selectedExample = $state<string | null>(null); - const modes = [ + const modes = $derived([ { value: 'exact-merge' as const, - label: 'Exact Merge', - description: 'Merge overlapping ranges exactly without additional aggregation', + label: $t('tools/cidr-summarizer.modes.exactMerge.label'), + description: $t('tools/cidr-summarizer.modes.exactMerge.description'), }, { value: 'minimal-cover' as const, - label: 'Minimal Cover', - description: 'Find the smallest set of CIDR blocks that covers all inputs', + label: $t('tools/cidr-summarizer.modes.minimalCover.label'), + description: $t('tools/cidr-summarizer.modes.minimalCover.description'), }, - ]; + ]); - const examples = [ + const examples = $derived([ { - label: 'Mixed IPv4/IPv6', + label: $t('tools/cidr-summarizer.examples.mixedIPv4v6'), content: `192.168.1.0/24 10.0.0.0/16 2001:db8::/32 ::1`, }, { - label: 'Overlapping Ranges', + label: $t('tools/cidr-summarizer.examples.overlappingRanges'), content: `192.168.1.0-192.168.1.100 192.168.1.50-192.168.1.200 192.168.2.0/24`, }, { - label: 'Single IPs', + label: $t('tools/cidr-summarizer.examples.singleIPs'), content: `10.0.0.1 10.0.0.2 10.0.0.3 10.0.0.4`, }, { - label: 'Mode Comparison', + label: $t('tools/cidr-summarizer.examples.modeComparison'), content: `192.168.1.1 192.168.1.3 192.168.1.5 @@ -58,7 +59,7 @@ 192.168.1.9-192.168.1.12`, }, { - label: 'Complex Mix', + label: $t('tools/cidr-summarizer.examples.complexMix'), content: `172.16.0.0/12 192.168.1.1 192.168.1.5-192.168.1.10 @@ -66,7 +67,7 @@ 2001:db8::/48 fe80::/10`, }, - ]; + ]); /* Set example content */ function setExample(content: string) { @@ -138,15 +139,15 @@ fe80::/10`, <div class="card"> <header class="card-header"> - <h2>CIDR Summarization Tool</h2> + <h2>{$t('tools/cidr-summarizer.title')}</h2> <p> - Convert mixed IP addresses, CIDR blocks, and ranges into optimized CIDR prefixes with separate IPv4/IPv6 results. + {$t('tools/cidr-summarizer.description')} </p> </header> <!-- Mode Selection --> <div class="mode-section"> - <h3>Summarization Mode</h3> + <h3>{$t('tools/cidr-summarizer.modes.title')}</h3> <div class="tabs"> {#each modes as modeOption (modeOption.value)} <button @@ -166,11 +167,11 @@ fe80::/10`, <!-- Input Section --> <div class="input-section"> - <h3>Input Data</h3> + <h3>{$t('tools/cidr-summarizer.input.title')}</h3> <div class="form-group"> <label for="input-text"> - Enter IP addresses, CIDR blocks, or ranges (one per line) - <Tooltip text="Supports: single IPs (192.168.1.1), CIDR blocks (10.0.0.0/8), ranges (172.16.0.1-172.16.0.100)"> + {$t('tools/cidr-summarizer.input.label')} + <Tooltip text={$t('tools/cidr-summarizer.input.tooltip')}> <Icon name="help" size="sm" /> </Tooltip> </label> @@ -178,20 +179,20 @@ fe80::/10`, <textarea id="input-text" bind:value={inputText} - placeholder="192.168.1.0/24 10.0.0.1-10.0.0.10 2001:db8::/32" + placeholder={$t('tools/cidr-summarizer.input.placeholder')} class="input-textarea" rows="8" ></textarea> <button type="button" class="btn btn-secondary btn-sm clear-btn" onclick={clearInput}> - <Tooltip text="Clear input"><Icon name="trash" size="sm" /></Tooltip> + <Tooltip text={$t('tools/cidr-summarizer.input.clearTooltip')}><Icon name="trash" size="sm" /></Tooltip> </button> </div> </div> <!-- Examples --> <div class="examples-section"> - <h4>Quick Examples</h4> + <h4>{$t('tools/cidr-summarizer.examples.title')}</h4> <div class="examples-grid"> {#each examples as example (example.label)} <button @@ -212,7 +213,7 @@ fe80::/10`, <div class="results-section"> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Parsing Errors</h3> + <h3>{$t('tools/cidr-summarizer.results.errorsTitle')}</h3> <ul class="error-list"> {#each result.errors as error (error)} <li>{error}</li> @@ -223,8 +224,8 @@ fe80::/10`, {#if result.ipv4.length > 0 || result.ipv6.length > 0} <div class="summary-header"> - <h3>Summarization Results</h3> - <Tooltip text="Copy all IPv4 and IPv6 results to clipboard"> + <h3>{$t('tools/cidr-summarizer.results.title')}</h3> + <Tooltip text={$t('tools/cidr-summarizer.results.copyAllTooltip')}> <button type="button" class="btn btn-primary btn-sm" @@ -232,7 +233,7 @@ fe80::/10`, onclick={copyAllResults} > <SvgIcon icon={clipboard.isCopied('all-results') ? 'check' : 'clipboard'} size="md" /> - Copy All + {$t('tools/cidr-summarizer.results.copyAll')} </button> </Tooltip> </div> @@ -242,8 +243,8 @@ fe80::/10`, {#if result.ipv4.length > 0} <div class="result-panel ipv4"> <div class="panel-header"> - <h4>IPv4 CIDR Blocks ({result.ipv4.length})</h4> - <Tooltip text="Copy all IPv4 CIDR blocks to clipboard"> + <h4>{$t('tools/cidr-summarizer.results.ipv4Title', { count: result.ipv4.length })}</h4> + <Tooltip text={$t('tools/cidr-summarizer.results.ipv4Tooltip')}> <button type="button" class="btn btn-icon" @@ -258,7 +259,7 @@ fe80::/10`, {#each result.ipv4 as cidr (cidr)} <div class="cidr-item"> <code class="cidr-block">{cidr}</code> - <Tooltip text="Copy this CIDR block to clipboard"> + <Tooltip text={$t('tools/cidr-summarizer.results.copyCIDRTooltip')}> <button type="button" class="btn btn-icon btn-xs" @@ -278,8 +279,8 @@ fe80::/10`, {#if result.ipv6.length > 0} <div class="result-panel ipv6"> <div class="panel-header"> - <h4>IPv6 CIDR Blocks ({result.ipv6.length})</h4> - <Tooltip text="Copy all IPv6 CIDR blocks to clipboard"> + <h4>{$t('tools/cidr-summarizer.results.ipv6Title', { count: result.ipv6.length })}</h4> + <Tooltip text={$t('tools/cidr-summarizer.results.ipv6Tooltip')}> <button type="button" class="btn btn-icon" @@ -294,7 +295,7 @@ fe80::/10`, {#each result.ipv6 as cidr (cidr)} <div class="cidr-item"> <code class="cidr-block">{cidr}</code> - <Tooltip text="Copy this CIDR block to clipboard"> + <Tooltip text={$t('tools/cidr-summarizer.results.copyCIDRTooltip')}> <button type="button" class="btn btn-icon btn-xs" @@ -313,35 +314,35 @@ fe80::/10`, <!-- Statistics --> <div class="stats-section"> - <h4>Summarization Statistics</h4> + <h4>{$t('tools/cidr-summarizer.stats.title')}</h4> <div class="stats-grid"> <div class="stat-card"> - <Tooltip text="Number of individual IPv4 items in the original input"> - <span class="stat-label">Original IPv4 Items</span> + <Tooltip text={$t('tools/cidr-summarizer.stats.originalIPv4Items.tooltip')}> + <span class="stat-label">{$t('tools/cidr-summarizer.stats.originalIPv4Items.label')}</span> </Tooltip> <span class="stat-value">{result.stats.originalIpv4Count}</span> </div> <div class="stat-card"> - <Tooltip text="Number of CIDR blocks after IPv4 summarization"> - <span class="stat-label">Summarized IPv4 Blocks</span> + <Tooltip text={$t('tools/cidr-summarizer.stats.summarizedIPv4Blocks.tooltip')}> + <span class="stat-label">{$t('tools/cidr-summarizer.stats.summarizedIPv4Blocks.label')}</span> </Tooltip> <span class="stat-value">{result.stats.summarizedIpv4Count}</span> </div> <div class="stat-card"> - <Tooltip text="Number of individual IPv6 items in the original input"> - <span class="stat-label">Original IPv6 Items</span> + <Tooltip text={$t('tools/cidr-summarizer.stats.originalIPv6Items.tooltip')}> + <span class="stat-label">{$t('tools/cidr-summarizer.stats.originalIPv6Items.label')}</span> </Tooltip> <span class="stat-value">{result.stats.originalIpv6Count}</span> </div> <div class="stat-card"> - <Tooltip text="Number of CIDR blocks after IPv6 summarization"> - <span class="stat-label">Summarized IPv6 Blocks</span> + <Tooltip text={$t('tools/cidr-summarizer.stats.summarizedIPv6Blocks.tooltip')}> + <span class="stat-label">{$t('tools/cidr-summarizer.stats.summarizedIPv6Blocks.label')}</span> </Tooltip> <span class="stat-value">{result.stats.summarizedIpv6Count}</span> </div> <div class="stat-card"> - <Tooltip text="Total number of individual IP addresses covered by all summarized blocks"> - <span class="stat-label">Total Addresses Covered</span> + <Tooltip text={$t('tools/cidr-summarizer.stats.totalAddressesCovered.tooltip')}> + <span class="stat-label">{$t('tools/cidr-summarizer.stats.totalAddressesCovered.label')}</span> </Tooltip> <span class="stat-value large">{result.stats.totalAddressesCovered}</span> </div> diff --git a/src/lib/components/tools/ClientIDOption61.svelte b/src/lib/components/tools/ClientIDOption61.svelte index a718b4b2..b5970c9c 100644 --- a/src/lib/components/tools/ClientIDOption61.svelte +++ b/src/lib/components/tools/ClientIDOption61.svelte @@ -15,13 +15,14 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let activeTab = $state<'build' | 'decode'>('build'); - const navOptions = [ - { value: 'build' as const, label: 'Build', icon: 'settings' }, - { value: 'decode' as const, label: 'Decode', icon: 'code' }, - ]; + const navOptions = $derived([ + { value: 'build' as const, label: $t('tools/clientid-option61.nav.build'), icon: 'settings' }, + { value: 'decode' as const, label: $t('tools/clientid-option61.nav.decode'), icon: 'code' }, + ]); let mode = $state<'hardware' | 'opaque'>('hardware'); let hardwareType = $state<number>(HARDWARE_TYPES.ETHERNET); @@ -159,8 +160,8 @@ </script> <ToolContentContainer - title="DHCPv4 Client Identifier (Option 61)" - description="Build and decode DHCPv4 Client Identifier (Option 61) with hardware type + MAC address or arbitrary opaque data per RFC 2132." + title={$t('tools/clientid-option61.title')} + description={$t('tools/clientid-option61.subtitle')} {navOptions} bind:selectedNav={activeTab} > @@ -185,18 +186,18 @@ {#if activeTab === 'build'} <div class="card input-card"> <div class="card-header"> - <h3>Build Client Identifier</h3> - <p class="help-text">Configure DHCPv4 Client Identifier for device identification</p> + <h3>{$t('tools/clientid-option61.build.title')}</h3> + <p class="help-text">{$t('tools/clientid-option61.build.helpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="mode"> <Icon name="settings" size="sm" /> - Mode + {$t('tools/clientid-option61.build.mode.label')} </label> <select id="mode" bind:value={mode}> - <option value="hardware">Hardware Type + MAC Address</option> - <option value="opaque">Opaque Data (Text or Hex)</option> + <option value="hardware">{$t('tools/clientid-option61.build.mode.hardware')}</option> + <option value="opaque">{$t('tools/clientid-option61.build.mode.opaque')}</option> </select> </div> @@ -204,29 +205,52 @@ <div class="input-group"> <label for="hardware-type"> <Icon name="cpu" size="sm" /> - Hardware Type + {$t('tools/clientid-option61.build.hardwareType.label')} </label> <select id="hardware-type" bind:value={hardwareType}> - <option value={HARDWARE_TYPES.ETHERNET}>Ethernet (1)</option> - <option value={HARDWARE_TYPES.EXPERIMENTAL_ETHERNET}>Experimental Ethernet (2)</option> - <option value={HARDWARE_TYPES.IEEE_802}>IEEE 802 (6)</option> - <option value={HARDWARE_TYPES.ARCNET}>ARCNET (7)</option> - <option value={HARDWARE_TYPES.FRAME_RELAY}>Frame Relay (15)</option> - <option value={HARDWARE_TYPES.ATM}>ATM (16)</option> - <option value={HARDWARE_TYPES.HDLC}>HDLC (17)</option> - <option value={HARDWARE_TYPES.FIBRE_CHANNEL}>Fibre Channel (18)</option> - <option value={HARDWARE_TYPES.IEEE_1394}>IEEE 1394 (24)</option> - <option value={HARDWARE_TYPES.INFINIBAND}>InfiniBand (32)</option> + <option value={HARDWARE_TYPES.ETHERNET} + >{$t('tools/clientid-option61.build.hardwareType.options.ethernet')}</option + > + <option value={HARDWARE_TYPES.EXPERIMENTAL_ETHERNET} + >{$t('tools/clientid-option61.build.hardwareType.options.experimentalEthernet')}</option + > + <option value={HARDWARE_TYPES.IEEE_802} + >{$t('tools/clientid-option61.build.hardwareType.options.ieee802')}</option + > + <option value={HARDWARE_TYPES.ARCNET} + >{$t('tools/clientid-option61.build.hardwareType.options.arcnet')}</option + > + <option value={HARDWARE_TYPES.FRAME_RELAY} + >{$t('tools/clientid-option61.build.hardwareType.options.frameRelay')}</option + > + <option value={HARDWARE_TYPES.ATM}>{$t('tools/clientid-option61.build.hardwareType.options.atm')}</option> + <option value={HARDWARE_TYPES.HDLC} + >{$t('tools/clientid-option61.build.hardwareType.options.hdlc')}</option + > + <option value={HARDWARE_TYPES.FIBRE_CHANNEL} + >{$t('tools/clientid-option61.build.hardwareType.options.fibreChannel')}</option + > + <option value={HARDWARE_TYPES.IEEE_1394} + >{$t('tools/clientid-option61.build.hardwareType.options.ieee1394')}</option + > + <option value={HARDWARE_TYPES.INFINIBAND} + >{$t('tools/clientid-option61.build.hardwareType.options.infiniband')}</option + > </select> </div> <div class="input-group"> <label for="mac-address"> <Icon name="hash" size="sm" /> - MAC Address + {$t('tools/clientid-option61.build.macAddress.label')} </label> - <input id="mac-address" type="text" bind:value={macAddress} placeholder="00:0c:29:4f:a3:d2" /> - <small>Hardware address in any common format</small> + <input + id="mac-address" + type="text" + bind:value={macAddress} + placeholder={$t('tools/clientid-option61.build.macAddress.placeholder')} + /> + <small>{$t('tools/clientid-option61.build.macAddress.helpText')}</small> </div> {/if} @@ -234,26 +258,34 @@ <div class="input-group"> <label for="opaque-format"> <Icon name="code" size="sm" /> - Data Format + {$t('tools/clientid-option61.build.dataFormat.label')} </label> <select id="opaque-format" bind:value={opaqueFormat}> - <option value="text">Text (ASCII)</option> - <option value="hex">Hexadecimal</option> + <option value="text">{$t('tools/clientid-option61.build.dataFormat.text')}</option> + <option value="hex">{$t('tools/clientid-option61.build.dataFormat.hex')}</option> </select> </div> <div class="input-group"> <label for="opaque-data"> <Icon name="edit" size="sm" /> - {opaqueFormat === 'hex' ? 'Hex Data' : 'Text Data'} + {opaqueFormat === 'hex' + ? $t('tools/clientid-option61.build.opaqueData.labelHex') + : $t('tools/clientid-option61.build.opaqueData.labelText')} </label> <input id="opaque-data" type="text" bind:value={opaqueData} - placeholder={opaqueFormat === 'hex' ? '0123456789abcdef' : 'client-device-001'} + placeholder={opaqueFormat === 'hex' + ? $t('tools/clientid-option61.build.opaqueData.placeholderHex') + : $t('tools/clientid-option61.build.opaqueData.placeholderText')} /> - <small>{opaqueFormat === 'hex' ? 'Hexadecimal string (even length)' : 'Plain text identifier'}</small> + <small + >{opaqueFormat === 'hex' + ? $t('tools/clientid-option61.build.opaqueData.helpTextHex') + : $t('tools/clientid-option61.build.opaqueData.helpTextText')}</small + > </div> {/if} </div> @@ -261,17 +293,22 @@ {:else} <div class="card input-card"> <div class="card-header"> - <h3>Decode Client Identifier</h3> - <p class="help-text">Decode hex-encoded Client Identifier back to fields</p> + <h3>{$t('tools/clientid-option61.decode.title')}</h3> + <p class="help-text">{$t('tools/clientid-option61.decode.helpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="decode-hex"> <Icon name="code" size="sm" /> - Hex Data + {$t('tools/clientid-option61.decode.hexData.label')} </label> - <input id="decode-hex" type="text" bind:value={decodeHex} placeholder="01000c294fa3d2" /> - <small>Paste hex-encoded Client Identifier to decode</small> + <input + id="decode-hex" + type="text" + bind:value={decodeHex} + placeholder={$t('tools/clientid-option61.decode.hexData.placeholder')} + /> + <small>{$t('tools/clientid-option61.decode.hexData.helpText')}</small> </div> </div> </div> @@ -279,7 +316,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/clientid-option61.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -291,14 +328,22 @@ {#if activeTab === 'build' && buildResult && validationErrors.length === 0} <div class="card results"> - <h3>Generated Client Identifier</h3> + <h3>{$t('tools/clientid-option61.results.buildTitle')}</h3> <div class="summary-card"> - <div><strong>Mode:</strong> {buildResult.mode === 'hardware' ? 'Hardware Type + MAC' : 'Opaque Data'}</div> - <div><strong>Length:</strong> {buildResult.length} bytes</div> + <div> + <strong>{$t('tools/clientid-option61.results.mode')}</strong> + {buildResult.mode === 'hardware' + ? $t('tools/clientid-option61.results.modeHardware') + : $t('tools/clientid-option61.results.modeOpaque')} + </div> + <div> + <strong>{$t('tools/clientid-option61.results.length')}</strong> + {$t('tools/clientid-option61.results.lengthBytes', { length: buildResult.length })} + </div> </div> - {#each [{ title: 'Hexadecimal', content: buildResult.hex, key: 'hex' }, { title: 'Wire Format (Spaced)', content: buildResult.wireFormat, key: 'wire' }] as output (output.key)} + {#each [{ title: $t('tools/clientid-option61.results.outputs.hexadecimal'), content: buildResult.hex, key: 'hex' }, { title: $t('tools/clientid-option61.results.outputs.wireFormat'), content: buildResult.wireFormat, key: 'wire' }] as output (output.key)} <div class="output-group"> <div class="output-header"> <h4>{output.title}</h4> @@ -309,7 +354,9 @@ onclick={() => clipboard.copy(output.content, output.key)} > <Icon name={clipboard.isCopied(output.key) ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied(output.key) ? 'Copied' : 'Copy'} + {clipboard.isCopied(output.key) + ? $t('tools/clientid-option61.buttons.copied') + : $t('tools/clientid-option61.buttons.copy')} </button> </div> <pre class="output-value code-block">{output.content}</pre> @@ -318,7 +365,7 @@ {#if buildResult.breakdown && buildResult.breakdown.length > 0} <div class="breakdown-section"> - <h4>Breakdown</h4> + <h4>{$t('tools/clientid-option61.results.breakdown')}</h4> {#each buildResult.breakdown as item, i (i)} <div class="breakdown-item"> <div class="breakdown-label">{item.field}</div> @@ -334,7 +381,7 @@ {/if} </div> - {#each [{ title: 'ISC DHCPd Configuration', content: buildResult.configExamples?.iscDhcpd, key: 'isc' }, { title: 'Kea DHCPv4 Configuration', content: buildResult.configExamples?.keaDhcp4, key: 'kea' }] as config (config.key)} + {#each [{ title: $t('tools/clientid-option61.results.configs.iscDhcpd'), content: buildResult.configExamples?.iscDhcpd, key: 'isc' }, { title: $t('tools/clientid-option61.results.configs.keaDhcp4'), content: buildResult.configExamples?.keaDhcp4, key: 'kea' }] as config (config.key)} {#if config.content} <div class="card results"> <div class="card-header-with-action"> @@ -346,7 +393,9 @@ onclick={() => clipboard.copy(config.content!, config.key)} > <Icon name={clipboard.isCopied(config.key) ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied(config.key) ? 'Copied' : 'Copy'} + {clipboard.isCopied(config.key) + ? $t('tools/clientid-option61.buttons.copied') + : $t('tools/clientid-option61.buttons.copy')} </button> </div> <pre class="output-value code-block">{config.content}</pre> @@ -357,30 +406,36 @@ {#if activeTab === 'decode' && decodeResult && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Client Identifier</h3> + <h3>{$t('tools/clientid-option61.results.decodeTitle')}</h3> <div class="summary-card"> <div> - <strong>Detected Mode:</strong> - {decodeResult.mode === 'hardware' ? 'Hardware Type + MAC' : 'Opaque Data'} + <strong>{$t('tools/clientid-option61.results.detectedMode')}</strong> + {decodeResult.mode === 'hardware' + ? $t('tools/clientid-option61.results.modeHardware') + : $t('tools/clientid-option61.results.modeOpaque')} + </div> + <div> + <strong>{$t('tools/clientid-option61.results.length')}</strong> + {$t('tools/clientid-option61.results.lengthBytes', { length: decodeResult.length })} </div> - <div><strong>Length:</strong> {decodeResult.length} bytes</div> </div> {#if decodeResult.decoded} <div class="decoded-fields"> {#if decodeResult.decoded.hardwareType !== undefined} <div class="decoded-field"> - <div class="field-label">Hardware Type</div> + <div class="field-label">{$t('tools/clientid-option61.results.fields.hardwareType')}</div> <div class="field-value"> - {decodeResult.decoded.hardwareType} ({decodeResult.decoded.hardwareTypeName || 'Unknown'}) + {decodeResult.decoded.hardwareType} ({decodeResult.decoded.hardwareTypeName || + $t('tools/clientid-option61.results.fields.hardwareTypeUnknown')}) </div> </div> {/if} {#if decodeResult.decoded.macAddress} <div class="decoded-field"> - <div class="field-label">MAC Address</div> + <div class="field-label">{$t('tools/clientid-option61.results.fields.macAddress')}</div> <div class="field-value"> <code>{decodeResult.decoded.macAddress}</code> <button @@ -396,7 +451,7 @@ {#if decodeResult.decoded.opaqueData} <div class="decoded-field"> - <div class="field-label">Opaque Data</div> + <div class="field-label">{$t('tools/clientid-option61.results.fields.opaqueData')}</div> <div class="field-value"> <code>{decodeResult.decoded.opaqueData}</code> <button @@ -414,7 +469,7 @@ {#if decodeResult.breakdown && decodeResult.breakdown.length > 0} <div class="breakdown-section"> - <h4>Breakdown</h4> + <h4>{$t('tools/clientid-option61.results.breakdown')}</h4> {#each decodeResult.breakdown as item, i (i)} <div class="breakdown-item"> <div class="breakdown-label">{item.field}</div> diff --git a/src/lib/components/tools/DHCPFingerprinting.svelte b/src/lib/components/tools/DHCPFingerprinting.svelte index 2de1580c..97f77e06 100644 --- a/src/lib/components/tools/DHCPFingerprinting.svelte +++ b/src/lib/components/tools/DHCPFingerprinting.svelte @@ -17,6 +17,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables/useClipboard.svelte'; + import { t } from '$lib/stores/language'; const clipboard = useClipboard(1800); @@ -31,55 +32,55 @@ let reverseQuery = $state<string>(''); let reverseResults = $state<typeof FINGERPRINT_DATABASE>([]); - const examples = [ + const examples = $derived([ { - label: 'Windows 10/11', - description: 'Modern Windows desktop', + label: $t('tools/dhcp-fingerprinting.examples.windows.label'), + description: $t('tools/dhcp-fingerprinting.examples.windows.description'), params: '1,3,6,15,31,33,43,44,46,47,119,121,249,252', vendor: '', }, { - label: 'macOS/iOS', - description: 'Apple device', + label: $t('tools/dhcp-fingerprinting.examples.macOS.label'), + description: $t('tools/dhcp-fingerprinting.examples.macOS.description'), params: '1,3,6,15,119,252', vendor: '', }, { - label: 'Android', - description: 'Android smartphone', + label: $t('tools/dhcp-fingerprinting.examples.android.label'), + description: $t('tools/dhcp-fingerprinting.examples.android.description'), params: '1,3,6,15,26,28,51,58,59,43', vendor: 'dhcpcd', }, { - label: 'Linux (dhclient)', - description: 'Linux with ISC dhclient', + label: $t('tools/dhcp-fingerprinting.examples.linux.label'), + description: $t('tools/dhcp-fingerprinting.examples.linux.description'), params: '1,3,6,15,26,28,42', vendor: '', }, { - label: 'Cisco IP Phone', - description: 'Cisco VoIP device', + label: $t('tools/dhcp-fingerprinting.examples.ciscoPhone.label'), + description: $t('tools/dhcp-fingerprinting.examples.ciscoPhone.description'), params: '1,3,6,12,15,28,42,66,67,120,150', vendor: 'Cisco', }, { - label: 'Raspberry Pi', - description: 'Raspberry Pi OS (Debian)', + label: $t('tools/dhcp-fingerprinting.examples.raspberryPi.label'), + description: $t('tools/dhcp-fingerprinting.examples.raspberryPi.description'), params: '1,3,6,12,15,28,40,41,42', vendor: '', }, { - label: 'Samsung Smart TV', - description: 'Smart TV device', + label: $t('tools/dhcp-fingerprinting.examples.samsungTV.label'), + description: $t('tools/dhcp-fingerprinting.examples.samsungTV.description'), params: '1,3,6,12,15,28,40,41,42,119', vendor: 'SAMSUNG', }, - ]; + ]); - const navOptions = [ - { value: 'lookup', label: 'Fingerprint Lookup', icon: 'fingerprint' }, - { value: 'reverse', label: 'Device Search', icon: 'monitor' }, - ]; + const navOptions = $derived([ + { value: 'lookup' as const, label: $t('tools/dhcp-fingerprinting.tabs.lookup'), icon: 'fingerprint' }, + { value: 'reverse' as const, label: $t('tools/dhcp-fingerprinting.tabs.reverse'), icon: 'monitor' }, + ]); function loadExample(ex: (typeof examples)[0]) { activeTab = 'lookup'; @@ -179,8 +180,8 @@ </script> <ToolContentContainer - title="DHCP Fingerprinting Database" - description="Identify devices based on their DHCP fingerprints using Parameter Request List (Option 55) and Vendor Class Identifier (Option 60). Database contains {FINGERPRINT_DATABASE.length} known fingerprints from common devices, operating systems, and IoT equipment." + title={$t('tools/dhcp-fingerprinting.title')} + description={$t('tools/dhcp-fingerprinting.subtitle', { count: FINGERPRINT_DATABASE.length })} {navOptions} bind:selectedNav={activeTab} > @@ -193,35 +194,35 @@ /> <div class="card input-card"> - <h3>Device Fingerprint Lookup</h3> + <h3>{$t('tools/dhcp-fingerprinting.lookup.title')}</h3> <div class="form-group"> - <label for="param-list">Parameter Request List (Option 55)</label> + <label for="param-list">{$t('tools/dhcp-fingerprinting.lookup.parameterList.label')}</label> <input id="param-list" type="text" bind:value={parameterInput} - placeholder="e.g., 1,3,6,15 or 0103060f or 1 3 6 15" + placeholder={$t('tools/dhcp-fingerprinting.lookup.parameterList.placeholder')} class="input" /> - <span class="hint">Enter as comma-separated, hex, or space-separated numbers</span> + <span class="hint">{$t('tools/dhcp-fingerprinting.lookup.parameterList.hint')}</span> </div> <div class="form-group"> - <label for="vendor-class">Vendor Class Identifier (Option 60) - Optional</label> + <label for="vendor-class">{$t('tools/dhcp-fingerprinting.lookup.vendorClass.label')}</label> <input id="vendor-class" type="text" bind:value={vendorClass} - placeholder="e.g., MSFT, dhcpcd, Cisco" + placeholder={$t('tools/dhcp-fingerprinting.lookup.vendorClass.placeholder')} class="input" /> - <span class="hint">Helps improve match accuracy</span> + <span class="hint">{$t('tools/dhcp-fingerprinting.lookup.vendorClass.hint')}</span> </div> {#if error} <div class="error-card"> - <strong>Error:</strong> + <strong>{$t('tools/dhcp-fingerprinting.lookup.error')}</strong> <p>{error}</p> </div> {/if} @@ -229,31 +230,35 @@ {#if parsedParams.length > 0} <div class="card result-card"> - <h3>Requested DHCP Options</h3> + <h3>{$t('tools/dhcp-fingerprinting.lookup.requestedOptions.title')}</h3> <div class="result-item"> - <span class="label">Parameter List:</span> + <span class="label">{$t('tools/dhcp-fingerprinting.lookup.requestedOptions.parameterList')}</span> <code class="code-value">{formatParameterListDisplay(parsedParams)}</code> <button class="btn-copy" class:copied={clipboard.isCopied('param-list')} onclick={() => clipboard.copy(formatParameterListDisplay(parsedParams), 'param-list')} - aria-label="Copy" + aria-label={$t('tools/dhcp-fingerprinting.buttons.copyAria')} > - {clipboard.isCopied('param-list') ? 'Copied' : 'Copy'} + {clipboard.isCopied('param-list') + ? $t('tools/dhcp-fingerprinting.buttons.copied') + : $t('tools/dhcp-fingerprinting.buttons.copy')} </button> </div> <div class="result-item"> - <span class="label">Hex Encoded:</span> + <span class="label">{$t('tools/dhcp-fingerprinting.lookup.requestedOptions.hexEncoded')}</span> <code class="code-value">{formatParameterListToHex(parsedParams)}</code> <button class="btn-copy" class:copied={clipboard.isCopied('param-hex')} onclick={() => clipboard.copy(formatParameterListToHex(parsedParams), 'param-hex')} - aria-label="Copy hex" + aria-label={$t('tools/dhcp-fingerprinting.buttons.copyHexAria')} > - {clipboard.isCopied('param-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('param-hex') + ? $t('tools/dhcp-fingerprinting.buttons.copied') + : $t('tools/dhcp-fingerprinting.buttons.copy')} </button> </div> @@ -261,14 +266,16 @@ {#each parsedParams as param, i (i)} <div class="option-badge"> <span class="option-num">{param}</span> - <span class="option-name">{DHCP_OPTION_NAMES[param] || 'Unknown'}</span> + <span class="option-name" + >{DHCP_OPTION_NAMES[param] || $t('tools/dhcp-fingerprinting.lookup.requestedOptions.unknown')}</span + > </div> {/each} </div> {#if analysis && analysis.warnings.length > 0} <div class="card warning-card"> - <h3>Security Warnings</h3> + <h3>{$t('tools/dhcp-fingerprinting.lookup.securityWarnings.title')}</h3> {#each analysis.warnings as warning, i (i)} <div class="warning-item">⚠️ {warning}</div> {/each} @@ -277,24 +284,34 @@ {#if analysis && (analysis.unusual.length > 0 || (analysis.missing.length > 0 && matches.length > 0))} <div class="card analysis-card"> - <h3>Option Analysis</h3> + <h3>{$t('tools/dhcp-fingerprinting.lookup.optionAnalysis.title')}</h3> {#if analysis.unusual.length > 0} <div class="info-section"> - <h4>Unusual Options Detected</h4> - <p>These options may indicate vendor-specific configurations:</p> + <h4>{$t('tools/dhcp-fingerprinting.lookup.optionAnalysis.unusualTitle')}</h4> + <p>{$t('tools/dhcp-fingerprinting.lookup.optionAnalysis.unusualDescription')}</p> <code class="code-value" - >{analysis.unusual.map((o) => `${o} (${DHCP_OPTION_NAMES[o] || 'Unknown'})`).join(', ')}</code + >{analysis.unusual + .map( + (o) => + `${o} (${DHCP_OPTION_NAMES[o] || $t('tools/dhcp-fingerprinting.lookup.requestedOptions.unknown')})`, + ) + .join(', ')}</code > </div> {/if} {#if analysis.missing.length > 0 && matches.length > 0} <div class="info-section"> - <h4>Missing Options (vs. Best Match)</h4> - <p>Options present in the best match but not in your fingerprint:</p> + <h4>{$t('tools/dhcp-fingerprinting.lookup.optionAnalysis.missingTitle')}</h4> + <p>{$t('tools/dhcp-fingerprinting.lookup.optionAnalysis.missingDescription')}</p> <code class="code-value" - >{analysis.missing.map((o) => `${o} (${DHCP_OPTION_NAMES[o] || 'Unknown'})`).join(', ')}</code + >{analysis.missing + .map( + (o) => + `${o} (${DHCP_OPTION_NAMES[o] || $t('tools/dhcp-fingerprinting.lookup.requestedOptions.unknown')})`, + ) + .join(', ')}</code > </div> {/if} @@ -306,10 +323,14 @@ {#if matches.length > 0} <div class="card matches-card"> <div class="matches-header"> - <h3>Matching Devices ({matches.length})</h3> + <h3>{$t('tools/dhcp-fingerprinting.lookup.matches.title', { count: matches.length })}</h3> <div class="export-buttons"> - <button class="btn btn-secondary btn-sm" onclick={handleExportJSON}>Export JSON</button> - <button class="btn btn-secondary btn-sm" onclick={handleExportCSV}>Export CSV</button> + <button class="btn btn-secondary btn-sm" onclick={handleExportJSON} + >{$t('tools/dhcp-fingerprinting.lookup.matches.exportJSON')}</button + > + <button class="btn btn-secondary btn-sm" onclick={handleExportCSV} + >{$t('tools/dhcp-fingerprinting.lookup.matches.exportCSV')}</button + > </div> </div> @@ -327,32 +348,36 @@ <h4>{match.fingerprint.device}</h4> <span class="confidence"> {confidenceBadges[match.fingerprint.confidence] || ''} - {match.fingerprint.confidence} confidence + {$t('tools/dhcp-fingerprinting.lookup.matches.confidence', { level: match.fingerprint.confidence })} </span> </div> <div class="match-score"> - <span class="score-value">{match.matchScore.toFixed(0)}%</span> - <span class="score-label">Match</span> + <span class="score-value" + >{$t('tools/dhcp-fingerprinting.lookup.matches.matchScore', { + score: match.matchScore.toFixed(0), + })}</span + > + <span class="score-label">{$t('tools/dhcp-fingerprinting.lookup.matches.matchLabel')}</span> </div> </div> <div class="match-details"> <div class="detail-row"> - <span class="detail-label">OS:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.lookup.matches.osLabel')}</span> <span>{match.fingerprint.os}</span> </div> <div class="detail-row"> - <span class="detail-label">Matched On:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.lookup.matches.matchedOnLabel')}</span> <span>{match.matchedOn.join(', ')}</span> </div> {#if match.fingerprint.description} <div class="detail-row"> - <span class="detail-label">Description:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.lookup.matches.descriptionLabel')}</span> <span>{match.fingerprint.description}</span> </div> {/if} <div class="detail-row"> - <span class="detail-label">Known Parameters:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.lookup.matches.knownParamsLabel')}</span> <code class="code-small">{formatParameterListDisplay(match.fingerprint.parameterRequestList)}</code> </div> </div> @@ -362,36 +387,41 @@ </div> {:else if parsedParams.length > 0 && !error} <div class="card no-match-card"> - <h3>No Matches Found</h3> - <p>The provided fingerprint doesn't match any known devices in the database. This could be:</p> + <h3>{$t('tools/dhcp-fingerprinting.lookup.noMatches.title')}</h3> + <p>{$t('tools/dhcp-fingerprinting.lookup.noMatches.description')}</p> <ul> - <li>A custom DHCP client configuration</li> - <li>An uncommon device or operating system</li> - <li>A device with a modified DHCP request list</li> + <li>{$t('tools/dhcp-fingerprinting.lookup.noMatches.reasons.custom')}</li> + <li>{$t('tools/dhcp-fingerprinting.lookup.noMatches.reasons.uncommon')}</li> + <li>{$t('tools/dhcp-fingerprinting.lookup.noMatches.reasons.modified')}</li> </ul> - <p class="hint">Try adding the Vendor Class Identifier if available.</p> + <p class="hint">{$t('tools/dhcp-fingerprinting.lookup.noMatches.hint')}</p> </div> {/if} {:else} <div class="card input-card"> - <h3>Search by Device or OS</h3> + <h3>{$t('tools/dhcp-fingerprinting.reverse.title')}</h3> <div class="form-group"> - <label for="reverse-query">Search for Device/OS/Vendor</label> + <label for="reverse-query">{$t('tools/dhcp-fingerprinting.reverse.search.label')}</label> <input id="reverse-query" type="text" bind:value={reverseQuery} - placeholder="e.g., iPhone, Windows, Cisco, Printer..." + placeholder={$t('tools/dhcp-fingerprinting.reverse.search.placeholder')} class="input" /> - <span class="hint">Search the database by device name, OS, or vendor</span> + <span class="hint">{$t('tools/dhcp-fingerprinting.reverse.search.hint')}</span> </div> </div> {#if reverseResults.length > 0} <div class="card result-card"> - <h3>Found {reverseResults.length} Device{reverseResults.length > 1 ? 's' : ''}</h3> + <h3> + {$t('tools/dhcp-fingerprinting.reverse.results.title', { + count: reverseResults.length, + plural: reverseResults.length > 1 ? 's' : '', + })} + </h3> {#each reverseResults as device, i (i)} <div class="reverse-item"> @@ -405,37 +435,39 @@ <h4>{device.device}</h4> <span class="confidence"> {confidenceBadges[device.confidence] || ''} - {device.confidence} confidence + {$t('tools/dhcp-fingerprinting.reverse.results.confidence', { level: device.confidence })} </span> </div> <div class="reverse-details"> <div class="detail-row"> - <span class="detail-label">OS:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.reverse.results.osLabel')}</span> <span>{device.os}</span> </div> {#if device.description} <div class="detail-row"> - <span class="detail-label">Description:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.reverse.results.descriptionLabel')}</span> <span>{device.description}</span> </div> {/if} <div class="detail-row"> - <span class="detail-label">Parameter Request List:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.reverse.results.parameterListLabel')}</span> <code class="code-small">{formatParameterListDisplay(device.parameterRequestList)}</code> <button class="btn-copy" class:copied={clipboard.isCopied(`reverse-${i}`)} onclick={() => clipboard.copy(formatParameterListDisplay(device.parameterRequestList), `reverse-${i}`)} - aria-label="Copy parameter list" + aria-label={$t('tools/dhcp-fingerprinting.buttons.copyParamListAria')} > - {clipboard.isCopied(`reverse-${i}`) ? 'Copied' : 'Copy'} + {clipboard.isCopied(`reverse-${i}`) + ? $t('tools/dhcp-fingerprinting.buttons.copied') + : $t('tools/dhcp-fingerprinting.buttons.copy')} </button> </div> {#if device.vendorClassPattern} <div class="detail-row"> - <span class="detail-label">Vendor Class Pattern:</span> + <span class="detail-label">{$t('tools/dhcp-fingerprinting.reverse.results.vendorPatternLabel')}</span> <code class="code-small">{device.vendorClassPattern}</code> </div> {/if} @@ -445,8 +477,8 @@ </div> {:else if reverseQuery.trim()} <div class="card no-match-card"> - <h3>No Devices Found</h3> - <p>No devices matched "{reverseQuery}". Try a different search term.</p> + <h3>{$t('tools/dhcp-fingerprinting.reverse.noResults.title')}</h3> + <p>{$t('tools/dhcp-fingerprinting.reverse.noResults.description', { query: reverseQuery })}</p> </div> {/if} {/if} diff --git a/src/lib/components/tools/DHCPOption119Builder.svelte b/src/lib/components/tools/DHCPOption119Builder.svelte index 7fdd3117..082ccba9 100644 --- a/src/lib/components/tools/DHCPOption119Builder.svelte +++ b/src/lib/components/tools/DHCPOption119Builder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { buildOption119, parseOption119, @@ -13,10 +14,10 @@ type ParsedDomainSearch, } from '$lib/utils/dhcp-option119.js'; - const modeOptions = [ - { value: 'encode' as const, label: 'Encode', icon: 'wrench' }, - { value: 'decode' as const, label: 'Decode', icon: 'search' }, - ]; + const modeOptions = $derived([ + { value: 'encode' as const, label: $t('tools/dhcp-option119-builder.modes.encode'), icon: 'wrench' }, + { value: 'decode' as const, label: $t('tools/dhcp-option119-builder.modes.decode'), icon: 'search' }, + ]); let mode = $state<'encode' | 'decode'>('encode'); let config = $state<DomainSearchConfig>({ @@ -49,41 +50,41 @@ description: string; } - const encodeExamples: EncodeExample[] = [ + const encodeExamples = $derived<EncodeExample[]>([ { - label: 'Corporate', + label: $t('tools/dhcp-option119-builder.encodeExamples.corporate.label'), domains: ['corp.example.com', 'example.com'], - description: 'Corporate network with domain compression', + description: $t('tools/dhcp-option119-builder.encodeExamples.corporate.description'), }, { - label: 'Multi-site', + label: $t('tools/dhcp-option119-builder.encodeExamples.multiSite.label'), domains: ['site1.example.com', 'site2.example.com', 'example.com'], - description: 'Multiple sites sharing common suffix', + description: $t('tools/dhcp-option119-builder.encodeExamples.multiSite.description'), }, { - label: 'Development', + label: $t('tools/dhcp-option119-builder.encodeExamples.development.label'), domains: ['dev.example.com', 'staging.example.com', 'example.com'], - description: 'Development environments', + description: $t('tools/dhcp-option119-builder.encodeExamples.development.description'), }, - ]; + ]); - const decodeExamples: DecodeExample[] = [ + const decodeExamples = $derived<DecodeExample[]>([ { - label: 'Corporate', + label: $t('tools/dhcp-option119-builder.decodeExamples.corporate.label'), hexInput: '04636f7270076578616d706c6503636f6d00c005', - description: 'corp.example.com, example.com (with compression)', + description: $t('tools/dhcp-option119-builder.decodeExamples.corporate.description'), }, { - label: 'Multi-site', + label: $t('tools/dhcp-option119-builder.decodeExamples.multiSite.label'), hexInput: '057369746531076578616d706c6503636f6d00057369746532c006c006', - description: 'site1.example.com, site2.example.com, example.com', + description: $t('tools/dhcp-option119-builder.decodeExamples.multiSite.description'), }, { - label: 'Single Domain', + label: $t('tools/dhcp-option119-builder.decodeExamples.singleDomain.label'), hexInput: '076578616d706c6503636f6d00', - description: 'example.com (no compression)', + description: $t('tools/dhcp-option119-builder.decodeExamples.singleDomain.description'), }, - ]; + ]); // Reactive generation - use untrack to prevent infinite loop $effect(() => { @@ -120,49 +121,49 @@ // Validate domains if (cfg.domains.length === 0) { - domainErrors.push('At least one domain is required'); + domainErrors.push($t('tools/dhcp-option119-builder.errors.atLeastOneDomain')); } for (let i = 0; i < cfg.domains.length; i++) { const domain = cfg.domains[i]; if (!domain.trim()) { - domainErrors.push(`Domain ${i + 1}: Value is required`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.domainRequired', { number: i + 1 })); continue; } if (!/^[a-zA-Z0-9.-]+$/.test(domain)) { - domainErrors.push(`Domain ${i + 1}: Invalid characters (use only letters, numbers, dots, hyphens)`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.invalidCharacters', { number: i + 1 })); continue; } if (domain.startsWith('.') || domain.endsWith('.')) { - domainErrors.push(`Domain ${i + 1}: Cannot start or end with a dot`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.cannotStartOrEndWithDot', { number: i + 1 })); continue; } if (domain.includes('..')) { - domainErrors.push(`Domain ${i + 1}: Cannot contain consecutive dots`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.consecutiveDots', { number: i + 1 })); continue; } if (domain.length > 253) { - domainErrors.push(`Domain ${i + 1}: Exceeds maximum length of 253 characters`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.exceedsMaxLength', { number: i + 1 })); continue; } const labels = domain.split('.'); for (const label of labels) { if (label.length === 0) { - domainErrors.push(`Domain ${i + 1}: Empty label found`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.emptyLabel', { number: i + 1 })); break; } if (label.length > 63) { - domainErrors.push(`Domain ${i + 1}: Label "${label}" exceeds maximum length of 63 characters`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.labelTooLong', { number: i + 1, label })); break; } if (label.startsWith('-') || label.endsWith('-')) { - domainErrors.push(`Domain ${i + 1}: Label "${label}" cannot start or end with hyphen`); + domainErrors.push($t('tools/dhcp-option119-builder.errors.labelInvalidHyphen', { number: i + 1, label })); break; } } @@ -173,19 +174,19 @@ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; if (cfg.network.subnet && cfg.network.subnet.trim() && !ipv4Regex.test(cfg.network.subnet)) { - netErrors.push('Invalid subnet address'); + netErrors.push($t('tools/dhcp-option119-builder.errors.invalidSubnet')); } if (cfg.network.netmask && cfg.network.netmask.trim() && !ipv4Regex.test(cfg.network.netmask)) { - netErrors.push('Invalid netmask'); + netErrors.push($t('tools/dhcp-option119-builder.errors.invalidNetmask')); } if (cfg.network.rangeStart && cfg.network.rangeStart.trim() && !ipv4Regex.test(cfg.network.rangeStart)) { - netErrors.push('Invalid range start address'); + netErrors.push($t('tools/dhcp-option119-builder.errors.invalidRangeStart')); } if (cfg.network.rangeEnd && cfg.network.rangeEnd.trim() && !ipv4Regex.test(cfg.network.rangeEnd)) { - netErrors.push('Invalid range end address'); + netErrors.push($t('tools/dhcp-option119-builder.errors.invalidRangeEnd')); } } @@ -196,7 +197,9 @@ try { result = buildOption119(cfg); } catch (error) { - validationErrors = [error instanceof Error ? error.message : 'Encoding failed']; + validationErrors = [ + error instanceof Error ? error.message : $t('tools/dhcp-option119-builder.errors.encodingFailed'), + ]; result = null; } } else { @@ -212,7 +215,7 @@ } if (!/^[0-9a-fA-F\s:]+$/.test(decodeInput)) { - validationErrors = ['Invalid hex input: only hexadecimal characters allowed']; + validationErrors = [$t('tools/dhcp-option119-builder.errors.invalidHex')]; decodeResult = null; return; } @@ -221,7 +224,9 @@ validationErrors = []; decodeResult = parseOption119(decodeInput); } catch (error) { - validationErrors = [error instanceof Error ? error.message : 'Decoding failed']; + validationErrors = [ + error instanceof Error ? error.message : $t('tools/dhcp-option119-builder.errors.decodingFailed'), + ]; decodeResult = null; } } @@ -288,8 +293,8 @@ </script> <ToolContentContainer - title="DHCP Option 119 - Domain Search List" - description="Encode and decode Domain Search List (RFC 3397/6731) to/from RFC 1035 wire format with domain compression. Generate configurations for ISC dhcpd and Kea DHCP." + title={$t('tools/dhcp-option119-builder.title')} + description={$t('tools/dhcp-option119-builder.subtitle')} navOptions={modeOptions} bind:selectedNav={mode} > @@ -314,16 +319,18 @@ {#if mode === 'encode'} <div class="card input-card"> <div class="card-header"> - <h3>Domain List</h3> + <h3>{$t('tools/dhcp-option119-builder.encode.domainListTitle')}</h3> </div> <div class="card-content"> {#each config.domains as _, i (`domain-${i}`)} <div class="domain-group"> <div class="domain-header"> <h4> - <Icon name="globe" size="sm" />Domain {i + 1} + <Icon name="globe" size="sm" />{$t('tools/dhcp-option119-builder.encode.domain.title', { + number: i + 1, + })} </h4> - <!-- <label for="domain-{i}"> + <!-- <label for="domain-{i}"> Domain Name </label> --> {#if config.domains.length > 1} @@ -334,21 +341,26 @@ </div> <div class="input-group"> - <input id="domain-{i}" type="text" bind:value={config.domains[i]} placeholder="example.com" /> + <input + id="domain-{i}" + type="text" + bind:value={config.domains[i]} + placeholder={$t('tools/dhcp-option119-builder.encode.domain.placeholder')} + /> </div> </div> {/each} <button type="button" class="btn-add" onclick={addDomain}> <Icon name="plus" size="sm" /> - Add Domain + {$t('tools/dhcp-option119-builder.encode.addDomain')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option119-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -360,11 +372,11 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Encoded Option 119</h3> + <h3>{$t('tools/dhcp-option119-builder.results.encodeTitle')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcp-option119-builder.results.hexEncoded.title')}</h4> <button type="button" class="copy-btn" @@ -372,7 +384,9 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/dhcp-option119-builder.buttons.copied') + : $t('tools/dhcp-option119-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> @@ -380,7 +394,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcp-option119-builder.results.wireFormat.title')}</h4> <button type="button" class="copy-btn" @@ -388,15 +402,23 @@ onclick={() => clipboard.copy(result!.wireFormat, 'wire')} > <Icon name={clipboard.isCopied('wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') + ? $t('tools/dhcp-option119-builder.buttons.copied') + : $t('tools/dhcp-option119-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.wireFormat}</pre> </div> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.totalLength} bytes</div> - <div><strong>Domains:</strong> {result.domainList.length}</div> + <div> + <strong>{$t('tools/dhcp-option119-builder.results.summary.totalLength')}</strong> + {$t('tools/dhcp-option119-builder.results.summary.bytes', { length: result.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option119-builder.results.summary.domains')}</strong> + {result.domainList.length} + </div> </div> </div> {/if} @@ -406,25 +428,35 @@ {#if result} <div class="card input-card"> <div class="card-header"> - <h3>Network Settings (Optional)</h3> - <p class="help-text">Customize network values for configuration examples below</p> + <h3>{$t('tools/dhcp-option119-builder.encode.networkSettings.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option119-builder.encode.networkSettings.help')}</p> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet + {$t('tools/dhcp-option119-builder.encode.networkSettings.subnet.label')} </label> - <input id="subnet" type="text" bind:value={config.network!.subnet} placeholder="192.168.1.0" /> + <input + id="subnet" + type="text" + bind:value={config.network!.subnet} + placeholder={$t('tools/dhcp-option119-builder.encode.networkSettings.subnet.placeholder')} + /> </div> <div class="input-group"> <label for="netmask"> <Icon name="network" size="sm" /> - Netmask + {$t('tools/dhcp-option119-builder.encode.networkSettings.netmask.label')} </label> - <input id="netmask" type="text" bind:value={config.network!.netmask} placeholder="255.255.255.0" /> + <input + id="netmask" + type="text" + bind:value={config.network!.netmask} + placeholder={$t('tools/dhcp-option119-builder.encode.networkSettings.netmask.placeholder')} + /> </div> </div> @@ -432,24 +464,34 @@ <div class="input-group"> <label for="range-start"> <Icon name="arrow-right" size="sm" /> - Range Start + {$t('tools/dhcp-option119-builder.encode.networkSettings.rangeStart.label')} </label> - <input id="range-start" type="text" bind:value={config.network!.rangeStart} placeholder="192.168.1.100" /> + <input + id="range-start" + type="text" + bind:value={config.network!.rangeStart} + placeholder={$t('tools/dhcp-option119-builder.encode.networkSettings.rangeStart.placeholder')} + /> </div> <div class="input-group"> <label for="range-end"> <Icon name="arrow-right" size="sm" /> - Range End + {$t('tools/dhcp-option119-builder.encode.networkSettings.rangeEnd.label')} </label> - <input id="range-end" type="text" bind:value={config.network!.rangeEnd} placeholder="192.168.1.200" /> + <input + id="range-end" + type="text" + bind:value={config.network!.rangeEnd} + placeholder={$t('tools/dhcp-option119-builder.encode.networkSettings.rangeEnd.placeholder')} + /> </div> </div> </div> {#if networkValidationErrors.length > 0} <div class="network-errors"> - <h4>Network Settings Errors</h4> + <h4>{$t('tools/dhcp-option119-builder.encode.networkSettings.errorsTitle')}</h4> {#each networkValidationErrors as error, i (i)} <div class="network-error-item"> <Icon name="alert-triangle" size="sm" /> @@ -463,12 +505,12 @@ {#if result && networkValidationErrors.length === 0} <div class="card results"> - <h3>Configuration Examples</h3> + <h3>{$t('tools/dhcp-option119-builder.results.configExamplesTitle')}</h3> {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration</h4> + <h4>{$t('tools/dhcp-option119-builder.results.formats.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -476,7 +518,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-option119-builder.buttons.copied') + : $t('tools/dhcp-option119-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -486,7 +530,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration</h4> + <h4>{$t('tools/dhcp-option119-builder.results.formats.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -494,7 +538,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-option119-builder.buttons.copied') + : $t('tools/dhcp-option119-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -505,31 +551,31 @@ {:else} <div class="card input-card"> <div class="card-header"> - <h3>Decode Option 119 Hex</h3> + <h3>{$t('tools/dhcp-option119-builder.decode.title')}</h3> </div> <div class="card-content"> <div class="input-group"> <label for="decode-input"> <Icon name="code" size="sm" /> - Hex-Encoded Option 119 + {$t('tools/dhcp-option119-builder.decode.input.label')} </label> <textarea id="decode-input" bind:value={decodeInput} - placeholder="Enter hex string (e.g., 0765786d706c6503636f6d00)" + placeholder={$t('tools/dhcp-option119-builder.decode.input.placeholder')} rows="4" ></textarea> </div> <button type="button" class="btn-primary" onclick={decode}> <Icon name="search" size="sm" /> - Decode + {$t('tools/dhcp-option119-builder.decode.button')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option119-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -541,15 +587,21 @@ {#if decodeResult && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Domain Search List</h3> + <h3>{$t('tools/dhcp-option119-builder.results.decodeTitle')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {decodeResult.totalLength} bytes</div> - <div><strong>Domains Found:</strong> {decodeResult.domains.length}</div> + <div> + <strong>{$t('tools/dhcp-option119-builder.results.summary.totalLength')}</strong> + {$t('tools/dhcp-option119-builder.results.summary.bytes', { length: decodeResult.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option119-builder.results.domainsFound')}</strong> + {decodeResult.domains.length} + </div> </div> <div class="domains-section"> - <h4>Domain List</h4> + <h4>{$t('tools/dhcp-option119-builder.results.domainListTitle')}</h4> {#each decodeResult.domains as domain, i (i)} <div class="domain-item"> <Icon name="globe" size="sm" /> diff --git a/src/lib/components/tools/DHCPOption121Builder.svelte b/src/lib/components/tools/DHCPOption121Builder.svelte index f1bedc3c..8e877bdc 100644 --- a/src/lib/components/tools/DHCPOption121Builder.svelte +++ b/src/lib/components/tools/DHCPOption121Builder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { buildOption121, parseOption121, @@ -13,10 +14,10 @@ type ParsedClasslessRoutes, } from '$lib/utils/dhcp-option121.js'; - const modeOptions = [ - { value: 'encode' as const, label: 'Encode', icon: 'wrench' }, - { value: 'decode' as const, label: 'Decode', icon: 'search' }, - ]; + const modeOptions = $derived([ + { value: 'encode' as const, label: $t('tools/dhcp-option121-builder.modes.encode'), icon: 'wrench' }, + { value: 'decode' as const, label: $t('tools/dhcp-option121-builder.modes.decode'), icon: 'search' }, + ]); let mode = $state<'encode' | 'decode'>('encode'); let config = $state<ClasslessRoutesConfig>({ @@ -49,51 +50,51 @@ description: string; } - const encodeExamples: EncodeExample[] = [ + const encodeExamples = $derived<EncodeExample[]>([ { - label: 'Private Networks', + label: $t('tools/dhcp-option121-builder.encodeExamples.privateNetworks.label'), routes: [ { destination: '10.0.0.0/8', gateway: '192.168.1.1' }, { destination: '172.16.0.0/12', gateway: '192.168.1.1' }, ], - description: 'Routes to RFC 1918 private networks', + description: $t('tools/dhcp-option121-builder.encodeExamples.privateNetworks.description'), }, { - label: 'Default + Specific', + label: $t('tools/dhcp-option121-builder.encodeExamples.defaultSpecific.label'), routes: [ { destination: '0.0.0.0/0', gateway: '192.168.1.1' }, { destination: '10.10.0.0/16', gateway: '192.168.1.254' }, ], - description: 'Default route with specific override', + description: $t('tools/dhcp-option121-builder.encodeExamples.defaultSpecific.description'), }, { - label: 'Multi-site VPN', + label: $t('tools/dhcp-option121-builder.encodeExamples.multiSiteVPN.label'), routes: [ { destination: '10.1.0.0/16', gateway: '192.168.1.10' }, { destination: '10.2.0.0/16', gateway: '192.168.1.20' }, { destination: '10.3.0.0/16', gateway: '192.168.1.30' }, ], - description: 'Multiple VPN site routes', + description: $t('tools/dhcp-option121-builder.encodeExamples.multiSiteVPN.description'), }, - ]; + ]); - const decodeExamples: DecodeExample[] = [ + const decodeExamples = $derived<DecodeExample[]>([ { - label: 'Private Networks', + label: $t('tools/dhcp-option121-builder.decodeExamples.privateNetworks.label'), hexInput: '080ac0a801010cac10c0a80101', - description: '10.0.0.0/8 and 172.16.0.0/12 via 192.168.1.1', + description: $t('tools/dhcp-option121-builder.decodeExamples.privateNetworks.description'), }, { - label: 'Default Route', + label: $t('tools/dhcp-option121-builder.decodeExamples.defaultRoute.label'), hexInput: '00c0a80101', - description: '0.0.0.0/0 via 192.168.1.1', + description: $t('tools/dhcp-option121-builder.decodeExamples.defaultRoute.description'), }, { - label: 'Specific /24', + label: $t('tools/dhcp-option121-builder.decodeExamples.specific24.label'), hexInput: '18c0a80ac0a80101', - description: '192.168.10.0/24 via 192.168.1.1', + description: $t('tools/dhcp-option121-builder.decodeExamples.specific24.description'), }, - ]; + ]); // Reactive generation - use untrack to prevent infinite loop $effect(() => { @@ -130,26 +131,26 @@ // Validate routes if (cfg.routes.length === 0) { - routeErrors.push('At least one route is required'); + routeErrors.push($t('tools/dhcp-option121-builder.errors.atLeastOneRoute')); } for (let i = 0; i < cfg.routes.length; i++) { const route = cfg.routes[i]; if (!route.destination.trim()) { - routeErrors.push(`Route ${i + 1}: Destination is required`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.destinationRequired', { number: i + 1 })); continue; } if (!route.gateway.trim()) { - routeErrors.push(`Route ${i + 1}: Gateway is required`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.gatewayRequired', { number: i + 1 })); continue; } // Validate CIDR format const cidrMatch = route.destination.match(/^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\/(\d{1,2})$/); if (!cidrMatch) { - routeErrors.push(`Route ${i + 1}: Invalid CIDR notation (use format: x.x.x.x/y)`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidCIDR', { number: i + 1 })); continue; } @@ -157,32 +158,32 @@ const prefixLen = parseInt(prefixLenStr, 10); if (prefixLen < 0 || prefixLen > 32) { - routeErrors.push(`Route ${i + 1}: Prefix length must be 0-32`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidPrefixLength', { number: i + 1 })); continue; } // Validate IPv4 address in CIDR const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; if (!ipv4Regex.test(prefix)) { - routeErrors.push(`Route ${i + 1}: Invalid IPv4 address in destination`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidIPv4Dest', { number: i + 1 })); continue; } const octets = prefix.split('.').map((o) => parseInt(o, 10)); if (octets.some((o) => o > 255)) { - routeErrors.push(`Route ${i + 1}: Invalid IPv4 address (octets must be 0-255)`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidIPv4Octets', { number: i + 1 })); continue; } // Validate gateway if (!ipv4Regex.test(route.gateway)) { - routeErrors.push(`Route ${i + 1}: Invalid gateway IPv4 address`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidGateway', { number: i + 1 })); continue; } const gwOctets = route.gateway.split('.').map((o) => parseInt(o, 10)); if (gwOctets.some((o) => o > 255)) { - routeErrors.push(`Route ${i + 1}: Invalid gateway address (octets must be 0-255)`); + routeErrors.push($t('tools/dhcp-option121-builder.errors.invalidGatewayOctets', { number: i + 1 })); continue; } } @@ -192,19 +193,19 @@ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; if (cfg.network.subnet && cfg.network.subnet.trim() && !ipv4Regex.test(cfg.network.subnet)) { - netErrors.push('Invalid subnet address'); + netErrors.push($t('tools/dhcp-option121-builder.errors.invalidSubnet')); } if (cfg.network.netmask && cfg.network.netmask.trim() && !ipv4Regex.test(cfg.network.netmask)) { - netErrors.push('Invalid netmask'); + netErrors.push($t('tools/dhcp-option121-builder.errors.invalidNetmask')); } if (cfg.network.rangeStart && cfg.network.rangeStart.trim() && !ipv4Regex.test(cfg.network.rangeStart)) { - netErrors.push('Invalid range start address'); + netErrors.push($t('tools/dhcp-option121-builder.errors.invalidRangeStart')); } if (cfg.network.rangeEnd && cfg.network.rangeEnd.trim() && !ipv4Regex.test(cfg.network.rangeEnd)) { - netErrors.push('Invalid range end address'); + netErrors.push($t('tools/dhcp-option121-builder.errors.invalidRangeEnd')); } } @@ -215,7 +216,9 @@ try { result = buildOption121(cfg); } catch (error) { - validationErrors = [error instanceof Error ? error.message : 'Encoding failed']; + validationErrors = [ + error instanceof Error ? error.message : $t('tools/dhcp-option121-builder.errors.encodingFailed'), + ]; result = null; } } else { @@ -231,7 +234,7 @@ } if (!/^[0-9a-fA-F\s:]+$/.test(decodeInput)) { - validationErrors = ['Invalid hex input: only hexadecimal characters allowed']; + validationErrors = [$t('tools/dhcp-option121-builder.errors.invalidHex')]; decodeResult = null; return; } @@ -240,7 +243,9 @@ validationErrors = []; decodeResult = parseOption121(decodeInput); } catch (error) { - validationErrors = [error instanceof Error ? error.message : 'Decoding failed']; + validationErrors = [ + error instanceof Error ? error.message : $t('tools/dhcp-option121-builder.errors.decodingFailed'), + ]; decodeResult = null; } } @@ -310,8 +315,8 @@ </script> <ToolContentContainer - title="DHCP Option 121/249 - Classless Static Routes" - description="Encode and decode Classless Static Routes (RFC 3442 / MSFT 249) with bit-packed network prefixes. Generate configurations for ISC dhcpd and Kea DHCP." + title={$t('tools/dhcp-option121-builder.title')} + description={$t('tools/dhcp-option121-builder.subtitle')} navOptions={modeOptions} bind:selectedNav={mode} > @@ -336,14 +341,16 @@ {#if mode === 'encode'} <div class="card input-card"> <div class="card-header"> - <h3>Static Routes</h3> + <h3>{$t('tools/dhcp-option121-builder.encode.staticRoutesTitle')}</h3> </div> <div class="card-content"> {#each config.routes as _, i (`route-${i}`)} <div class="route-group"> <div class="route-header"> <h4> - <Icon name="compass" size="sm" />Route {i + 1} + <Icon name="compass" size="sm" />{$t('tools/dhcp-option121-builder.encode.route.title', { + number: i + 1, + })} </h4> {#if config.routes.length > 1} <button type="button" class="btn-icon" onclick={() => removeRoute(i)}> @@ -356,22 +363,27 @@ <div class="input-group"> <label for="destination-{i}"> <Icon name="target" size="sm" /> - Destination (CIDR) + {$t('tools/dhcp-option121-builder.encode.route.destination.label')} </label> <input id="destination-{i}" type="text" bind:value={config.routes[i].destination} - placeholder="10.0.0.0/8" + placeholder={$t('tools/dhcp-option121-builder.encode.route.destination.placeholder')} /> </div> <div class="input-group"> <label for="gateway-{i}"> <Icon name="arrow-right" size="sm" /> - Gateway + {$t('tools/dhcp-option121-builder.encode.route.gateway.label')} </label> - <input id="gateway-{i}" type="text" bind:value={config.routes[i].gateway} placeholder="192.168.1.1" /> + <input + id="gateway-{i}" + type="text" + bind:value={config.routes[i].gateway} + placeholder={$t('tools/dhcp-option121-builder.encode.route.gateway.placeholder')} + /> </div> </div> </div> @@ -379,14 +391,14 @@ <button type="button" class="btn-add" onclick={addRoute}> <Icon name="plus" size="sm" /> - Add Route + {$t('tools/dhcp-option121-builder.encode.addRoute')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option121-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -398,11 +410,11 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Encoded Option 121/249</h3> + <h3>{$t('tools/dhcp-option121-builder.results.encodeTitle')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcp-option121-builder.results.hexEncoded.title')}</h4> <button type="button" class="copy-btn" @@ -410,7 +422,9 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/dhcp-option121-builder.buttons.copied') + : $t('tools/dhcp-option121-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> @@ -418,7 +432,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcp-option121-builder.results.wireFormat.title')}</h4> <button type="button" class="copy-btn" @@ -426,15 +440,20 @@ onclick={() => clipboard.copy(result!.wireFormat, 'wire')} > <Icon name={clipboard.isCopied('wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') + ? $t('tools/dhcp-option121-builder.buttons.copied') + : $t('tools/dhcp-option121-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.wireFormat}</pre> </div> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.totalLength} bytes</div> - <div><strong>Routes:</strong> {result.routes.length}</div> + <div> + <strong>{$t('tools/dhcp-option121-builder.results.summary.totalLength')}</strong> + {$t('tools/dhcp-option121-builder.results.summary.bytes', { length: result.totalLength })} + </div> + <div><strong>{$t('tools/dhcp-option121-builder.results.summary.routes')}</strong> {result.routes.length}</div> </div> </div> {/if} @@ -444,25 +463,35 @@ {#if result} <div class="card input-card"> <div class="card-header"> - <h3>Network Settings (Optional)</h3> - <p class="help-text">Customize network values for configuration examples below</p> + <h3>{$t('tools/dhcp-option121-builder.encode.networkSettings.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option121-builder.encode.networkSettings.help')}</p> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet + {$t('tools/dhcp-option121-builder.encode.networkSettings.subnet.label')} </label> - <input id="subnet" type="text" bind:value={config.network!.subnet} placeholder="192.168.1.0" /> + <input + id="subnet" + type="text" + bind:value={config.network!.subnet} + placeholder={$t('tools/dhcp-option121-builder.encode.networkSettings.subnet.placeholder')} + /> </div> <div class="input-group"> <label for="netmask"> <Icon name="network" size="sm" /> - Netmask + {$t('tools/dhcp-option121-builder.encode.networkSettings.netmask.label')} </label> - <input id="netmask" type="text" bind:value={config.network!.netmask} placeholder="255.255.255.0" /> + <input + id="netmask" + type="text" + bind:value={config.network!.netmask} + placeholder={$t('tools/dhcp-option121-builder.encode.networkSettings.netmask.placeholder')} + /> </div> </div> @@ -470,24 +499,34 @@ <div class="input-group"> <label for="range-start"> <Icon name="arrow-right" size="sm" /> - Range Start + {$t('tools/dhcp-option121-builder.encode.networkSettings.rangeStart.label')} </label> - <input id="range-start" type="text" bind:value={config.network!.rangeStart} placeholder="192.168.1.100" /> + <input + id="range-start" + type="text" + bind:value={config.network!.rangeStart} + placeholder={$t('tools/dhcp-option121-builder.encode.networkSettings.rangeStart.placeholder')} + /> </div> <div class="input-group"> <label for="range-end"> <Icon name="arrow-right" size="sm" /> - Range End + {$t('tools/dhcp-option121-builder.encode.networkSettings.rangeEnd.label')} </label> - <input id="range-end" type="text" bind:value={config.network!.rangeEnd} placeholder="192.168.1.200" /> + <input + id="range-end" + type="text" + bind:value={config.network!.rangeEnd} + placeholder={$t('tools/dhcp-option121-builder.encode.networkSettings.rangeEnd.placeholder')} + /> </div> </div> </div> {#if networkValidationErrors.length > 0} <div class="network-errors"> - <h4>Network Settings Errors</h4> + <h4>{$t('tools/dhcp-option121-builder.encode.networkSettings.errorsTitle')}</h4> {#each networkValidationErrors as error, i (i)} <div class="network-error-item"> <Icon name="alert-triangle" size="sm" /> @@ -501,12 +540,12 @@ {#if result && networkValidationErrors.length === 0} <div class="card results"> - <h3>Configuration Examples</h3> + <h3>{$t('tools/dhcp-option121-builder.results.configExamplesTitle')}</h3> {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration (Option 121)</h4> + <h4>{$t('tools/dhcp-option121-builder.results.formats.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -514,7 +553,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-option121-builder.buttons.copied') + : $t('tools/dhcp-option121-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -524,7 +565,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration</h4> + <h4>{$t('tools/dhcp-option121-builder.results.formats.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -532,7 +573,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-option121-builder.buttons.copied') + : $t('tools/dhcp-option121-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -542,7 +585,7 @@ {#if result.examples.msftOption249} <div class="output-group"> <div class="output-header"> - <h4>Microsoft Option 249 Configuration</h4> + <h4>{$t('tools/dhcp-option121-builder.results.formats.msftOption249')}</h4> <button type="button" class="copy-btn" @@ -550,7 +593,9 @@ onclick={() => clipboard.copy(result!.examples.msftOption249!, 'msft')} > <Icon name={clipboard.isCopied('msft') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('msft') ? 'Copied' : 'Copy'} + {clipboard.isCopied('msft') + ? $t('tools/dhcp-option121-builder.buttons.copied') + : $t('tools/dhcp-option121-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.msftOption249}</pre> @@ -561,31 +606,31 @@ {:else} <div class="card input-card"> <div class="card-header"> - <h3>Decode Option 121/249 Hex</h3> + <h3>{$t('tools/dhcp-option121-builder.decode.title')}</h3> </div> <div class="card-content"> <div class="input-group"> <label for="decode-input"> <Icon name="code" size="sm" /> - Hex-Encoded Option 121/249 + {$t('tools/dhcp-option121-builder.decode.input.label')} </label> <textarea id="decode-input" bind:value={decodeInput} - placeholder="Enter hex string (e.g., 080ac0a80101acc01000c0a80101)" + placeholder={$t('tools/dhcp-option121-builder.decode.input.placeholder')} rows="4" ></textarea> </div> <button type="button" class="btn-primary" onclick={decode}> <Icon name="search" size="sm" /> - Decode + {$t('tools/dhcp-option121-builder.decode.button')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option121-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -597,25 +642,31 @@ {#if decodeResult && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Classless Static Routes</h3> + <h3>{$t('tools/dhcp-option121-builder.results.decodeTitle')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {decodeResult.totalLength} bytes</div> - <div><strong>Routes Found:</strong> {decodeResult.routes.length}</div> + <div> + <strong>{$t('tools/dhcp-option121-builder.results.summary.totalLength')}</strong> + {$t('tools/dhcp-option121-builder.results.summary.bytes', { length: decodeResult.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option121-builder.results.routesFound')}</strong> + {decodeResult.routes.length} + </div> </div> <div class="routes-section"> - <h4>Route List</h4> + <h4>{$t('tools/dhcp-option121-builder.results.routeListTitle')}</h4> {#each decodeResult.routes as route, i (i)} <div class="route-item"> <div class="route-field"> <Icon name="target" size="sm" /> - <span class="field-label">Destination:</span> + <span class="field-label">{$t('tools/dhcp-option121-builder.results.destination')}</span> <span class="field-value">{route.destination}</span> </div> <div class="route-field"> <Icon name="arrow-right" size="sm" /> - <span class="field-label">Gateway:</span> + <span class="field-label">{$t('tools/dhcp-option121-builder.results.gateway')}</span> <span class="field-value">{route.gateway}</span> </div> </div> diff --git a/src/lib/components/tools/DHCPOption150Builder.svelte b/src/lib/components/tools/DHCPOption150Builder.svelte index 4d868fa0..808d639c 100644 --- a/src/lib/components/tools/DHCPOption150Builder.svelte +++ b/src/lib/components/tools/DHCPOption150Builder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { buildTFTPOptions, parseOption150, @@ -16,10 +17,10 @@ type ParsedStringOption, } from '$lib/utils/dhcp-option150.js'; - const modeOptions = [ - { value: 'encode' as const, label: 'Encode', icon: 'wrench' }, - { value: 'decode' as const, label: 'Decode', icon: 'search' }, - ]; + const modeOptions = $derived([ + { value: 'encode' as const, label: $t('tools/dhcp-option150-builder.modes.encode'), icon: 'wrench' }, + { value: 'decode' as const, label: $t('tools/dhcp-option150-builder.modes.decode'), icon: 'search' }, + ]); let mode = $state<'encode' | 'decode'>('encode'); let config = $state<TFTPConfig>({ @@ -58,58 +59,58 @@ description: string; } - const encodeExamples: EncodeExample[] = [ + const encodeExamples: EncodeExample[] = $derived([ { - label: 'Cisco IP Phones', + label: $t('tools/dhcp-option150-builder.encodeExamples.ciscoIPPhones.label'), option150Servers: ['192.168.1.10', '192.168.1.11'], - description: 'Redundant TFTP servers for Cisco IP phone configuration', + description: $t('tools/dhcp-option150-builder.encodeExamples.ciscoIPPhones.description'), }, { - label: 'PXE Boot (Standard)', + label: $t('tools/dhcp-option150-builder.encodeExamples.pxeBootStandard.label'), option66Server: 'pxe.example.com', option67Bootfile: 'pxelinux.0', - description: 'Standard PXE boot with single TFTP server', + description: $t('tools/dhcp-option150-builder.encodeExamples.pxeBootStandard.description'), }, { - label: 'PXE Boot (UEFI)', + label: $t('tools/dhcp-option150-builder.encodeExamples.pxeBootUEFI.label'), option66Server: '192.168.1.10', option67Bootfile: 'bootx64.efi', - description: 'UEFI PXE boot configuration', + description: $t('tools/dhcp-option150-builder.encodeExamples.pxeBootUEFI.description'), }, { - label: 'Combined (Option 150 + 67)', + label: $t('tools/dhcp-option150-builder.encodeExamples.combined.label'), option150Servers: ['192.168.1.10', '192.168.1.11'], option67Bootfile: 'SEP{MAC}.cnf.xml', - description: 'Cisco phones with redundant TFTP and config template', + description: $t('tools/dhcp-option150-builder.encodeExamples.combined.description'), }, - ]; + ]); - const decodeExamples: DecodeExample[] = [ + const decodeExamples: DecodeExample[] = $derived([ { - label: 'Option 150: Dual TFTP', + label: $t('tools/dhcp-option150-builder.decodeExamples.option150DualTFTP.label'), mode: 'option150', hexInput: 'c0a8010ac0a8010b', - description: '192.168.1.10 and 192.168.1.11', + description: $t('tools/dhcp-option150-builder.decodeExamples.option150DualTFTP.description'), }, { - label: 'Option 66: Hostname', + label: $t('tools/dhcp-option150-builder.decodeExamples.option66Hostname.label'), mode: 'option66', hexInput: '7078652e6578616d706c652e636f6d', - description: 'pxe.example.com', + description: $t('tools/dhcp-option150-builder.decodeExamples.option66Hostname.description'), }, { - label: 'Option 67: PXE Boot', + label: $t('tools/dhcp-option150-builder.decodeExamples.option67PXEBoot.label'), mode: 'option67', hexInput: '7078656c696e75782e30', - description: 'pxelinux.0', + description: $t('tools/dhcp-option150-builder.decodeExamples.option67PXEBoot.description'), }, { - label: 'Option 67: UEFI Boot', + label: $t('tools/dhcp-option150-builder.decodeExamples.option67UEFIBoot.label'), mode: 'option67', hexInput: '626f6f747836 42e656669', - description: 'bootx64.efi', + description: $t('tools/dhcp-option150-builder.decodeExamples.option67UEFIBoot.description'), }, - ]; + ]); // Reactive generation $effect(() => { @@ -345,8 +346,8 @@ </script> <ToolContentContainer - title="DHCP Options 150/66/67 - TFTP Server Configuration" - description="Configure TFTP servers for PXE boot and Cisco IP phones. Option 150 (Cisco TFTP list), Option 66 (TFTP server name), and Option 67 (bootfile name)." + title={$t('tools/dhcp-option150-builder.title')} + description={$t('tools/dhcp-option150-builder.subtitle')} navOptions={modeOptions} bind:selectedNav={mode} > @@ -371,8 +372,8 @@ {#if mode === 'encode'} <div class="card input-card"> <div class="card-header"> - <h3>Option 150: Cisco TFTP Server List</h3> - <p class="help-text">Multiple IPv4 addresses for redundant TFTP servers (Cisco IP phones)</p> + <h3>{$t('tools/dhcp-option150-builder.encode.option150.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option150-builder.encode.option150.helpText')}</p> </div> <div class="card-content"> {#if config.option150Servers && config.option150Servers.length > 0} @@ -381,13 +382,13 @@ <div class="input-group flex-grow"> <label for="opt150-server-{i}"> <Icon name="server" size="sm" /> - Server {i + 1} + {$t('tools/dhcp-option150-builder.encode.option150.serverLabel', { number: i + 1 })} </label> <input id="opt150-server-{i}" type="text" bind:value={config.option150Servers[i]} - placeholder="192.168.1.10" + placeholder={$t('tools/dhcp-option150-builder.encode.option150.serverPlaceholder')} /> </div> <button type="button" class="btn-icon" onclick={() => removeOption150Server(i)}> @@ -399,27 +400,27 @@ <button type="button" class="btn-add" onclick={addOption150Server}> <Icon name="plus" size="sm" /> - Add TFTP Server + {$t('tools/dhcp-option150-builder.encode.option150.addButton')} </button> </div> </div> <div class="card input-card"> <div class="card-header"> - <h3>Option 66: TFTP Server Name (Standard)</h3> - <p class="help-text">Single hostname or IP address for standard PXE boot</p> + <h3>{$t('tools/dhcp-option150-builder.encode.option66.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option150-builder.encode.option66.helpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="opt66-server"> <Icon name="globe" size="sm" /> - TFTP Server Hostname/IP + {$t('tools/dhcp-option150-builder.encode.option66.label')} </label> <input id="opt66-server" type="text" bind:value={config.option66Server} - placeholder="tftp.example.com or 192.168.1.10" + placeholder={$t('tools/dhcp-option150-builder.encode.option66.placeholder')} /> </div> </div> @@ -427,23 +428,28 @@ <div class="card input-card"> <div class="card-header"> - <h3>Option 67: Bootfile Name</h3> - <p class="help-text">Filename to boot from TFTP server (e.g., pxelinux.0 for BIOS, bootx64.efi for UEFI)</p> + <h3>{$t('tools/dhcp-option150-builder.encode.option67.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option150-builder.encode.option67.helpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="opt67-bootfile"> <Icon name="file" size="sm" /> - Bootfile Name + {$t('tools/dhcp-option150-builder.encode.option67.label')} </label> - <input id="opt67-bootfile" type="text" bind:value={config.option67Bootfile} placeholder="pxelinux.0" /> + <input + id="opt67-bootfile" + type="text" + bind:value={config.option67Bootfile} + placeholder={$t('tools/dhcp-option150-builder.encode.option67.placeholder')} + /> </div> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option150-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -456,11 +462,11 @@ {#if result && validationErrors.length === 0} {#if result.option150} <div class="card results"> - <h3>Option 150: TFTP Server List</h3> + <h3>{$t('tools/dhcp-option150-builder.results.option150.title')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option150.hexEncodedTitle')}</h4> <button type="button" class="copy-btn" @@ -468,7 +474,9 @@ onclick={() => clipboard.copy(result!.option150!.hexEncoded, 'opt150-hex')} > <Icon name={clipboard.isCopied('opt150-hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt150-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt150-hex') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option150.hexEncoded}</pre> @@ -476,7 +484,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option150.wireFormatTitle')}</h4> <button type="button" class="copy-btn" @@ -484,26 +492,36 @@ onclick={() => clipboard.copy(result!.option150!.wireFormat, 'opt150-wire')} > <Icon name={clipboard.isCopied('opt150-wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt150-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt150-wire') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option150.wireFormat}</pre> </div> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.option150.totalLength} bytes</div> - <div><strong>Servers:</strong> {result.option150.servers.length}</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.results.option150.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.results.option150.bytes', { length: result.option150.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option150-builder.results.option150.servers')}</strong> + {$t('tools/dhcp-option150-builder.results.option150.serverCount', { + count: result.option150.servers.length, + })} + </div> </div> </div> {/if} {#if result.option66} <div class="card results"> - <h3>Option 66: TFTP Server Name</h3> + <h3>{$t('tools/dhcp-option150-builder.results.option66.title')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Value</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option66.valueTitle')}</h4> <button type="button" class="copy-btn" @@ -511,7 +529,9 @@ onclick={() => clipboard.copy(result!.option66!.value, 'opt66-value')} > <Icon name={clipboard.isCopied('opt66-value') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt66-value') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt66-value') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option66.value}</pre> @@ -519,7 +539,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option66.hexEncodedTitle')}</h4> <button type="button" class="copy-btn" @@ -527,25 +547,30 @@ onclick={() => clipboard.copy(result!.option66!.hexEncoded, 'opt66-hex')} > <Icon name={clipboard.isCopied('opt66-hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt66-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt66-hex') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option66.hexEncoded}</pre> </div> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.option66.totalLength} bytes</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.results.option66.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.results.option66.bytes', { length: result.option66.totalLength })} + </div> </div> </div> {/if} {#if result.option67} <div class="card results"> - <h3>Option 67: Bootfile Name</h3> + <h3>{$t('tools/dhcp-option150-builder.results.option67.title')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Value</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option67.valueTitle')}</h4> <button type="button" class="copy-btn" @@ -553,7 +578,9 @@ onclick={() => clipboard.copy(result!.option67!.value, 'opt67-value')} > <Icon name={clipboard.isCopied('opt67-value') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt67-value') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt67-value') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option67.value}</pre> @@ -561,7 +588,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded</h4> + <h4>{$t('tools/dhcp-option150-builder.results.option67.hexEncodedTitle')}</h4> <button type="button" class="copy-btn" @@ -569,14 +596,19 @@ onclick={() => clipboard.copy(result!.option67!.hexEncoded, 'opt67-hex')} > <Icon name={clipboard.isCopied('opt67-hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt67-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt67-hex') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option67.hexEncoded}</pre> </div> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.option67.totalLength} bytes</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.results.option67.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.results.option67.bytes', { length: result.option67.totalLength })} + </div> </div> </div> {/if} @@ -587,25 +619,35 @@ {#if result} <div class="card input-card"> <div class="card-header"> - <h3>Network Settings (Optional)</h3> - <p class="help-text">Customize network values for configuration examples below</p> + <h3>{$t('tools/dhcp-option150-builder.encode.networkSettings.title')}</h3> + <p class="help-text">{$t('tools/dhcp-option150-builder.encode.networkSettings.helpText')}</p> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet + {$t('tools/dhcp-option150-builder.encode.networkSettings.subnet.label')} </label> - <input id="subnet" type="text" bind:value={config.network!.subnet} placeholder="192.168.1.0" /> + <input + id="subnet" + type="text" + bind:value={config.network!.subnet} + placeholder={$t('tools/dhcp-option150-builder.encode.networkSettings.subnet.placeholder')} + /> </div> <div class="input-group"> <label for="netmask"> <Icon name="network" size="sm" /> - Netmask + {$t('tools/dhcp-option150-builder.encode.networkSettings.netmask.label')} </label> - <input id="netmask" type="text" bind:value={config.network!.netmask} placeholder="255.255.255.0" /> + <input + id="netmask" + type="text" + bind:value={config.network!.netmask} + placeholder={$t('tools/dhcp-option150-builder.encode.networkSettings.netmask.placeholder')} + /> </div> </div> @@ -613,24 +655,34 @@ <div class="input-group"> <label for="range-start"> <Icon name="arrow-right" size="sm" /> - Range Start + {$t('tools/dhcp-option150-builder.encode.networkSettings.rangeStart.label')} </label> - <input id="range-start" type="text" bind:value={config.network!.rangeStart} placeholder="192.168.1.100" /> + <input + id="range-start" + type="text" + bind:value={config.network!.rangeStart} + placeholder={$t('tools/dhcp-option150-builder.encode.networkSettings.rangeStart.placeholder')} + /> </div> <div class="input-group"> <label for="range-end"> <Icon name="arrow-right" size="sm" /> - Range End + {$t('tools/dhcp-option150-builder.encode.networkSettings.rangeEnd.label')} </label> - <input id="range-end" type="text" bind:value={config.network!.rangeEnd} placeholder="192.168.1.200" /> + <input + id="range-end" + type="text" + bind:value={config.network!.rangeEnd} + placeholder={$t('tools/dhcp-option150-builder.encode.networkSettings.rangeEnd.placeholder')} + /> </div> </div> </div> {#if networkValidationErrors.length > 0} <div class="network-errors"> - <h4>Network Settings Errors</h4> + <h4>{$t('tools/dhcp-option150-builder.encode.networkSettings.errorsTitle')}</h4> {#each networkValidationErrors as error, i (i)} <div class="network-error-item"> <Icon name="alert-triangle" size="sm" /> @@ -644,12 +696,12 @@ {#if result && networkValidationErrors.length === 0} <div class="card results"> - <h3>Configuration Examples</h3> + <h3>{$t('tools/dhcp-option150-builder.results.configExamples.title')}</h3> {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration</h4> + <h4>{$t('tools/dhcp-option150-builder.results.configExamples.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -657,7 +709,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -667,7 +721,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration</h4> + <h4>{$t('tools/dhcp-option150-builder.results.configExamples.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -675,7 +729,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -685,7 +741,7 @@ {#if result.examples.ciscoIos} <div class="output-group"> <div class="output-header"> - <h4>Cisco IOS Configuration</h4> + <h4>{$t('tools/dhcp-option150-builder.results.configExamples.ciscoIos')}</h4> <button type="button" class="copy-btn" @@ -693,7 +749,9 @@ onclick={() => clipboard.copy(result!.examples.ciscoIos!, 'cisco')} > <Icon name={clipboard.isCopied('cisco') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('cisco') ? 'Copied' : 'Copy'} + {clipboard.isCopied('cisco') + ? $t('tools/dhcp-option150-builder.buttons.copied') + : $t('tools/dhcp-option150-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.ciscoIos}</pre> @@ -704,43 +762,43 @@ {:else} <div class="card input-card"> <div class="card-header"> - <h3>Decode TFTP Option</h3> + <h3>{$t('tools/dhcp-option150-builder.decode.title')}</h3> </div> <div class="card-content"> <div class="input-group"> <label for="decode-mode"> <Icon name="settings" size="sm" /> - Option Type + {$t('tools/dhcp-option150-builder.decode.optionType.label')} </label> <select id="decode-mode" bind:value={decodeMode}> - <option value="option150">Option 150: TFTP Server List</option> - <option value="option66">Option 66: TFTP Server Name</option> - <option value="option67">Option 67: Bootfile Name</option> + <option value="option150">{$t('tools/dhcp-option150-builder.decode.optionType.option150')}</option> + <option value="option66">{$t('tools/dhcp-option150-builder.decode.optionType.option66')}</option> + <option value="option67">{$t('tools/dhcp-option150-builder.decode.optionType.option67')}</option> </select> </div> <div class="input-group"> <label for="decode-input"> <Icon name="code" size="sm" /> - Hex-Encoded Option Data + {$t('tools/dhcp-option150-builder.decode.hexInput.label')} </label> <textarea id="decode-input" bind:value={decodeInput} - placeholder="Enter hex string (e.g., c0a8010ac0a8010b for Option 150)" + placeholder={$t('tools/dhcp-option150-builder.decode.hexInput.placeholder')} rows="4" ></textarea> </div> <button type="button" class="btn-primary" onclick={decode}> <Icon name="search" size="sm" /> - Decode + {$t('tools/dhcp-option150-builder.decode.decodeButton')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option150-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -752,19 +810,29 @@ {#if decodeResult150 && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Option 150: TFTP Server List</h3> + <h3>{$t('tools/dhcp-option150-builder.decodeResults.option150.title')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {decodeResult150.totalLength} bytes</div> - <div><strong>Servers Found:</strong> {decodeResult150.servers.length}</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.decodeResults.option150.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.decodeResults.option150.bytes', { length: decodeResult150.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option150-builder.decodeResults.option150.serversFound')}</strong> + {$t('tools/dhcp-option150-builder.decodeResults.option150.serverCount', { + count: decodeResult150.servers.length, + })} + </div> </div> <div class="servers-section"> - <h4>TFTP Servers</h4> + <h4>{$t('tools/dhcp-option150-builder.decodeResults.option150.serversTitle')}</h4> {#each decodeResult150.servers as server, i (i)} <div class="server-item"> <Icon name="server" size="sm" /> - <span class="field-label">Server {i + 1}:</span> + <span class="field-label" + >{$t('tools/dhcp-option150-builder.decodeResults.option150.serverLabel', { number: i + 1 })}</span + > <span class="field-value">{server}</span> </div> {/each} @@ -774,14 +842,17 @@ {#if decodeResult66 && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Option 66: TFTP Server Name</h3> + <h3>{$t('tools/dhcp-option150-builder.decodeResults.option66.title')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {decodeResult66.totalLength} bytes</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.decodeResults.option66.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.decodeResults.option66.bytes', { length: decodeResult66.totalLength })} + </div> </div> <div class="decoded-value"> - <h4>TFTP Server</h4> + <h4>{$t('tools/dhcp-option150-builder.decodeResults.option66.serverTitle')}</h4> <div class="value-display"> <Icon name="globe" size="sm" /> <span>{decodeResult66.value}</span> @@ -792,14 +863,17 @@ {#if decodeResult67 && validationErrors.length === 0} <div class="card results"> - <h3>Decoded Option 67: Bootfile Name</h3> + <h3>{$t('tools/dhcp-option150-builder.decodeResults.option67.title')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {decodeResult67.totalLength} bytes</div> + <div> + <strong>{$t('tools/dhcp-option150-builder.decodeResults.option67.totalLength')}</strong> + {$t('tools/dhcp-option150-builder.decodeResults.option67.bytes', { length: decodeResult67.totalLength })} + </div> </div> <div class="decoded-value"> - <h4>Bootfile</h4> + <h4>{$t('tools/dhcp-option150-builder.decodeResults.option67.bootfileTitle')}</h4> <div class="value-display"> <Icon name="file" size="sm" /> <span>{decodeResult67.value}</span> diff --git a/src/lib/components/tools/DHCPOption43Generator.svelte b/src/lib/components/tools/DHCPOption43Generator.svelte index e47e2263..83ace628 100644 --- a/src/lib/components/tools/DHCPOption43Generator.svelte +++ b/src/lib/components/tools/DHCPOption43Generator.svelte @@ -10,6 +10,7 @@ import { useClipboard, useExamples } from '$lib/composables'; import Icon from '$lib/components/global/Icon.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; + import { t } from '$lib/stores/language'; let vendorType = $state<VendorType>('cisco-catalyst'); let ipInput = $state(''); @@ -23,36 +24,36 @@ { vendor: 'cisco-catalyst' as VendorType, ips: '192.168.1.10\n192.168.1.11', - description: 'Cisco Catalyst with dual controllers', + description: $t('tools/dhcp-option43-generator.examples.ciscoCatalyst'), }, { vendor: 'cisco-meraki' as VendorType, ips: '192.168.10.5', - description: 'Single Meraki cloud controller', + description: $t('tools/dhcp-option43-generator.examples.ciscoMeraki'), }, { vendor: 'ruckus-smartzone' as VendorType, ips: '10.0.0.100', - description: 'Ruckus SmartZone controller', + description: $t('tools/dhcp-option43-generator.examples.ruckusSmartzone'), }, { vendor: 'aruba' as VendorType, ips: '172.16.1.50', - description: 'Aruba wireless controller', + description: $t('tools/dhcp-option43-generator.examples.aruba'), }, { vendor: 'unifi' as VendorType, ips: '192.168.1.20', - description: 'UniFi Network Controller', + description: $t('tools/dhcp-option43-generator.examples.unifi'), }, { vendor: 'ruckus-zonedirector' as VendorType, ips: '10.50.100.200', - description: 'Ruckus ZoneDirector (legacy)', + description: $t('tools/dhcp-option43-generator.examples.ruckusZonedirector'), }, ]; - const examples = useExamples(examplesList); + const examples = useExamples(() => examplesList); function generate() { errors = []; @@ -61,22 +62,24 @@ const ips = parseIPList(ipInput); if (ips.length === 0) { - errors = ['Please enter at least one IP address']; + errors = [$t('tools/dhcp-option43-generator.errors.noIPs')]; return; } // Validate each IP const invalidIPs = ips.filter((ip) => !isValidIPv4(ip)); if (invalidIPs.length > 0) { - errors = [`Invalid IP address format: ${invalidIPs.join(', ')}`]; + errors = [$t('tools/dhcp-option43-generator.errors.invalidIP', { ips: invalidIPs.join(', ') })]; return; } // Check max IPs for vendor if (ips.length > vendorInfo.maxIPs) { - errors = [ - `${vendorInfo.name} supports maximum ${vendorInfo.maxIPs} controller${vendorInfo.maxIPs > 1 ? 's' : ''}. You entered ${ips.length}.`, - ]; + const errorKey = + vendorInfo.maxIPs > 1 + ? 'tools/dhcp-option43-generator.errors.maxExceededPlural' + : 'tools/dhcp-option43-generator.errors.maxExceeded'; + errors = [$t(errorKey, { vendor: vendorInfo.name, max: vendorInfo.maxIPs, count: ips.length })]; return; } @@ -102,20 +105,20 @@ onSelect={loadExample} getLabel={(ex) => VENDOR_INFO[ex.vendor].name} getDescription={(ex) => ex.description} - getTooltip={(ex) => `Generate Option 43 for ${VENDOR_INFO[ex.vendor].name}`} + getTooltip={(ex) => $t('tools/dhcp-option43-generator.examples.tooltip', { vendor: VENDOR_INFO[ex.vendor].name })} /> <!-- Input Form --> <div class="card input-card"> <div class="card-header"> - <h3>Generator Configuration</h3> + <h3>{$t('tools/dhcp-option43-generator.input.title')}</h3> </div> <div class="card-content"> <section class="inputs"> <div class="input-group"> <label for="vendor"> <Icon name="wifi" size="sm" /> - Wireless Controller Vendor + {$t('tools/dhcp-option43-generator.input.vendor.label')} </label> <select id="vendor" @@ -135,22 +138,26 @@ <div class="input-group"> <label for="ip-input"> <Icon name="network" size="sm" /> - Controller IP Address{vendorInfo.maxIPs > 1 ? 'es' : ''} + {vendorInfo.maxIPs > 1 + ? $t('tools/dhcp-option43-generator.input.ipAddresses.labelPlural') + : $t('tools/dhcp-option43-generator.input.ipAddresses.label')} <span class="label-hint"> - (max {vendorInfo.maxIPs}) + {$t('tools/dhcp-option43-generator.input.ipAddresses.maxHint', { max: vendorInfo.maxIPs })} </span> </label> <textarea id="ip-input" bind:value={ipInput} - placeholder={`Enter IP address${vendorInfo.maxIPs > 1 ? 'es' : ''} (one per line or comma-separated)\ne.g., 192.168.1.10, 192.168.1.11`} + placeholder={vendorInfo.maxIPs > 1 + ? $t('tools/dhcp-option43-generator.input.ipAddresses.placeholderPlural') + : $t('tools/dhcp-option43-generator.input.ipAddresses.placeholder')} rows="3" onchange={() => examples.clear()} ></textarea> <div class="input-actions"> <button type="button" class="btn-primary" onclick={generate}> <Icon name="zap" size="sm" /> - Generate + {$t('tools/dhcp-option43-generator.input.generateButton')} </button> </div> </div> @@ -171,14 +178,14 @@ {#if result} <div class="card results"> - <h3>Generated Option 43 Values</h3> + <h3>{$t('tools/dhcp-option43-generator.results.title')}</h3> {#if result.iosCommand && result.workings} <div class="ios-command-section"> <div class="ios-header"> <h4> <Icon name="terminal" size="sm" /> - {result.commandLabel || 'DHCP Server Command'} + {result.commandLabel || $t('tools/dhcp-option43-generator.results.commandSection.defaultLabel')} </h4> <button type="button" @@ -187,13 +194,15 @@ onclick={() => clipboard.copy(result!.iosCommand!, 'ios')} > <Icon name={clipboard.isCopied('ios') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('ios') ? 'Copied' : 'Copy'} + {clipboard.isCopied('ios') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <pre class="ios-command">{result.iosCommand}</pre> <div class="workings"> - <h5>How this value is calculated:</h5> + <h5>{$t('tools/dhcp-option43-generator.results.commandSection.calculationTitle')}</h5> <ul> {#each result.workings as working, i (i)} <li>{working}</li> @@ -211,7 +220,7 @@ <div class="output-formats"> <div class="output-group"> <div class="output-header"> - <h4>Hexadecimal String</h4> + <h4>{$t('tools/dhcp-option43-generator.results.formats.hex.title')}</h4> <button type="button" class="copy-btn" @@ -219,16 +228,18 @@ onclick={() => clipboard.copy(result!.hex, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <code class="output-value">{result.hex}</code> - <p class="format-hint">Raw hexadecimal - used in most DHCP server configurations</p> + <p class="format-hint">{$t('tools/dhcp-option43-generator.results.formats.hex.hint')}</p> </div> <div class="output-group"> <div class="output-header"> - <h4>Colon-Separated Hex</h4> + <h4>{$t('tools/dhcp-option43-generator.results.formats.colonHex.title')}</h4> <button type="button" class="copy-btn" @@ -236,16 +247,18 @@ onclick={() => clipboard.copy(result!.colonHex, 'colonHex')} > <Icon name={clipboard.isCopied('colonHex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('colonHex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('colonHex') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <code class="output-value">{result.colonHex}</code> - <p class="format-hint">Used by Infoblox and some network appliances</p> + <p class="format-hint">{$t('tools/dhcp-option43-generator.results.formats.colonHex.hint')}</p> </div> <div class="output-group"> <div class="output-header"> - <h4>Windows DHCP Binary</h4> + <h4>{$t('tools/dhcp-option43-generator.results.formats.windowsBinary.title')}</h4> <button type="button" class="copy-btn" @@ -253,16 +266,18 @@ onclick={() => clipboard.copy(result!.windowsBinary, 'windows')} > <Icon name={clipboard.isCopied('windows') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('windows') ? 'Copied' : 'Copy'} + {clipboard.isCopied('windows') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <code class="output-value">{result.windowsBinary}</code> - <p class="format-hint">Enter in Windows DHCP Server's Binary field for Option 43</p> + <p class="format-hint">{$t('tools/dhcp-option43-generator.results.formats.windowsBinary.hint')}</p> </div> <div class="output-group"> <div class="output-header"> - <h4>ISC DHCP Configuration</h4> + <h4>{$t('tools/dhcp-option43-generator.results.formats.iscDhcp.title')}</h4> <button type="button" class="copy-btn" @@ -270,16 +285,18 @@ onclick={() => clipboard.copy(result!.iscDhcp, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.iscDhcp}</pre> - <p class="format-hint">Add to dhcpd.conf for ISC DHCP server</p> + <p class="format-hint">{$t('tools/dhcp-option43-generator.results.formats.iscDhcp.hint')}</p> </div> <div class="output-group"> <div class="output-header"> - <h4>Mikrotik Configuration</h4> + <h4>{$t('tools/dhcp-option43-generator.results.formats.mikrotik.title')}</h4> <button type="button" class="copy-btn" @@ -287,30 +304,26 @@ onclick={() => clipboard.copy(result!.mikrotik, 'mikrotik')} > <Icon name={clipboard.isCopied('mikrotik') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('mikrotik') ? 'Copied' : 'Copy'} + {clipboard.isCopied('mikrotik') + ? $t('tools/dhcp-option43-generator.buttons.copied') + : $t('tools/dhcp-option43-generator.buttons.copy')} </button> </div> <code class="output-value">{result.mikrotik}</code> - <p class="format-hint">RouterOS DHCP option configuration command</p> + <p class="format-hint">{$t('tools/dhcp-option43-generator.results.formats.mikrotik.hint')}</p> </div> </div> </div> <div class="card info"> - <h3>Important Notes</h3> + <h3>{$t('tools/dhcp-option43-generator.notes.title')}</h3> <ul class="notes-list"> - <li> - <strong>DHCP Option 43</strong> is vendor-specific and must match the AP manufacturer's expected format - </li> - <li> - Some vendors require <strong>Option 60</strong> (Vendor Class Identifier) to be set in addition to Option 43 - </li> - <li>Ensure controller IPs are reachable from the AP management network</li> - <li> - For high availability, configure <strong>multiple controller IPs</strong> when supported - </li> - <li>Changes to DHCP options require AP to renew lease or reboot to take effect</li> - <li>Always test in a controlled environment before deploying to production networks</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.vendorSpecific')}</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.option60')}</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.reachability')}</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.highAvailability')}</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.leaseRenewal')}</li> + <li>{$t('tools/dhcp-option43-generator.notes.list.testing')}</li> </ul> </div> {/if} @@ -630,10 +643,6 @@ li { line-height: 1.6; color: var(--text-primary); - - strong { - color: var(--color-primary); - } } } } diff --git a/src/lib/components/tools/DHCPOption60Builder.svelte b/src/lib/components/tools/DHCPOption60Builder.svelte index f983dcb5..de583ac9 100644 --- a/src/lib/components/tools/DHCPOption60Builder.svelte +++ b/src/lib/components/tools/DHCPOption60Builder.svelte @@ -11,6 +11,7 @@ } from '$lib/utils/dhcp-option60.js'; import { useClipboard, useExamples } from '$lib/composables'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; + import { t } from '$lib/stores/language'; let selectedPreset = $state<VendorPreset>('cisco-phone'); let customValue = $state(''); @@ -23,38 +24,38 @@ const clipboard = useClipboard(); const examplesList = [ - { preset: 'cisco-phone' as VendorPreset, description: 'Cisco IP Phones with TFTP' }, - { preset: 'cisco-ap' as VendorPreset, description: 'Cisco APs with Option 43' }, - { preset: 'pxe-client' as VendorPreset, description: 'PXE network boot' }, - { preset: 'aruba-ap' as VendorPreset, description: 'Aruba wireless APs' }, + { preset: 'cisco-phone' as VendorPreset, description: $t('tools/dhcp-option60-builder.examples.ciscoPhone') }, + { preset: 'cisco-ap' as VendorPreset, description: $t('tools/dhcp-option60-builder.examples.ciscoAp') }, + { preset: 'pxe-client' as VendorPreset, description: $t('tools/dhcp-option60-builder.examples.pxeClient') }, + { preset: 'aruba-ap' as VendorPreset, description: $t('tools/dhcp-option60-builder.examples.arubaAp') }, ]; - const examples = useExamples(examplesList); + const examples = useExamples(() => examplesList); - const importantNotes = [ - '<strong>Option 60</strong> (Vendor Class Identifier) allows DHCP servers to provide different configurations based on client type', - 'Class-based policies enable <strong>separate IP pools</strong> and options for different device types', - 'Wireless APs typically require both <strong>Option 60 and Option 43</strong> for controller discovery', - 'Test configurations in a lab environment before deploying to production networks', - 'Adjust subnet addresses, pool ranges, and option values to match your network design', - ]; + const importantNotes = $derived([ + $t('tools/dhcp-option60-builder.notes.list.vendorClass'), + $t('tools/dhcp-option60-builder.notes.list.classPolicies'), + $t('tools/dhcp-option60-builder.notes.list.option43'), + $t('tools/dhcp-option60-builder.notes.list.testing'), + $t('tools/dhcp-option60-builder.notes.list.customize'), + ]); const poolFields = $derived([ { id: 'poolStart', icon: 'arrow-right', - label: 'Pool Start', - help: 'First IP in matching pool', - placeholder: '192.168.10.100', + label: $t('tools/dhcp-option60-builder.input.poolStart.label'), + help: $t('tools/dhcp-option60-builder.input.poolStart.help'), + placeholder: $t('tools/dhcp-option60-builder.input.poolStart.placeholder'), bind: () => networkConfig.poolStart, set: (v: string) => (networkConfig.poolStart = v), }, { id: 'poolEnd', icon: 'arrow-left', - label: 'Pool End', - help: 'Last IP in matching pool', - placeholder: '192.168.10.200', + label: $t('tools/dhcp-option60-builder.input.poolEnd.label'), + help: $t('tools/dhcp-option60-builder.input.poolEnd.help'), + placeholder: $t('tools/dhcp-option60-builder.input.poolEnd.placeholder'), bind: () => networkConfig.poolEnd, set: (v: string) => (networkConfig.poolEnd = v), }, @@ -64,18 +65,18 @@ { id: 'nonMatchingPoolStart', icon: 'arrow-right', - label: 'Non-Matching Pool Start', - help: 'First IP for non-matching clients', - placeholder: '192.168.10.50', + label: $t('tools/dhcp-option60-builder.input.nonMatchingPoolStart.label'), + help: $t('tools/dhcp-option60-builder.input.nonMatchingPoolStart.help'), + placeholder: $t('tools/dhcp-option60-builder.input.nonMatchingPoolStart.placeholder'), bind: () => networkConfig.nonMatchingPoolStart, set: (v: string) => (networkConfig.nonMatchingPoolStart = v), }, { id: 'nonMatchingPoolEnd', icon: 'arrow-left', - label: 'Non-Matching Pool End', - help: 'Last IP for non-matching clients', - placeholder: '192.168.10.99', + label: $t('tools/dhcp-option60-builder.input.nonMatchingPoolEnd.label'), + help: $t('tools/dhcp-option60-builder.input.nonMatchingPoolEnd.help'), + placeholder: $t('tools/dhcp-option60-builder.input.nonMatchingPoolEnd.placeholder'), bind: () => networkConfig.nonMatchingPoolEnd, set: (v: string) => (networkConfig.nonMatchingPoolEnd = v), }, @@ -131,43 +132,43 @@ // Validate subnet if (!isValidCIDR(networkConfig.subnet)) { - validationErrors.push('Invalid subnet CIDR notation (e.g., 192.168.10.0/24)'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidSubnet')); } // Validate pool IPs if (!isValidIPv4(networkConfig.poolStart)) { - validationErrors.push('Invalid pool start IP address'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidPoolStart')); } if (!isValidIPv4(networkConfig.poolEnd)) { - validationErrors.push('Invalid pool end IP address'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidPoolEnd')); } // Validate non-matching pool if provided if (networkConfig.nonMatchingPoolStart && !isValidIPv4(networkConfig.nonMatchingPoolStart)) { - validationErrors.push('Invalid non-matching pool start IP address'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidNonMatchingPoolStart')); } if (networkConfig.nonMatchingPoolEnd && !isValidIPv4(networkConfig.nonMatchingPoolEnd)) { - validationErrors.push('Invalid non-matching pool end IP address'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidNonMatchingPoolEnd')); } // Validate server IP if needed and provided if (needsServerIp && networkConfig.serverIp && !isValidIPv4(networkConfig.serverIp)) { - validationErrors.push('Invalid server IP address'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidServerIp')); } // Validate boot filename if needed and provided if (needsBootFilename && networkConfig.bootFilename && !isValidFilename(networkConfig.bootFilename)) { - validationErrors.push('Invalid boot filename'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidBootFilename')); } // Validate MikroTik server name if (networkConfig.mikrotikServerName && networkConfig.mikrotikServerName.trim().length === 0) { - validationErrors.push('MikroTik server name cannot be empty'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidMikrotikServerName')); } // Validate lease time format (basic check) if (networkConfig.leaseTime && !/^\d+[smhd]$/.test(networkConfig.leaseTime.trim())) { - validationErrors.push('Invalid lease time format (e.g., 24h, 1h, 30m)'); + validationErrors.push($t('tools/dhcp-option60-builder.errors.invalidLeaseTime')); } return validationErrors; @@ -181,11 +182,11 @@ // Validate custom input if custom preset if (selectedPreset === 'custom') { if (!customValue.trim()) { - errors = ['Custom vendor class identifier is required']; + errors = [$t('tools/dhcp-option60-builder.errors.customRequired')]; return; } if (!isValidVendorClass(customValue)) { - errors = ['Invalid vendor class identifier. Must be 1-255 printable ASCII characters.']; + errors = [$t('tools/dhcp-option60-builder.errors.invalidVendorClass')]; return; } } @@ -199,7 +200,7 @@ result = generateOption60(selectedPreset, customValue || undefined, networkConfig); } catch (err: unknown) { - errors = [err instanceof Error ? err.message : 'Failed to generate configuration']; + errors = [err instanceof Error ? err.message : $t('tools/dhcp-option60-builder.errors.failedToGenerate')]; } } @@ -219,20 +220,20 @@ onSelect={loadExample} getLabel={(ex) => VENDOR_PRESETS[ex.preset].name} getDescription={(ex) => ex.description} - getTooltip={(ex) => `Generate config for ${VENDOR_PRESETS[ex.preset].name}`} + getTooltip={(ex) => $t('tools/dhcp-option60-builder.examples.tooltip', { vendor: VENDOR_PRESETS[ex.preset].name })} /> <!-- Input Form --> <div class="card input-card"> <div class="card-header"> - <h3>Vendor Class Configuration</h3> + <h3>{$t('tools/dhcp-option60-builder.input.title')}</h3> </div> <div class="card-content"> <section class="inputs"> <div class="input-group"> <label for="preset"> <Icon name="tag" size="sm" /> - Vendor Preset + {$t('tools/dhcp-option60-builder.input.preset.label')} </label> <select id="preset" @@ -253,25 +254,36 @@ <div class="input-group"> <label for="custom"> <Icon name="edit" size="sm" /> - Custom Vendor Class + {$t('tools/dhcp-option60-builder.input.custom.label')} </label> - <input id="custom" type="text" bind:value={customValue} placeholder="MyCustomVendorClass" maxlength="255" /> - <span class="help-text">1-255 printable ASCII characters</span> + <input + id="custom" + type="text" + bind:value={customValue} + placeholder={$t('tools/dhcp-option60-builder.input.custom.placeholder')} + maxlength="255" + /> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.custom.help')}</span> </div> {/if} <!-- Advanced Configuration Toggle --> <div class="advanced-section"> <details open> - <summary><h3>Advanced Options</h3></summary> + <summary><h3>{$t('tools/dhcp-option60-builder.input.advancedOptions')}</h3></summary> <!-- Subnet Configuration --> <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet (CIDR) + {$t('tools/dhcp-option60-builder.input.subnet.label')} </label> - <input id="subnet" type="text" bind:value={networkConfig.subnet} placeholder="192.168.10.0/24" /> - <span class="help-text">Network address in CIDR notation</span> + <input + id="subnet" + type="text" + bind:value={networkConfig.subnet} + placeholder={$t('tools/dhcp-option60-builder.input.subnet.placeholder')} + /> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.subnet.help')}</span> </div> <!-- Matching Pool Range --> @@ -321,14 +333,12 @@ <div class="input-group"> <label for="serverIp"> <Icon name="server" size="sm" /> - {selectedPreset === 'pxe-client' - ? 'TFTP Server IP' - : selectedPreset === 'docsis' - ? 'Config File Server IP' - : 'TFTP Server IP'} + {selectedPreset === 'docsis' + ? $t('tools/dhcp-option60-builder.input.serverIp.label.config') + : $t('tools/dhcp-option60-builder.input.serverIp.label.tftp')} </label> <input id="serverIp" type="text" bind:value={networkConfig.serverIp} placeholder="192.168.10.5" /> - <span class="help-text">IP address of the provisioning server</span> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.serverIp.help')}</span> </div> {/if} @@ -337,19 +347,21 @@ <div class="input-group"> <label for="bootFilename"> <Icon name="file" size="sm" /> - {selectedPreset === 'docsis' ? 'Config Filename' : 'Boot Filename'} + {selectedPreset === 'docsis' + ? $t('tools/dhcp-option60-builder.input.bootFilename.label.config') + : $t('tools/dhcp-option60-builder.input.bootFilename.label.default')} </label> <input id="bootFilename" type="text" bind:value={networkConfig.bootFilename} placeholder={selectedPreset === 'pxe-client' - ? 'pxelinux.0' + ? $t('tools/dhcp-option60-builder.input.bootFilename.placeholder.pxe') : selectedPreset === 'docsis' - ? 'modem.cfg' - : 'SEPDefault.cnf.xml'} + ? $t('tools/dhcp-option60-builder.input.bootFilename.placeholder.docsis') + : $t('tools/dhcp-option60-builder.input.bootFilename.placeholder.cisco')} /> - <span class="help-text">Name of the configuration or boot file</span> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.bootFilename.help')}</span> </div> {/if} @@ -357,25 +369,30 @@ <div class="input-group"> <label for="mikrotikServerName"> <Icon name="server" size="sm" /> - MikroTik DHCP Server Name + {$t('tools/dhcp-option60-builder.input.mikrotikServerName.label')} </label> <input id="mikrotikServerName" type="text" bind:value={networkConfig.mikrotikServerName} - placeholder="dhcp1" + placeholder={$t('tools/dhcp-option60-builder.input.mikrotikServerName.placeholder')} /> - <span class="help-text">Name of DHCP server in MikroTik config</span> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.mikrotikServerName.help')}</span> </div> <!-- Lease Time --> <div class="input-group"> <label for="leaseTime"> <Icon name="clock" size="sm" /> - Lease Time (dnsmasq) + {$t('tools/dhcp-option60-builder.input.leaseTime.label')} </label> - <input id="leaseTime" type="text" bind:value={networkConfig.leaseTime} placeholder="24h" /> - <span class="help-text">DHCP lease time (e.g., 24h, 1h, 30m)</span> + <input + id="leaseTime" + type="text" + bind:value={networkConfig.leaseTime} + placeholder={$t('tools/dhcp-option60-builder.input.leaseTime.placeholder')} + /> + <span class="help-text">{$t('tools/dhcp-option60-builder.input.leaseTime.help')}</span> </div> </details> </div> @@ -397,14 +414,14 @@ {#if result} <div class="card results"> - <h3>Generated Configurations</h3> + <h3>{$t('tools/dhcp-option60-builder.results.title')}</h3> <!-- Vendor Class Identifier --> <div class="vci-section"> <div class="vci-header"> <h4> <Icon name="tag" size="sm" /> - Vendor Class Identifier (Option 60) + {$t('tools/dhcp-option60-builder.results.vendorClass.title')} </h4> <button type="button" @@ -413,20 +430,22 @@ onclick={() => clipboard.copy(result!.vendorClass, 'vci')} > <Icon name={clipboard.isCopied('vci') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('vci') ? 'Copied' : 'Copy'} + {clipboard.isCopied('vci') + ? $t('tools/dhcp-option60-builder.buttons.copied') + : $t('tools/dhcp-option60-builder.buttons.copy')} </button> </div> <code class="vci-value">{result.vendorClass}</code> <div class="use-case"> <Icon name="info" size="sm" /> - <p><strong>Use Case:</strong> {result.useCase}</p> + <p><strong>{$t('tools/dhcp-option60-builder.results.vendorClass.useCase')}</strong> {result.useCase}</p> </div> </div> <!-- Server Configurations --> <div class="output-formats"> - {#each [{ id: 'isc', title: 'ISC DHCP Server', content: result.iscDhcpConfig, hint: 'Add to /etc/dhcp/dhcpd.conf' }, { id: 'kea', title: 'Kea DHCP Server', content: result.keaConfig, hint: 'Add to Kea configuration JSON' }, { id: 'windows', title: 'Windows DHCP Server', content: result.windowsConfig, hint: 'Run PowerShell commands as Administrator' }, { id: 'dnsmasq', title: 'dnsmasq', content: result.dnsmasqConfig, hint: 'Add to /etc/dnsmasq.conf' }, { id: 'mikrotik', title: 'MikroTik RouterOS', content: result.mikrotikConfig, hint: 'RouterOS CLI commands' }] as config (config.id)} + {#each [{ id: 'isc', title: $t('tools/dhcp-option60-builder.results.formats.isc.title'), content: result.iscDhcpConfig, hint: $t('tools/dhcp-option60-builder.results.formats.isc.hint') }, { id: 'kea', title: $t('tools/dhcp-option60-builder.results.formats.kea.title'), content: result.keaConfig, hint: $t('tools/dhcp-option60-builder.results.formats.kea.hint') }, { id: 'windows', title: $t('tools/dhcp-option60-builder.results.formats.windows.title'), content: result.windowsConfig, hint: $t('tools/dhcp-option60-builder.results.formats.windows.hint') }, { id: 'dnsmasq', title: $t('tools/dhcp-option60-builder.results.formats.dnsmasq.title'), content: result.dnsmasqConfig, hint: $t('tools/dhcp-option60-builder.results.formats.dnsmasq.hint') }, { id: 'mikrotik', title: $t('tools/dhcp-option60-builder.results.formats.mikrotik.title'), content: result.mikrotikConfig, hint: $t('tools/dhcp-option60-builder.results.formats.mikrotik.hint') }] as config (config.id)} <div class="output-group"> <div class="output-header"> <h4>{config.title}</h4> @@ -437,7 +456,9 @@ onclick={() => clipboard.copy(config.content, config.id)} > <Icon name={clipboard.isCopied(config.id) ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied(config.id) ? 'Copied' : 'Copy'} + {clipboard.isCopied(config.id) + ? $t('tools/dhcp-option60-builder.buttons.copied') + : $t('tools/dhcp-option60-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{config.content}</pre> @@ -448,11 +469,10 @@ </div> <div class="card info"> - <h3>Important Notes</h3> + <h3>{$t('tools/dhcp-option60-builder.notes.title')}</h3> <ul class="notes-list"> {#each importantNotes as note (note)} - <!-- eslint-disable-next-line svelte/no-at-html-tags --> - <li>{@html note}</li> + <li>{note}</li> {/each} </ul> </div> diff --git a/src/lib/components/tools/DHCPOption82Builder.svelte b/src/lib/components/tools/DHCPOption82Builder.svelte index 217352e5..56bd0e24 100644 --- a/src/lib/components/tools/DHCPOption82Builder.svelte +++ b/src/lib/components/tools/DHCPOption82Builder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { buildOption82, parseOption82, @@ -15,10 +16,10 @@ type EncodingFormat, } from '$lib/utils/dhcp-option82.js'; - const modeOptions = [ - { value: 'build' as const, label: 'Build', icon: 'wrench' }, - { value: 'parse' as const, label: 'Parse', icon: 'search' }, - ]; + const modeOptions = $derived([ + { value: 'build' as const, label: $t('tools/dhcp-option82-builder.modes.build'), icon: 'wrench' }, + { value: 'parse' as const, label: $t('tools/dhcp-option82-builder.modes.parse'), icon: 'search' }, + ]); let mode = $state<'build' | 'parse'>('build'); let config = $state<Option82Config>(getDefaultOption82Config()); @@ -30,12 +31,12 @@ const clipboard = useClipboard(); - const formatOptions: Array<{ value: EncodingFormat; label: string }> = [ - { value: 'ascii', label: 'ASCII Text' }, - { value: 'hex', label: 'Hexadecimal' }, - { value: 'vlan-id', label: 'VLAN ID' }, - { value: 'hostname-port', label: 'Hostname:Port' }, - ]; + const formatOptions = $derived<Array<{ value: EncodingFormat; label: string }>>([ + { value: 'ascii', label: $t('tools/dhcp-option82-builder.formats.ascii') }, + { value: 'hex', label: $t('tools/dhcp-option82-builder.formats.hex') }, + { value: 'vlan-id', label: $t('tools/dhcp-option82-builder.formats.vlanId') }, + { value: 'hostname-port', label: $t('tools/dhcp-option82-builder.formats.hostnamePort') }, + ]); interface BuildExample { label: string; @@ -51,68 +52,68 @@ description: string; } - const buildExamples: BuildExample[] = [ + const buildExamples = $derived<BuildExample[]>([ { - label: 'VLAN 100', + label: $t('tools/dhcp-option82-builder.buildExamples.vlan100.label'), type: 'circuit-id', format: 'vlan-id', value: '100', - description: 'Circuit-ID as VLAN ID 100', + description: $t('tools/dhcp-option82-builder.buildExamples.vlan100.description'), }, { - label: 'Switch Port', + label: $t('tools/dhcp-option82-builder.buildExamples.switchPort.label'), type: 'circuit-id', format: 'hostname-port', value: 'sw1:Gi0/1', - description: 'Circuit-ID as hostname:port', + description: $t('tools/dhcp-option82-builder.buildExamples.switchPort.description'), }, { - label: 'Custom Circuit', + label: $t('tools/dhcp-option82-builder.buildExamples.customCircuit.label'), type: 'circuit-id', format: 'ascii', value: 'building-a-floor-3', - description: 'Circuit-ID as custom ASCII text', + description: $t('tools/dhcp-option82-builder.buildExamples.customCircuit.description'), }, { - label: 'Switch Hostname', + label: $t('tools/dhcp-option82-builder.buildExamples.switchHostname.label'), type: 'remote-id', format: 'ascii', value: 'relay-sw1.example.com', - description: 'Remote-ID as hostname', + description: $t('tools/dhcp-option82-builder.buildExamples.switchHostname.description'), }, { - label: 'MAC Address', + label: $t('tools/dhcp-option82-builder.buildExamples.macAddress.label'), type: 'remote-id', format: 'hex', value: '001122334455', - description: 'Remote-ID as MAC address', + description: $t('tools/dhcp-option82-builder.buildExamples.macAddress.description'), }, { - label: 'Agent ID', + label: $t('tools/dhcp-option82-builder.buildExamples.agentId.label'), type: 'remote-id', format: 'ascii', value: 'DHCP-RELAY-01', - description: 'Remote-ID as relay agent identifier', + description: $t('tools/dhcp-option82-builder.buildExamples.agentId.description'), }, - ]; + ]); - const parseExamples: ParseExample[] = [ + const parseExamples = $derived<ParseExample[]>([ { - label: 'VLAN + Hostname', + label: $t('tools/dhcp-option82-builder.parseExamples.vlanHostname.label'), hexInput: '01020064020c7377312e6578616d706c65', - description: 'Circuit-ID (VLAN 100) + Remote-ID (sw1.example)', + description: $t('tools/dhcp-option82-builder.parseExamples.vlanHostname.description'), }, { - label: 'Switch Port', + label: $t('tools/dhcp-option82-builder.parseExamples.switchPort.label'), hexInput: '01094769302f31020c7377312e6578616d706c65', - description: 'Circuit-ID (Gi0/1) + Remote-ID (sw1.example)', + description: $t('tools/dhcp-option82-builder.parseExamples.switchPort.description'), }, { - label: 'MAC Address', + label: $t('tools/dhcp-option82-builder.parseExamples.macAddress.label'), hexInput: '0206001122334455', - description: 'Remote-ID as MAC address (00:11:22:33:44:55)', + description: $t('tools/dhcp-option82-builder.parseExamples.macAddress.description'), }, - ]; + ]); // Reactive generation - use untrack to prevent infinite loop $effect(() => { @@ -153,20 +154,20 @@ const sub = cfg.suboptions[i]; if (!sub.value.trim()) { - errors.push(`Suboption ${i + 1}: Value is required`); + errors.push($t('tools/dhcp-option82-builder.errors.valueRequired', { number: i + 1 })); continue; } if (sub.format === 'vlan-id') { const vlan = parseInt(sub.value, 10); if (isNaN(vlan) || vlan < 0 || vlan > 4095) { - errors.push(`Suboption ${i + 1}: VLAN ID must be between 0 and 4095`); + errors.push($t('tools/dhcp-option82-builder.errors.vlanRange', { number: i + 1 })); } } if (sub.format === 'hex') { if (!/^[0-9a-fA-F:]+$/.test(sub.value.replace(/\s/g, ''))) { - errors.push(`Suboption ${i + 1}: Invalid hex format`); + errors.push($t('tools/dhcp-option82-builder.errors.invalidHex', { number: i + 1 })); } } } @@ -188,7 +189,7 @@ } if (!/^[0-9a-fA-F\s:]+$/.test(parseInput)) { - validationErrors = ['Invalid hex input: only hexadecimal characters allowed']; + validationErrors = [$t('tools/dhcp-option82-builder.errors.invalidHexInput')]; parseResult = null; return; } @@ -267,8 +268,8 @@ </script> <ToolContentContainer - title="DHCP Option 82 Builder" - description="Construct and parse DHCP Relay Agent Information (Option 82) with Circuit-ID, Remote-ID, and VLAN formats. Includes examples for relay ACLs and policies." + title={$t('tools/dhcp-option82-builder.title')} + description={$t('tools/dhcp-option82-builder.subtitle')} navOptions={modeOptions} bind:selectedNav={mode} > @@ -293,13 +294,13 @@ {#if mode === 'build'} <div class="card input-card"> <div class="card-header"> - <h3>Configuration</h3> + <h3>{$t('tools/dhcp-option82-builder.build.configurationTitle')}</h3> </div> <div class="card-content"> {#each config.suboptions as suboption, i (`sub-${i}-${suboption.type}`)} <div class="suboption-group"> <div class="suboption-header"> - <h4>Suboption {i + 1}</h4> + <h4>{$t('tools/dhcp-option82-builder.build.suboption.title', { number: i + 1 })}</h4> {#if config.suboptions.length > 1} <button type="button" class="btn-icon" onclick={() => removeSuboption(i)}> <Icon name="x" size="sm" /> @@ -311,18 +312,18 @@ <div class="input-group"> <label for="type-{i}"> <Icon name="tag" size="sm" /> - Suboption Type + {$t('tools/dhcp-option82-builder.build.suboption.typeLabel')} </label> <select id="type-{i}" bind:value={suboption.type}> - <option value="circuit-id">Circuit-ID (Suboption 1)</option> - <option value="remote-id">Remote-ID (Suboption 2)</option> + <option value="circuit-id">{$t('tools/dhcp-option82-builder.build.suboption.circuitId')}</option> + <option value="remote-id">{$t('tools/dhcp-option82-builder.build.suboption.remoteId')}</option> </select> </div> <div class="input-group"> <label for="format-{i}"> <Icon name="code" size="sm" /> - Encoding Format + {$t('tools/dhcp-option82-builder.build.suboption.encodingFormat')} </label> <select id="format-{i}" bind:value={suboption.format}> {#each formatOptions as option (option.value)} @@ -335,17 +336,17 @@ <div class="input-group"> <label for="value-{i}"> <Icon name="edit" size="sm" /> - Value + {$t('tools/dhcp-option82-builder.build.suboption.valueLabel')} </label> <input id="value-{i}" type="text" bind:value={suboption.value} placeholder={suboption.format === 'vlan-id' - ? '100' + ? $t('tools/dhcp-option82-builder.build.suboption.placeholders.vlanId') : suboption.format === 'hex' - ? '001122334455' - : 'Enter value'} + ? $t('tools/dhcp-option82-builder.build.suboption.placeholders.hex') + : $t('tools/dhcp-option82-builder.build.suboption.placeholders.default')} /> </div> </div> @@ -353,14 +354,14 @@ <button type="button" class="btn-add" onclick={addSuboption}> <Icon name="plus" size="sm" /> - Add Suboption + {$t('tools/dhcp-option82-builder.build.addSuboption')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option82-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -372,11 +373,11 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Generated Option 82</h3> + <h3>{$t('tools/dhcp-option82-builder.build.results.title')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded Value</h4> + <h4>{$t('tools/dhcp-option82-builder.build.results.hexEncoded')}</h4> <button type="button" class="copy-btn" @@ -384,24 +385,39 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/dhcp-option82-builder.buttons.copied') + : $t('tools/dhcp-option82-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> </div> <div class="breakdown-section"> - <h4>Breakdown</h4> + <h4>{$t('tools/dhcp-option82-builder.build.results.breakdown')}</h4> {#each result.breakdown as breakdown, i (i)} <div class="breakdown-item"> <div class="breakdown-header"> - <strong>{breakdown.type} (Code {breakdown.typeCode})</strong> - <span class="breakdown-length">Length: {breakdown.length} bytes</span> + <strong + >{$t('tools/dhcp-option82-builder.build.results.typeCode', { + type: breakdown.type, + code: breakdown.typeCode, + })}</strong + > + <span class="breakdown-length" + >{$t('tools/dhcp-option82-builder.build.results.length', { length: breakdown.length })}</span + > </div> <p class="breakdown-desc">{breakdown.description}</p> <div class="breakdown-values"> - <div><strong>Value:</strong> {breakdown.value}</div> - <div><strong>Hex:</strong> {breakdown.hexValue}</div> + <div> + <strong>{$t('tools/dhcp-option82-builder.build.results.valueLabel')}</strong> + {breakdown.value} + </div> + <div> + <strong>{$t('tools/dhcp-option82-builder.build.results.hexLabel')}</strong> + {breakdown.hexValue} + </div> </div> </div> {/each} @@ -410,7 +426,7 @@ {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration Example</h4> + <h4>{$t('tools/dhcp-option82-builder.build.results.examples.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -418,7 +434,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-option82-builder.buttons.copied') + : $t('tools/dhcp-option82-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -428,7 +446,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration Example</h4> + <h4>{$t('tools/dhcp-option82-builder.build.results.examples.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -436,7 +454,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-option82-builder.buttons.copied') + : $t('tools/dhcp-option82-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -446,7 +466,7 @@ {#if result.examples.ciscoRelay} <div class="output-group"> <div class="output-header"> - <h4>Cisco Relay Agent Example</h4> + <h4>{$t('tools/dhcp-option82-builder.build.results.examples.ciscoRelay')}</h4> <button type="button" class="copy-btn" @@ -454,7 +474,9 @@ onclick={() => clipboard.copy(result!.examples.ciscoRelay!, 'cisco')} > <Icon name={clipboard.isCopied('cisco') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('cisco') ? 'Copied' : 'Copy'} + {clipboard.isCopied('cisco') + ? $t('tools/dhcp-option82-builder.buttons.copied') + : $t('tools/dhcp-option82-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.ciscoRelay}</pre> @@ -465,31 +487,31 @@ {:else} <div class="card input-card"> <div class="card-header"> - <h3>Parse Option 82 Hex</h3> + <h3>{$t('tools/dhcp-option82-builder.parse.title')}</h3> </div> <div class="card-content"> <div class="input-group"> <label for="parse-input"> <Icon name="code" size="sm" /> - Hex-Encoded Option 82 + {$t('tools/dhcp-option82-builder.parse.hexEncoded')} </label> <textarea id="parse-input" bind:value={parseInput} - placeholder="Enter hex string (e.g., 01064769302f31020b7377312e6578616d706c65)" + placeholder={$t('tools/dhcp-option82-builder.parse.placeholder')} rows="4" ></textarea> </div> <button type="button" class="btn-primary" onclick={parse}> <Icon name="search" size="sm" /> - Parse + {$t('tools/dhcp-option82-builder.parse.button')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-option82-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -501,25 +523,44 @@ {#if parseResult && validationErrors.length === 0} <div class="card results"> - <h3>Parsed Option 82</h3> + <h3>{$t('tools/dhcp-option82-builder.parse.results.title')}</h3> <div class="parse-summary"> - <div><strong>Total Length:</strong> {parseResult.totalLength} bytes</div> - <div><strong>Suboptions Found:</strong> {parseResult.suboptions.length}</div> + <div> + <strong>{$t('tools/dhcp-option82-builder.parse.results.totalLength')}</strong> + {$t('tools/dhcp-option82-builder.parse.results.bytes', { length: parseResult.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcp-option82-builder.parse.results.suboptionsFound')}</strong> + {$t('tools/dhcp-option82-builder.parse.results.count', { count: parseResult.suboptions.length })} + </div> </div> <div class="breakdown-section"> - <h4>Suboptions</h4> + <h4>{$t('tools/dhcp-option82-builder.parse.results.suboptionsTitle')}</h4> {#each parseResult.suboptions as suboption, i (i)} <div class="breakdown-item"> <div class="breakdown-header"> - <strong>{suboption.type} (Code {suboption.typeCode})</strong> - <span class="breakdown-length">Length: {suboption.length} bytes</span> + <strong + >{$t('tools/dhcp-option82-builder.parse.results.typeCode', { + type: suboption.type, + code: suboption.typeCode, + })}</strong + > + <span class="breakdown-length" + >{$t('tools/dhcp-option82-builder.parse.results.length', { length: suboption.length })}</span + > </div> <p class="breakdown-desc">{suboption.description}</p> <div class="breakdown-values"> - <div><strong>Decoded Value:</strong> {suboption.value}</div> - <div><strong>Hex Value:</strong> {suboption.hexValue}</div> + <div> + <strong>{$t('tools/dhcp-option82-builder.parse.results.decodedValue')}</strong> + {suboption.value} + </div> + <div> + <strong>{$t('tools/dhcp-option82-builder.parse.results.hexValue')}</strong> + {suboption.hexValue} + </div> </div> </div> {/each} diff --git a/src/lib/components/tools/DHCPSnippetsGenerator.svelte b/src/lib/components/tools/DHCPSnippetsGenerator.svelte index d461c559..1e3bc879 100644 --- a/src/lib/components/tools/DHCPSnippetsGenerator.svelte +++ b/src/lib/components/tools/DHCPSnippetsGenerator.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { generateSnippets, getDefaultSnippetConfig, @@ -15,11 +16,11 @@ const clipboard = useClipboard(); - const targetOptions: Array<{ value: DhcpTarget; label: string }> = [ - { value: 'isc-dhcpd', label: 'ISC dhcpd' }, - { value: 'kea-dhcp4', label: 'Kea DHCPv4' }, - { value: 'kea-dhcp6', label: 'Kea DHCPv6' }, - ]; + const targetOptions = $derived<Array<{ value: DhcpTarget; label: string }>>([ + { value: 'isc-dhcpd', label: $t('tools/dhcp-snippets-generator.targets.iscDhcpd') }, + { value: 'kea-dhcp4', label: $t('tools/dhcp-snippets-generator.targets.keaDhcp4') }, + { value: 'kea-dhcp6', label: $t('tools/dhcp-snippets-generator.targets.keaDhcp6') }, + ]); // Reactive generation $effect(() => { @@ -42,7 +43,7 @@ } } - function toggleTarget(target: DhcpTarget) { + function toggleTarge$t(target: DhcpTarget) { if (config.targets.includes(target)) { config.targets = config.targets.filter((t) => t !== target); } else { @@ -53,14 +54,14 @@ <div class="card input-card"> <div class="card-header"> - <h3>Configuration</h3> + <h3>{$t('tools/dhcp-snippets-generator.configuration.title')}</h3> </div> <div class="card-content"> <!-- Target selection --> <div class="input-group"> <label> <Icon name="server" size="sm" /> - Target Servers + {$t('tools/dhcp-snippets-generator.configuration.targetServers')} </label> <div class="checkbox-group"> {#each targetOptions as option (option.value)} @@ -68,7 +69,7 @@ <input type="checkbox" checked={config.targets.includes(option.value)} - onchange={() => toggleTarget(option.value)} + onchange={() => toggleTarge$t(option.value)} /> {option.label} </label> @@ -80,11 +81,11 @@ <div class="input-group"> <label for="mode"> <Icon name="network" size="sm" /> - IP Mode + {$t('tools/dhcp-snippets-generator.configuration.ipMode')} </label> <select id="mode" bind:value={config.mode}> - <option value="dhcp4">DHCPv4 (IPv4)</option> - <option value="dhcp6">DHCPv6 (IPv6)</option> + <option value="dhcp4">{$t('tools/dhcp-snippets-generator.configuration.modes.dhcp4')}</option> + <option value="dhcp6">{$t('tools/dhcp-snippets-generator.configuration.modes.dhcp6')}</option> </select> </div> @@ -92,13 +93,15 @@ <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet (CIDR) + {$t('tools/dhcp-snippets-generator.configuration.subnet.label')} </label> <input id="subnet" type="text" bind:value={config.subnet} - placeholder={config.mode === 'dhcp6' ? '2001:db8::/64' : '192.168.1.0/24'} + placeholder={config.mode === 'dhcp6' + ? $t('tools/dhcp-snippets-generator.configuration.subnet.placeholderV6') + : $t('tools/dhcp-snippets-generator.configuration.subnet.placeholderV4')} /> </div> @@ -106,13 +109,21 @@ <div class="input-group"> <label> <Icon name="layers" size="sm" /> - Address Pools + {$t('tools/dhcp-snippets-generator.configuration.pools.label')} </label> {#each config.pools as pool, i (i)} <div class="pool-row"> - <input type="text" bind:value={pool.start} placeholder="Start IP" /> + <input + type="text" + bind:value={pool.start} + placeholder={$t('tools/dhcp-snippets-generator.configuration.pools.startPlaceholder')} + /> <span>-</span> - <input type="text" bind:value={pool.end} placeholder="End IP" /> + <input + type="text" + bind:value={pool.end} + placeholder={$t('tools/dhcp-snippets-generator.configuration.pools.endPlaceholder')} + /> {#if config.pools.length > 1} <button type="button" class="btn-icon" onclick={() => removePool(i)}> <Icon name="x" size="sm" /> @@ -122,7 +133,7 @@ {/each} <button type="button" class="btn-add" onclick={addPool}> <Icon name="plus" size="sm" /> - Add Pool + {$t('tools/dhcp-snippets-generator.configuration.pools.addPool')} </button> </div> @@ -130,13 +141,15 @@ <div class="input-group"> <label for="gateway"> <Icon name="arrow-right" size="sm" /> - Gateway (Router) + {$t('tools/dhcp-snippets-generator.configuration.gateway.label')} </label> <input id="gateway" type="text" bind:value={config.gateway} - placeholder={config.mode === 'dhcp6' ? 'fe80::1' : '192.168.1.1'} + placeholder={config.mode === 'dhcp6' + ? $t('tools/dhcp-snippets-generator.configuration.gateway.placeholderV6') + : $t('tools/dhcp-snippets-generator.configuration.gateway.placeholderV4')} /> </div> @@ -144,14 +157,14 @@ <div class="input-group"> <label for="dns"> <Icon name="globe" size="sm" /> - DNS Servers (comma-separated) + {$t('tools/dhcp-snippets-generator.configuration.dns.label')} </label> <input id="dns" type="text" value={config.dnsServers?.join(', ') || ''} oninput={(e) => (config.dnsServers = e.currentTarget.value.split(',').map((s) => s.trim()))} - placeholder="8.8.8.8, 8.8.4.4" + placeholder={$t('tools/dhcp-snippets-generator.configuration.dns.placeholder')} /> </div> @@ -159,9 +172,14 @@ <div class="input-group"> <label for="domain"> <Icon name="globe" size="sm" /> - Domain Name + {$t('tools/dhcp-snippets-generator.configuration.domain.label')} </label> - <input id="domain" type="text" bind:value={config.domainName} placeholder="example.com" /> + <input + id="domain" + type="text" + bind:value={config.domainName} + placeholder={$t('tools/dhcp-snippets-generator.configuration.domain.placeholder')} + /> </div> <!-- Lease times --> @@ -169,16 +187,26 @@ <div class="input-group"> <label for="defaultLease"> <Icon name="clock" size="sm" /> - Default Lease (seconds) + {$t('tools/dhcp-snippets-generator.configuration.leases.defaultLabel')} </label> - <input id="defaultLease" type="number" bind:value={config.defaultLeaseTime} placeholder="86400" /> + <input + id="defaultLease" + type="number" + bind:value={config.defaultLeaseTime} + placeholder={$t('tools/dhcp-snippets-generator.configuration.leases.defaultPlaceholder')} + /> </div> <div class="input-group"> <label for="maxLease"> <Icon name="clock" size="sm" /> - Max Lease (seconds) + {$t('tools/dhcp-snippets-generator.configuration.leases.maxLabel')} </label> - <input id="maxLease" type="number" bind:value={config.maxLeaseTime} placeholder="604800" /> + <input + id="maxLease" + type="number" + bind:value={config.maxLeaseTime} + placeholder={$t('tools/dhcp-snippets-generator.configuration.leases.maxPlaceholder')} + /> </div> </div> @@ -186,11 +214,11 @@ <div class="input-row"> <label class="checkbox-label"> <input type="checkbox" bind:checked={config.emitOptionNames} /> - Use option names (ISC) + {$t('tools/dhcp-snippets-generator.configuration.options.useOptionNames')} </label> <label class="checkbox-label"> <input type="checkbox" bind:checked={config.prettyJson} /> - Pretty JSON (Kea) + {$t('tools/dhcp-snippets-generator.configuration.options.prettyJson')} </label> </div> </div> @@ -199,7 +227,7 @@ {#if result} {#if result.validations.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-snippets-generator.errors.title')}</h3> {#each result.validations as error (error.field)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -215,12 +243,12 @@ </div> <div class="card results"> - <h3>Generated Snippets</h3> + <h3>{$t('tools/dhcp-snippets-generator.results.title')}</h3> {#if result.iscDhcpdSnippet} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd.conf</h4> + <h4>{$t('tools/dhcp-snippets-generator.results.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -228,7 +256,9 @@ onclick={() => clipboard.copy(result!.iscDhcpdSnippet!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-snippets-generator.buttons.copied') + : $t('tools/dhcp-snippets-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.iscDhcpdSnippet}</pre> @@ -238,7 +268,7 @@ {#if result.keaDhcp4Snippet} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 JSON</h4> + <h4>{$t('tools/dhcp-snippets-generator.results.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -246,7 +276,9 @@ onclick={() => clipboard.copy(result!.keaDhcp4Snippet!, 'kea4')} > <Icon name={clipboard.isCopied('kea4') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea4') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea4') + ? $t('tools/dhcp-snippets-generator.buttons.copied') + : $t('tools/dhcp-snippets-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.keaDhcp4Snippet}</pre> @@ -256,7 +288,7 @@ {#if result.keaDhcp6Snippet} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv6 JSON</h4> + <h4>{$t('tools/dhcp-snippets-generator.results.keaDhcp6')}</h4> <button type="button" class="copy-btn" @@ -264,7 +296,9 @@ onclick={() => clipboard.copy(result!.keaDhcp6Snippet!, 'kea6')} > <Icon name={clipboard.isCopied('kea6') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea6') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea6') + ? $t('tools/dhcp-snippets-generator.buttons.copied') + : $t('tools/dhcp-snippets-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.keaDhcp6Snippet}</pre> diff --git a/src/lib/components/tools/DHCPv6DNSBuilder.svelte b/src/lib/components/tools/DHCPv6DNSBuilder.svelte index aef2db27..cf362c74 100644 --- a/src/lib/components/tools/DHCPv6DNSBuilder.svelte +++ b/src/lib/components/tools/DHCPv6DNSBuilder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { type DNSv6Config, type DNSv6Result, @@ -30,28 +31,28 @@ description: string; } - const examples: DNSv6Example[] = [ + const examples = $derived<DNSv6Example[]>([ { - label: 'Google Public DNS', + label: $t('tools/dhcpv6-dns-builder.examples.googleDNS.label'), config: DNSv6_EXAMPLES[0], - description: 'Google Public DNS servers with example.com search domains', + description: $t('tools/dhcpv6-dns-builder.examples.googleDNS.description'), }, { - label: 'Cloudflare DNS', + label: $t('tools/dhcpv6-dns-builder.examples.cloudflareDNS.label'), config: DNSv6_EXAMPLES[1], - description: 'Cloudflare 1.1.1.1 DNS with local search domain', + description: $t('tools/dhcpv6-dns-builder.examples.cloudflareDNS.description'), }, { - label: 'Quad9 DNS', + label: $t('tools/dhcpv6-dns-builder.examples.quad9DNS.label'), config: DNSv6_EXAMPLES[2], - description: 'Quad9 DNS with corporate search domains', + description: $t('tools/dhcpv6-dns-builder.examples.quad9DNS.description'), }, { - label: 'Local Network', + label: $t('tools/dhcpv6-dns-builder.examples.localNetwork.label'), config: DNSv6_EXAMPLES[3], - description: 'Local ULA DNS server with home.arpa domain', + description: $t('tools/dhcpv6-dns-builder.examples.localNetwork.description'), }, - ]; + ]); function loadExample(example: DNSv6Example, index: number): void { config = { @@ -150,8 +151,8 @@ </script> <ToolContentContainer - title="DHCPv6 DNS Options (RFC 3646)" - description="Configure DNS servers (Option 23) and search domains (Option 24) for DHCPv6 clients. Supports IPv6 DNS servers and multiple search domains." + title={$t('tools/dhcpv6-dns-builder.title')} + description={$t('tools/dhcpv6-dns-builder.subtitle')} > <ExamplesCard {examples} @@ -163,8 +164,8 @@ <div class="card input-card"> <div class="card-header"> - <h3>Option 23: DNS Recursive Name Servers</h3> - <p class="help-text">IPv6 addresses of DNS servers for client name resolution</p> + <h3>{$t('tools/dhcpv6-dns-builder.option23.title')}</h3> + <p class="help-text">{$t('tools/dhcpv6-dns-builder.option23.helpText')}</p> </div> <div class="card-content"> {#each config.dnsServers as _, i (`dns-${i}`)} @@ -172,13 +173,13 @@ <div class="input-group flex-grow"> <label for="dns-server-{i}"> <Icon name="server" size="sm" /> - DNS Server {i + 1} + {$t('tools/dhcpv6-dns-builder.option23.serverLabel', { number: i + 1 })} </label> <input id="dns-server-{i}" type="text" bind:value={config.dnsServers[i]} - placeholder="2001:4860:4860::8888" + placeholder={$t('tools/dhcpv6-dns-builder.option23.placeholder')} /> </div> <button @@ -186,7 +187,7 @@ class="btn-icon btn-remove" onclick={() => removeDNSServer(i)} disabled={config.dnsServers.length === 1} - aria-label="Remove DNS server" + aria-label={$t('tools/dhcpv6-dns-builder.option23.removeLabel')} > <Icon name="x" size="sm" /> </button> @@ -195,15 +196,15 @@ <button type="button" class="btn-add" onclick={addDNSServer}> <Icon name="plus" size="sm" /> - Add DNS Server + {$t('tools/dhcpv6-dns-builder.option23.addButton')} </button> </div> </div> <div class="card input-card"> <div class="card-header"> - <h3>Option 24: Domain Search List</h3> - <p class="help-text">DNS search domains for hostname resolution</p> + <h3>{$t('tools/dhcpv6-dns-builder.option24.title')}</h3> + <p class="help-text">{$t('tools/dhcpv6-dns-builder.option24.helpText')}</p> </div> <div class="card-content"> {#each config.searchDomains as _, i (`domain-${i}`)} @@ -211,16 +212,21 @@ <div class="input-group flex-grow"> <label for="search-domain-{i}"> <Icon name="globe" size="sm" /> - Search Domain {i + 1} + {$t('tools/dhcpv6-dns-builder.option24.domainLabel', { number: i + 1 })} </label> - <input id="search-domain-{i}" type="text" bind:value={config.searchDomains[i]} placeholder="example.com" /> + <input + id="search-domain-{i}" + type="text" + bind:value={config.searchDomains[i]} + placeholder={$t('tools/dhcpv6-dns-builder.option24.placeholder')} + /> </div> <button type="button" class="btn-icon btn-remove" onclick={() => removeSearchDomain(i)} disabled={config.searchDomains.length === 1} - aria-label="Remove search domain" + aria-label={$t('tools/dhcpv6-dns-builder.option24.removeLabel')} > <Icon name="x" size="sm" /> </button> @@ -229,14 +235,14 @@ <button type="button" class="btn-add" onclick={addSearchDomain}> <Icon name="plus" size="sm" /> - Add Search Domain + {$t('tools/dhcpv6-dns-builder.option24.addButton')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcpv6-dns-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -249,19 +255,25 @@ {#if result && validationErrors.length === 0} {#if result.option23} <div class="card results"> - <h3>Option 23: DNS Recursive Name Servers</h3> + <h3>{$t('tools/dhcpv6-dns-builder.results.option23Title')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.option23.totalLength} bytes</div> - <div><strong>Servers:</strong> {result.option23.servers.length}</div> + <div> + <strong>{$t('tools/dhcpv6-dns-builder.results.totalLength')}</strong> + {$t('tools/dhcpv6-dns-builder.results.lengthBytes', { length: result.option23.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcpv6-dns-builder.results.servers')}</strong> + {$t('tools/dhcpv6-dns-builder.results.serversCount', { count: result.option23.servers.length })} + </div> </div> <div class="servers-section"> - <h4>DNS Servers</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.dnsServersHeading')}</h4> {#each result.option23.servers as server, i (i)} <div class="server-item"> <Icon name="server" size="sm" /> - <span class="field-label">Server {i + 1}:</span> + <span class="field-label">{$t('tools/dhcpv6-dns-builder.results.serverLabel', { number: i + 1 })}</span> <span class="field-value">{server}</span> </div> {/each} @@ -269,7 +281,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.hexEncodedTitle')}</h4> <button type="button" class="copy-btn" @@ -277,7 +289,9 @@ onclick={() => clipboard.copy(result!.option23!.hexEncoded, 'opt23-hex')} > <Icon name={clipboard.isCopied('opt23-hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt23-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt23-hex') + ? $t('tools/dhcpv6-dns-builder.buttons.copied') + : $t('tools/dhcpv6-dns-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option23.hexEncoded}</pre> @@ -285,7 +299,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.wireFormatTitle')}</h4> <button type="button" class="copy-btn" @@ -293,7 +307,9 @@ onclick={() => clipboard.copy(result!.option23!.wireFormat, 'opt23-wire')} > <Icon name={clipboard.isCopied('opt23-wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt23-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt23-wire') + ? $t('tools/dhcpv6-dns-builder.buttons.copied') + : $t('tools/dhcpv6-dns-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option23.wireFormat}</pre> @@ -303,19 +319,25 @@ {#if result.option24} <div class="card results"> - <h3>Option 24: Domain Search List</h3> + <h3>{$t('tools/dhcpv6-dns-builder.results.option24Title')}</h3> <div class="summary-card"> - <div><strong>Total Length:</strong> {result.option24.totalLength} bytes</div> - <div><strong>Domains:</strong> {result.option24.domains.length}</div> + <div> + <strong>{$t('tools/dhcpv6-dns-builder.results.totalLength')}</strong> + {$t('tools/dhcpv6-dns-builder.results.lengthBytes', { length: result.option24.totalLength })} + </div> + <div> + <strong>{$t('tools/dhcpv6-dns-builder.results.domains')}</strong> + {$t('tools/dhcpv6-dns-builder.results.domainsCount', { count: result.option24.domains.length })} + </div> </div> <div class="servers-section"> - <h4>Search Domains</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.searchDomainsHeading')}</h4> {#each result.option24.domains as domain, i (i)} <div class="server-item"> <Icon name="globe" size="sm" /> - <span class="field-label">Domain {i + 1}:</span> + <span class="field-label">{$t('tools/dhcpv6-dns-builder.results.domainLabel', { number: i + 1 })}</span> <span class="field-value">{domain}</span> </div> {/each} @@ -323,7 +345,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.hexEncodedTitle')}</h4> <button type="button" class="copy-btn" @@ -331,7 +353,9 @@ onclick={() => clipboard.copy(result!.option24!.hexEncoded, 'opt24-hex')} > <Icon name={clipboard.isCopied('opt24-hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt24-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt24-hex') + ? $t('tools/dhcpv6-dns-builder.buttons.copied') + : $t('tools/dhcpv6-dns-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option24.hexEncoded}</pre> @@ -339,7 +363,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.wireFormatTitle')}</h4> <button type="button" class="copy-btn" @@ -347,7 +371,9 @@ onclick={() => clipboard.copy(result!.option24!.wireFormat, 'opt24-wire')} > <Icon name={clipboard.isCopied('opt24-wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('opt24-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('opt24-wire') + ? $t('tools/dhcpv6-dns-builder.buttons.copied') + : $t('tools/dhcpv6-dns-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.option24.wireFormat}</pre> @@ -355,7 +381,7 @@ {#if result.option24.breakdown.length > 0} <div class="breakdown-section"> - <h4>Domain Encoding Breakdown</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.breakdownTitle')}</h4> {#each result.option24.breakdown as item, i (i)} <div class="breakdown-item"> <div class="breakdown-label">{item.domain}</div> @@ -369,11 +395,11 @@ {#if result.examples.keaDhcp6} <div class="card results"> - <h3>Configuration Example</h3> + <h3>{$t('tools/dhcpv6-dns-builder.results.configExampleTitle')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv6 Configuration</h4> + <h4>{$t('tools/dhcpv6-dns-builder.results.keaDhcpv6Title')}</h4> <button type="button" class="copy-btn" @@ -381,7 +407,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp6!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcpv6-dns-builder.buttons.copied') + : $t('tools/dhcpv6-dns-builder.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp6}</pre> @@ -390,23 +418,22 @@ {/if} <div class="card results info-card"> - <h3>About RFC 3646</h3> + <h3>{$t('tools/dhcpv6-dns-builder.about.title')}</h3> <p> - RFC 3646 defines DNS configuration options for DHCPv6, allowing IPv6 clients to automatically discover DNS - servers and search domains. + {$t('tools/dhcpv6-dns-builder.about.intro')} </p> <ul> <li> - <strong>Option 23:</strong> DNS Recursive Name Server - List of IPv6 DNS server addresses (16 bytes each) + <strong>Option 23:</strong> + {$t('tools/dhcpv6-dns-builder.about.option23Description')} </li> <li> - <strong>Option 24:</strong> Domain Search List - DNS search domains encoded in DNS wire format (length-prefixed - labels) + <strong>Option 24:</strong> + {$t('tools/dhcpv6-dns-builder.about.option24Description')} </li> </ul> <p> - These options are essential for IPv6 network autoconfiguration, enabling clients to resolve hostnames without - manual DNS configuration. + {$t('tools/dhcpv6-dns-builder.about.conclusion')} </p> </div> {/if} diff --git a/src/lib/components/tools/DHCPv6FQDN.svelte b/src/lib/components/tools/DHCPv6FQDN.svelte index b2e1ba24..5f24b66e 100644 --- a/src/lib/components/tools/DHCPv6FQDN.svelte +++ b/src/lib/components/tools/DHCPv6FQDN.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { type FQDNConfig, type FQDNResult, @@ -92,10 +93,7 @@ }); </script> -<ToolContentContainer - title="DHCPv6 Client FQDN Option (RFC 4704)" - description="Configure the Client FQDN Option (Option 39) for DHCPv6, enabling dynamic DNS updates and hostname management for IPv6 clients." -> +<ToolContentContainer title={$t('tools/dhcpv6-fqdn.title')} description={$t('tools/dhcpv6-fqdn.subtitle')}> <ExamplesCard examples={FQDN_EXAMPLES} onSelect={loadExample} @@ -106,24 +104,29 @@ <div class="card input-card"> <div class="card-header"> - <h3>FQDN Configuration</h3> - <p class="help-text">Fully Qualified Domain Name for the DHCPv6 client</p> + <h3>{$t('tools/dhcpv6-fqdn.configuration.fqdnTitle')}</h3> + <p class="help-text">{$t('tools/dhcpv6-fqdn.configuration.fqdnHelpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="fqdn"> <Icon name="globe" size="sm" /> - Fully Qualified Domain Name (FQDN) + {$t('tools/dhcpv6-fqdn.configuration.fqdnLabel')} </label> - <input id="fqdn" type="text" bind:value={config.fqdn} placeholder="client.example.com" /> + <input + id="fqdn" + type="text" + bind:value={config.fqdn} + placeholder={$t('tools/dhcpv6-fqdn.configuration.fqdnPlaceholder')} + /> </div> </div> </div> <div class="card input-card"> <div class="card-header"> - <h3>DNS Update Flags</h3> - <p class="help-text">Control how DNS updates are performed</p> + <h3>{$t('tools/dhcpv6-fqdn.configuration.flagsTitle')}</h3> + <p class="help-text">{$t('tools/dhcpv6-fqdn.configuration.flagsHelpText')}</p> </div> <div class="card-content flags-content"> <div class="checkbox-group"> @@ -131,8 +134,8 @@ <label for="server-update"> <Icon name="server" size="sm" /> <div class="checkbox-text"> - <strong>Server Should Update DNS (S Flag)</strong> - <span class="help-text">Server will perform AAAA and PTR record updates</span> + <strong>{$t('tools/dhcpv6-fqdn.configuration.serverUpdate.label')}</strong> + <span class="help-text">{$t('tools/dhcpv6-fqdn.configuration.serverUpdate.help')}</span> </div> </label> </div> @@ -142,8 +145,8 @@ <label for="server-override"> <Icon name="shield" size="sm" /> <div class="checkbox-text"> - <strong>Server Override (O Flag)</strong> - <span class="help-text">Server can override client's preferences</span> + <strong>{$t('tools/dhcpv6-fqdn.configuration.serverOverride.label')}</strong> + <span class="help-text">{$t('tools/dhcpv6-fqdn.configuration.serverOverride.help')}</span> </div> </label> </div> @@ -153,8 +156,8 @@ <label for="client-update"> <Icon name="user" size="sm" /> <div class="checkbox-text"> - <strong>Client Should Update DNS (N Flag = 0)</strong> - <span class="help-text">Client will perform its own DNS updates</span> + <strong>{$t('tools/dhcpv6-fqdn.configuration.clientUpdate.label')}</strong> + <span class="help-text">{$t('tools/dhcpv6-fqdn.configuration.clientUpdate.help')}</span> </div> </label> </div> @@ -163,7 +166,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcpv6-fqdn.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -175,35 +178,50 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Option 39: Client FQDN</h3> + <h3>{$t('tools/dhcpv6-fqdn.results.title')}</h3> <div class="summary-card"> - <div><strong>FQDN:</strong> {result.fqdn}</div> - <div><strong>Total Length:</strong> {result.totalLength} bytes</div> + <div><strong>{$t('tools/dhcpv6-fqdn.results.fqdn')}</strong> {result.fqdn}</div> + <div> + <strong>{$t('tools/dhcpv6-fqdn.results.totalLength')}</strong> + {$t('tools/dhcpv6-fqdn.results.lengthBytes', { length: result.totalLength })} + </div> </div> <div class="flags-section"> - <h4>Flags Breakdown</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.flagsBreakdown')}</h4> <div class="flags-grid"> <div class="flag-item" class:active={result.flags.S}> <Icon name="server" size="sm" /> <div class="flag-content"> - <strong>S Flag</strong> - <span>{result.flags.S ? 'Set' : 'Not Set'}</span> + <strong>{$t('tools/dhcpv6-fqdn.results.sFlag')}</strong> + <span + >{result.flags.S + ? $t('tools/dhcpv6-fqdn.results.flagSet') + : $t('tools/dhcpv6-fqdn.results.flagNotSet')}</span + > </div> </div> <div class="flag-item" class:active={result.flags.O}> <Icon name="shield" size="sm" /> <div class="flag-content"> - <strong>O Flag</strong> - <span>{result.flags.O ? 'Set' : 'Not Set'}</span> + <strong>{$t('tools/dhcpv6-fqdn.results.oFlag')}</strong> + <span + >{result.flags.O + ? $t('tools/dhcpv6-fqdn.results.flagSet') + : $t('tools/dhcpv6-fqdn.results.flagNotSet')}</span + > </div> </div> <div class="flag-item" class:active={result.flags.N}> <Icon name="user" size="sm" /> <div class="flag-content"> - <strong>N Flag</strong> - <span>{result.flags.N ? 'Set' : 'Not Set'}</span> + <strong>{$t('tools/dhcpv6-fqdn.results.nFlag')}</strong> + <span + >{result.flags.N + ? $t('tools/dhcpv6-fqdn.results.flagSet') + : $t('tools/dhcpv6-fqdn.results.flagNotSet')}</span + > </div> </div> </div> @@ -219,7 +237,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Flags Byte</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.flagsByte')}</h4> <button type="button" class="copy-btn" @@ -227,7 +245,9 @@ onclick={() => clipboard.copy(result!.flags.flagsByte, 'flags')} > <Icon name={clipboard.isCopied('flags') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('flags') ? 'Copied' : 'Copy'} + {clipboard.isCopied('flags') + ? $t('tools/dhcpv6-fqdn.buttons.copied') + : $t('tools/dhcpv6-fqdn.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.flags.flagsByte}</pre> @@ -236,7 +256,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.hexEncoded')}</h4> <button type="button" class="copy-btn" @@ -244,7 +264,7 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') ? $t('tools/dhcpv6-fqdn.buttons.copied') : $t('tools/dhcpv6-fqdn.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> @@ -252,7 +272,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.wireFormat')}</h4> <button type="button" class="copy-btn" @@ -260,21 +280,21 @@ onclick={() => clipboard.copy(result!.wireFormat, 'wire')} > <Icon name={clipboard.isCopied('wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') ? $t('tools/dhcpv6-fqdn.buttons.copied') : $t('tools/dhcpv6-fqdn.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.wireFormat}</pre> </div> <div class="breakdown-section"> - <h4>Encoding Breakdown</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.encodingBreakdown')}</h4> <div class="breakdown-grid"> <div class="breakdown-item"> - <span class="breakdown-label">Flags:</span> + <span class="breakdown-label">{$t('tools/dhcpv6-fqdn.results.flags')}</span> <span class="breakdown-value">{result.breakdown.flags}</span> </div> <div class="breakdown-item"> - <span class="breakdown-label">FQDN:</span> + <span class="breakdown-label">{$t('tools/dhcpv6-fqdn.results.fqdnLabel')}</span> <span class="breakdown-value">{result.breakdown.fqdn}</span> </div> </div> @@ -283,11 +303,11 @@ {#if result.examples.keaDhcp6} <div class="card results"> - <h3>Configuration Example</h3> + <h3>{$t('tools/dhcpv6-fqdn.results.configExample')}</h3> <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv6 Configuration</h4> + <h4>{$t('tools/dhcpv6-fqdn.results.keaDhcpv6')}</h4> <button type="button" class="copy-btn" @@ -295,7 +315,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp6!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcpv6-fqdn.buttons.copied') + : $t('tools/dhcpv6-fqdn.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp6}</pre> @@ -304,25 +326,26 @@ {/if} <div class="card results info-card"> - <h3>About RFC 4704</h3> + <h3>{$t('tools/dhcpv6-fqdn.about.title')}</h3> <p> - RFC 4704 defines the Client FQDN Option for DHCPv6, enabling clients and servers to negotiate dynamic DNS - updates for IPv6 addresses. + {$t('tools/dhcpv6-fqdn.about.intro')} </p> <ul> <li> - <strong>S Flag (Bit 0):</strong> Server should perform DNS updates + <strong>S Flag (Bit 0):</strong> + {$t('tools/dhcpv6-fqdn.about.sFlagDescription')} </li> <li> - <strong>O Flag (Bit 1):</strong> Server can override client preferences + <strong>O Flag (Bit 1):</strong> + {$t('tools/dhcpv6-fqdn.about.oFlagDescription')} </li> <li> - <strong>N Flag (Bit 2):</strong> Client requests server to perform updates (client will NOT update) + <strong>N Flag (Bit 2):</strong> + {$t('tools/dhcpv6-fqdn.about.nFlagDescription')} </li> </ul> <p> - The FQDN is encoded using DNS wire format with length-prefixed labels, enabling automated hostname registration - in DNS for IPv6 networks. + {$t('tools/dhcpv6-fqdn.about.conclusion')} </p> </div> {/if} diff --git a/src/lib/components/tools/DKIMKeyGenerator.svelte b/src/lib/components/tools/DKIMKeyGenerator.svelte index 5c1dc1fa..f0cca3d6 100644 --- a/src/lib/components/tools/DKIMKeyGenerator.svelte +++ b/src/lib/components/tools/DKIMKeyGenerator.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; + import { t } from '$lib/stores/language'; interface DKIMKey { privateKey: string; @@ -152,26 +153,26 @@ showButtonSuccess('download-txt'); } - const examples = [ + const examples = $derived([ { - name: 'Standard Setup', + name: $t('tools/dkim-key-generator.examples.standard.name'), selector: 'default', domain: 'example.com', keySize: 2048, }, { - name: 'Monthly Rotation', + name: $t('tools/dkim-key-generator.examples.monthly.name'), selector: '202412', domain: 'mycompany.com', keySize: 2048, }, { - name: 'Service-Specific', + name: $t('tools/dkim-key-generator.examples.serviceSpecific.name'), selector: 'mailgun', domain: 'notifications.example.com', keySize: 1024, }, - ]; + ]); function loadExample(example: (typeof examples)[0]): void { selector = example.selector; @@ -183,8 +184,8 @@ <div class="card"> <div class="card-header"> - <h1>DKIM Key Generator</h1> - <p class="card-subtitle">Generate DKIM RSA keypairs with selectors and DNS TXT records for email authentication.</p> + <h1>{$t('tools/dkim-key-generator.title')}</h1> + <p class="card-subtitle">{$t('tools/dkim-key-generator.description')}</p> </div> <div class="grid-layout"> @@ -193,36 +194,42 @@ <div class="section-header"> <h3> <Icon name="settings" size="sm" /> - Configuration + {$t('tools/dkim-key-generator.config.title')} </h3> </div> <div class="config-grid"> <div class="input-group"> - <label - for="selector" - use:tooltip={"Unique identifier for this DKIM key (e.g., 'default', '202412', 'mailgun')"} - > - Selector: + <label for="selector" use:tooltip={$t('tools/dkim-key-generator.config.selectorTooltip')}> + {$t('tools/dkim-key-generator.config.selectorLabel')} </label> - <input id="selector" type="text" bind:value={selector} placeholder="default" /> + <input + id="selector" + type="text" + bind:value={selector} + placeholder={$t('tools/dkim-key-generator.config.selectorPlaceholder')} + /> </div> <div class="input-group"> - <label for="domain" use:tooltip={'Domain that will use this DKIM key for signing emails'}> Domain: </label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('tools/dkim-key-generator.config.domainTooltip')}> + {$t('tools/dkim-key-generator.config.domainLabel')} + </label> + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools/dkim-key-generator.config.domainPlaceholder')} + /> </div> <div class="input-group"> - <label - for="keySize" - use:tooltip={'RSA key size in bits. 2048-bit recommended for security, 1024-bit for compatibility'} - > - Key Size: + <label for="keySize" use:tooltip={$t('tools/dkim-key-generator.config.keySizeTooltip')}> + {$t('tools/dkim-key-generator.config.keySizeLabel')} </label> <select id="keySize" bind:value={keySize}> - <option value={1024}>1024-bit (Legacy)</option> - <option value={2048}>2048-bit (Recommended)</option> + <option value={1024}>{$t('tools/dkim-key-generator.config.keySizes.1024')}</option> + <option value={2048}>{$t('tools/dkim-key-generator.config.keySizes.2048')}</option> </select> </div> </div> @@ -234,7 +241,9 @@ disabled={isGenerating || !selector.trim() || !domain.trim()} > <Icon name={isGenerating ? 'loader' : 'key'} size="sm" /> - {isGenerating ? 'Generating...' : 'Generate DKIM Keys'} + {isGenerating + ? $t('tools/dkim-key-generator.config.generating') + : $t('tools/dkim-key-generator.config.generateButton')} </button> </div> @@ -243,32 +252,38 @@ <div class="section-header"> <h3> <Icon name="shield" size="sm" /> - Generated Keys + {$t('tools/dkim-key-generator.keys.title')} </h3> </div> <div class="key-item"> <div class="key-header"> - <h4>Private Key</h4> + <h4>{$t('tools/dkim-key-generator.keys.privateKey.title')}</h4> <div class="key-actions"> <button type="button" class="toggle-btn" onclick={() => (showPrivateKey = !showPrivateKey)} - use:tooltip={showPrivateKey ? 'Hide private key' : 'Show private key'} + use:tooltip={showPrivateKey + ? $t('tools/dkim-key-generator.keys.privateKey.hideTooltip') + : $t('tools/dkim-key-generator.keys.privateKey.showTooltip')} > <Icon name={showPrivateKey ? 'hide' : 'eye'} size="sm" /> - {showPrivateKey ? 'Hide' : 'Show'} + {showPrivateKey + ? $t('tools/dkim-key-generator.keys.privateKey.hideButton') + : $t('tools/dkim-key-generator.keys.privateKey.showButton')} </button> <button type="button" class="download-btn" class:success={buttonStates['download-private']} onclick={downloadPrivateKey} - use:tooltip={'Download private key as PEM file'} + use:tooltip={$t('tools/dkim-key-generator.keys.privateKey.downloadTooltip')} > <Icon name={buttonStates['download-private'] ? 'check' : 'download'} size="sm" /> - {buttonStates['download-private'] ? 'Downloaded!' : 'Download'} + {buttonStates['download-private'] + ? $t('tools/dkim-key-generator.keys.privateKey.downloaded') + : $t('tools/dkim-key-generator.keys.privateKey.downloadButton')} </button> </div> </div> @@ -282,39 +297,43 @@ {:else} <div class="key-hidden"> <Icon name="hide" size="sm" /> - Private key hidden for security + {$t('tools/dkim-key-generator.keys.privateKey.hidden')} </div> {/if} <div class="security-warning"> <Icon name="alert-triangle" size="sm" /> - Keep this private key secure. Never share it publicly or store it in version control. + {$t('tools/dkim-key-generator.keys.privateKey.warning')} </div> </div> <div class="key-item"> <div class="key-header"> - <h4>Public Key</h4> + <h4>{$t('tools/dkim-key-generator.keys.publicKey.title')}</h4> <div class="key-actions"> <button type="button" class="copy-btn" class:success={buttonStates['copy-public']} onclick={() => copyToClipboard(generatedKey?.publicKey || '', 'copy-public')} - use:tooltip={'Copy public key to clipboard'} + use:tooltip={$t('tools/dkim-key-generator.keys.publicKey.copyTooltip')} > <Icon name={buttonStates['copy-public'] ? 'check' : 'copy'} size="sm" /> - {buttonStates['copy-public'] ? 'Copied!' : 'Copy'} + {buttonStates['copy-public'] + ? $t('tools/dkim-key-generator.keys.publicKey.copied') + : $t('tools/dkim-key-generator.keys.publicKey.copyButton')} </button> <button type="button" class="download-btn" class:success={buttonStates['download-public']} onclick={downloadPublicKey} - use:tooltip={'Download public key as PEM file'} + use:tooltip={$t('tools/dkim-key-generator.keys.publicKey.downloadTooltip')} > <Icon name={buttonStates['download-public'] ? 'check' : 'download'} size="sm" /> - {buttonStates['download-public'] ? 'Downloaded!' : 'Download'} + {buttonStates['download-public'] + ? $t('tools/dkim-key-generator.keys.publicKey.downloaded') + : $t('tools/dkim-key-generator.keys.publicKey.downloadButton')} </button> </div> </div> @@ -333,40 +352,44 @@ <div class="results-section"> <div class="dns-section"> <div class="section-header"> - <h3>DNS TXT Record</h3> + <h3>{$t('tools/dkim-key-generator.dns.title')}</h3> <div class="actions"> <button type="button" class="copy-btn" class:success={buttonStates['copy-txt']} onclick={() => copyToClipboard(txtRecord, 'copy-txt')} - use:tooltip={'Copy DNS TXT record to clipboard'} + use:tooltip={$t('tools/dkim-key-generator.dns.copyTooltip')} > <Icon name={buttonStates['copy-txt'] ? 'check' : 'copy'} size="sm" /> - {buttonStates['copy-txt'] ? 'Copied!' : 'Copy'} + {buttonStates['copy-txt'] + ? $t('tools/dkim-key-generator.dns.copied') + : $t('tools/dkim-key-generator.dns.copyButton')} </button> <button type="button" class="export-btn" class:success={buttonStates['download-txt']} onclick={downloadTXTRecord} - use:tooltip={'Download DNS record as text file'} + use:tooltip={$t('tools/dkim-key-generator.dns.exportTooltip')} > <Icon name={buttonStates['download-txt'] ? 'check' : 'download'} size="sm" /> - {buttonStates['download-txt'] ? 'Downloaded!' : 'Export'} + {buttonStates['download-txt'] + ? $t('tools/dkim-key-generator.dns.exported') + : $t('tools/dkim-key-generator.dns.exportButton')} </button> </div> </div> <div class="record-output"> - <h4>Zone File Format:</h4> + <h4>{$t('tools/dkim-key-generator.dns.zoneFileFormat')}</h4> <div class="code-block"> <code>{txtRecord}</code> </div> </div> <div class="record-output"> - <h4>DKIM Record Value:</h4> + <h4>{$t('tools/dkim-key-generator.dns.dkimRecordValue')}</h4> <div class="code-block"> <code>{dkimRecord}</code> </div> @@ -377,35 +400,35 @@ <div class="section-header"> <h3> <Icon name="info" size="sm" /> - Implementation Notes + {$t('tools/dkim-key-generator.implementation.title')} </h3> </div> <div class="info-grid"> <div class="info-item"> - <strong>Selector:</strong> + <strong>{$t('tools/dkim-key-generator.implementation.selector')}</strong> {generatedKey.selector} </div> <div class="info-item"> - <strong>Domain:</strong> + <strong>{$t('tools/dkim-key-generator.implementation.domain')}</strong> {domain} </div> <div class="info-item"> - <strong>Key Size:</strong> - {generatedKey.keySize}-bit RSA + <strong>{$t('tools/dkim-key-generator.implementation.keySize')}</strong> + {$t('tools/dkim-key-generator.implementation.keySizeBits', { size: generatedKey.keySize })} </div> <div class="info-item"> - <strong>Algorithm:</strong> RSA-SHA256 (rsa-sha256) + <strong>{$t('tools/dkim-key-generator.implementation.algorithm')}</strong> + {$t('tools/dkim-key-generator.implementation.algorithmValue')} </div> </div> <div class="implementation-steps"> - <h4>Next Steps:</h4> + <h4>{$t('tools/dkim-key-generator.implementation.nextStepsTitle')}</h4> <ol> - <li>Add the DNS TXT record to your domain's DNS configuration</li> - <li>Configure your mail server with the private key</li> - <li>Set up DKIM signing for outgoing emails</li> - <li>Test DKIM signatures using online validation tools</li> + {#each $t('tools/dkim-key-generator.implementation.steps') as step, index (index)} + <li>{step}</li> + {/each} </ol> </div> </div> @@ -417,7 +440,7 @@ <details class="examples-toggle"> <summary> <Icon name="lightbulb" size="sm" /> - Example Configurations + {$t('tools/dkim-key-generator.examples.title')} </summary> <div class="examples-grid"> {#each examples as example, exIdx (`${example.name}-${exIdx}`)} @@ -426,9 +449,12 @@ <strong>{example.name}</strong> </div> <div class="example-details"> - <div>Selector: <code>{example.selector}</code></div> - <div>Domain: <code>{example.domain}</code></div> - <div>Key Size: <code>{example.keySize}-bit</code></div> + <div>{$t('tools/dkim-key-generator.examples.selectorLabel')} <code>{example.selector}</code></div> + <div>{$t('tools/dkim-key-generator.examples.domainLabel')} <code>{example.domain}</code></div> + <div> + {$t('tools/dkim-key-generator.examples.keySizeLabel')} + <code>{$t('tools/dkim-key-generator.examples.keySizeBits', { size: example.keySize })}</code> + </div> </div> </button> {/each} diff --git a/src/lib/components/tools/DMARCBuilder.svelte b/src/lib/components/tools/DMARCBuilder.svelte index c9d639a8..c718a965 100644 --- a/src/lib/components/tools/DMARCBuilder.svelte +++ b/src/lib/components/tools/DMARCBuilder.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; + import { t } from '$lib/stores/language'; interface DMARCPolicy { version: 'DMARC1'; @@ -35,23 +36,23 @@ // Button success states let buttonStates = $state<Record<string, boolean>>({}); - const policyDescriptions = { - none: 'Monitor only - no action taken on failed emails', - quarantine: 'Failed emails sent to spam/junk folder', - reject: 'Failed emails rejected at SMTP level', - }; + const policyDescriptions = $derived({ + none: $t('tools/dmarc-builder.policy.types.none.description'), + quarantine: $t('tools/dmarc-builder.policy.types.quarantine.description'), + reject: $t('tools/dmarc-builder.policy.types.reject.description'), + }); - const alignmentDescriptions = { - r: 'Relaxed - domain and subdomains match', - s: 'Strict - exact domain match only', - }; + const alignmentDescriptions = $derived({ + r: $t('tools/dmarc-builder.advanced.alignment.relaxed.description'), + s: $t('tools/dmarc-builder.advanced.alignment.strict.description'), + }); - const failureOptionDescriptions = { - '0': 'Generate reports if both SPF and DKIM fail', - '1': 'Generate reports if either SPF or DKIM fail', - d: 'Generate reports if DKIM fails', - s: 'Generate reports if SPF fails', - }; + const failureOptionDescriptions = $derived({ + '0': $t('tools/dmarc-builder.advanced.failureOptions.options.0'), + '1': $t('tools/dmarc-builder.advanced.failureOptions.options.1'), + d: $t('tools/dmarc-builder.advanced.failureOptions.options.d'), + s: $t('tools/dmarc-builder.advanced.failureOptions.options.s'), + }); const dmarcRecord = $derived.by(() => { let record = `v=${policy.version}; p=${policy.policy}`; @@ -101,40 +102,40 @@ // Check domain format if (!domain.trim()) { - errors.push('Domain is required'); + errors.push($t('tools/dmarc-builder.validation.errors.domainRequired')); } else if (!domain.includes('.')) { - warnings.push('Domain should include TLD (e.g., .com, .org)'); + warnings.push($t('tools/dmarc-builder.validation.warnings.domainNoTLD')); } // Policy progression warnings if (policy.policy === 'reject' && !policy.reportingURI) { - warnings.push('Consider adding reporting URI before using reject policy'); + warnings.push($t('tools/dmarc-builder.validation.warnings.rejectNeedsReporting')); } if (policy.policy === 'none' && policy.percentage < 100) { - warnings.push('Percentage should be 100% for monitoring-only policy'); + warnings.push($t('tools/dmarc-builder.validation.warnings.noneWithPercentage')); } // Alignment warnings if (policy.dkimAlignment === 's' && policy.spfAlignment === 's') { - warnings.push('Strict alignment for both SPF and DKIM may cause legitimate emails to fail'); + warnings.push($t('tools/dmarc-builder.validation.warnings.strictAlignment')); } // Reporting warnings if (policy.reportingURI && !policy.reportingURI.includes('@')) { - errors.push('Reporting URI must be a valid email address'); + errors.push($t('tools/dmarc-builder.validation.errors.reportingEmail')); } if (policy.forensicURI && !policy.forensicURI.includes('@')) { - errors.push('Forensic URI must be a valid email address'); + errors.push($t('tools/dmarc-builder.validation.errors.forensicEmail')); } // Record length check const recordLength = dmarcRecord.length; if (recordLength > 255) { - errors.push(`DMARC record too long (${recordLength} chars). DNS TXT limit is 255.`); + errors.push($t('tools/dmarc-builder.validation.errors.recordTooLong', { length: recordLength })); } else if (recordLength > 200) { - warnings.push(`DMARC record is long (${recordLength} chars). Consider shortening.`); + warnings.push($t('tools/dmarc-builder.validation.warnings.recordLong', { length: recordLength })); } return { @@ -179,10 +180,10 @@ } } - const examplePolicies = [ + const examplePolicies = $derived([ { - name: 'Monitor Only', - description: 'Start monitoring without affecting email delivery', + name: $t('tools/dmarc-builder.examples.monitor.name'), + description: $t('tools/dmarc-builder.examples.monitor.description'), domain: 'example.com', config: { policy: 'none' as const, @@ -194,8 +195,8 @@ }, }, { - name: 'Quarantine Phase', - description: 'Move suspicious emails to spam folder', + name: $t('tools/dmarc-builder.examples.quarantine.name'), + description: $t('tools/dmarc-builder.examples.quarantine.description'), domain: 'mycompany.com', config: { policy: 'quarantine' as const, @@ -207,8 +208,8 @@ }, }, { - name: 'Full Protection', - description: 'Reject all failing emails with forensics', + name: $t('tools/dmarc-builder.examples.fullProtection.name'), + description: $t('tools/dmarc-builder.examples.fullProtection.description'), domain: 'secure.example.com', config: { policy: 'reject' as const, @@ -221,7 +222,7 @@ failureOptions: ['1' as const], }, }, - ]; + ]); function loadExample(example: (typeof examplePolicies)[0]): void { domain = example.domain; @@ -233,22 +234,14 @@ selectedExample = example.name; } - const deploymentSteps = [ - 'Start with p=none to monitor current email authentication status', - 'Analyze DMARC reports to identify legitimate vs malicious sources', - 'Configure SPF and DKIM for all legitimate sending sources', - 'Gradually increase to p=quarantine with low percentage (pct=25)', - 'Monitor for false positives and adjust alignment if needed', - 'Increase percentage gradually (50%, 75%, 100%)', - 'Finally move to p=reject when confident in configuration', - ]; + const deploymentSteps = $derived($t('tools/dmarc-builder.deployment.steps')); </script> <div class="card"> <div class="card-header"> - <h1>DMARC Policy Builder</h1> + <h1>{$t('tools/dmarc-builder.title')}</h1> <p class="card-subtitle"> - Create DMARC policies with alignment options, reporting addresses, and failure handling configuration. + {$t('tools/dmarc-builder.description')} </p> </div> @@ -258,13 +251,20 @@ <div class="section-header"> <h3> <Icon name="globe" size="sm" /> - Domain Configuration + {$t('tools/dmarc-builder.domain.title')} </h3> </div> <div class="input-group"> - <label for="domain" use:tooltip={'Domain that this DMARC policy will protect'}> Domain: </label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('tools/dmarc-builder.domain.tooltip')}> + {$t('tools/dmarc-builder.domain.label')} + </label> + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools/dmarc-builder.domain.placeholder')} + /> </div> </div> @@ -272,19 +272,19 @@ <div class="section-header"> <h3> <Icon name="shield" size="sm" /> - Policy Configuration + {$t('tools/dmarc-builder.policy.title')} </h3> </div> <div class="policy-grid"> <div class="input-group"> - <label for="policy" use:tooltip={'Action to take for emails that fail DMARC authentication'}> - Policy (p): + <label for="policy" use:tooltip={$t('tools/dmarc-builder.policy.mainTooltip')}> + {$t('tools/dmarc-builder.policy.mainLabel')} </label> <select id="policy" bind:value={policy.policy}> - <option value="none">none - Monitor only</option> - <option value="quarantine">quarantine - Send to spam</option> - <option value="reject">reject - Block email</option> + <option value="none">{$t('tools/dmarc-builder.policy.types.none.label')}</option> + <option value="quarantine">{$t('tools/dmarc-builder.policy.types.quarantine.label')}</option> + <option value="reject">{$t('tools/dmarc-builder.policy.types.reject.label')}</option> </select> <div class="policy-description"> {policyDescriptions[policy.policy]} @@ -292,11 +292,8 @@ </div> <div class="input-group"> - <label - for="percentage" - use:tooltip={'Percentage of failing emails to apply policy to (useful for gradual deployment)'} - > - Percentage (pct): + <label for="percentage" use:tooltip={$t('tools/dmarc-builder.policy.percentageTooltip')}> + {$t('tools/dmarc-builder.policy.percentageLabel')} </label> <div class="percentage-input"> <input id="percentage" type="range" bind:value={policy.percentage} min="0" max="100" step="5" /> @@ -308,33 +305,33 @@ <details class="advanced-toggle" bind:open={showAdvanced}> <summary> <Icon name="settings" size="sm" /> - Advanced Options + {$t('tools/dmarc-builder.advanced.title')} </summary> <div class="advanced-grid"> <div class="input-group"> - <label for="subdomainPolicy" use:tooltip={'Policy for subdomains (inherits main policy if not set)'}> - Subdomain Policy (sp): + <label for="subdomainPolicy" use:tooltip={$t('tools/dmarc-builder.policy.subdomainTooltip')}> + {$t('tools/dmarc-builder.policy.subdomainLabel')} </label> <select id="subdomainPolicy" bind:value={policy.subdomainPolicy}> - <option value={undefined}>Inherit from main policy</option> - <option value="none">none - Monitor only</option> - <option value="quarantine">quarantine - Send to spam</option> - <option value="reject">reject - Block email</option> + <option value={undefined}>{$t('tools/dmarc-builder.policy.subdomainInherit')}</option> + <option value="none">{$t('tools/dmarc-builder.policy.types.none.label')}</option> + <option value="quarantine">{$t('tools/dmarc-builder.policy.types.quarantine.label')}</option> + <option value="reject">{$t('tools/dmarc-builder.policy.types.reject.label')}</option> </select> </div> <div class="alignment-section"> - <h4>Authentication Alignment</h4> + <h4>{$t('tools/dmarc-builder.advanced.alignment.title')}</h4> <div class="alignment-grid"> <div class="input-group"> - <label for="dkimAlignment" use:tooltip={'How strictly DKIM signature domain must match From domain'}> - DKIM Alignment (adkim): + <label for="dkimAlignment" use:tooltip={$t('tools/dmarc-builder.advanced.alignment.dkimTooltip')}> + {$t('tools/dmarc-builder.advanced.alignment.dkimLabel')} </label> <select id="dkimAlignment" bind:value={policy.dkimAlignment}> - <option value="r">r - Relaxed</option> - <option value="s">s - Strict</option> + <option value="r">{$t('tools/dmarc-builder.advanced.alignment.relaxed.label')}</option> + <option value="s">{$t('tools/dmarc-builder.advanced.alignment.strict.label')}</option> </select> <div class="alignment-description"> {alignmentDescriptions[policy.dkimAlignment]} @@ -342,12 +339,12 @@ </div> <div class="input-group"> - <label for="spfAlignment" use:tooltip={'How strictly SPF domain must match From domain'}> - SPF Alignment (aspf): + <label for="spfAlignment" use:tooltip={$t('tools/dmarc-builder.advanced.alignment.spfTooltip')}> + {$t('tools/dmarc-builder.advanced.alignment.spfLabel')} </label> <select id="spfAlignment" bind:value={policy.spfAlignment}> - <option value="r">r - Relaxed</option> - <option value="s">s - Strict</option> + <option value="r">{$t('tools/dmarc-builder.advanced.alignment.relaxed.label')}</option> + <option value="s">{$t('tools/dmarc-builder.advanced.alignment.strict.label')}</option> </select> <div class="alignment-description"> {alignmentDescriptions[policy.spfAlignment]} @@ -357,51 +354,53 @@ </div> <div class="reporting-section"> - <h4>Reporting Configuration</h4> + <h4>{$t('tools/dmarc-builder.advanced.reporting.title')}</h4> <div class="reporting-grid"> <div class="input-group"> - <label for="reportingURI" use:tooltip={'Email address to receive aggregate DMARC reports'}> - Reporting Email (rua): + <label for="reportingURI" use:tooltip={$t('tools/dmarc-builder.advanced.reporting.aggregateTooltip')}> + {$t('tools/dmarc-builder.advanced.reporting.aggregateLabel')} </label> <input id="reportingURI" type="email" bind:value={policy.reportingURI} - placeholder="dmarc@example.com" + placeholder={$t('tools/dmarc-builder.advanced.reporting.aggregatePlaceholder')} /> </div> <div class="input-group"> - <label - for="forensicURI" - use:tooltip={'Email address to receive forensic failure reports (detailed samples)'} - > - Forensic Email (ruf): + <label for="forensicURI" use:tooltip={$t('tools/dmarc-builder.advanced.reporting.forensicTooltip')}> + {$t('tools/dmarc-builder.advanced.reporting.forensicLabel')} </label> <input id="forensicURI" type="email" bind:value={policy.forensicURI} - placeholder="forensic@example.com" + placeholder={$t('tools/dmarc-builder.advanced.reporting.forensicPlaceholder')} /> </div> <div class="input-group"> - <label for="reportInterval" use:tooltip={'How often aggregate reports are sent (in seconds)'}> - Report Interval (ri): + <label + for="reportInterval" + use:tooltip={$t('tools/dmarc-builder.advanced.reporting.intervalTooltip')} + > + {$t('tools/dmarc-builder.advanced.reporting.intervalLabel')} </label> <select id="reportInterval" bind:value={policy.reportInterval}> - <option value={3600}>1 hour</option> - <option value={86400}>24 hours (daily)</option> - <option value={604800}>7 days (weekly)</option> + <option value={3600}>{$t('tools/dmarc-builder.advanced.reporting.intervals.hourly')}</option> + <option value={86400}>{$t('tools/dmarc-builder.advanced.reporting.intervals.daily')}</option> + <option value={604800}>{$t('tools/dmarc-builder.advanced.reporting.intervals.weekly')}</option> </select> </div> </div> </div> <div class="failure-options-section"> - <h4 use:tooltip={'When to generate forensic failure reports'}>Failure Reporting Options (fo):</h4> + <h4 use:tooltip={$t('tools/dmarc-builder.advanced.failureOptions.tooltip')}> + {$t('tools/dmarc-builder.advanced.failureOptions.title')} + </h4> <div class="failure-options"> {#each Object.entries(failureOptionDescriptions) as [option, description] (option)} @@ -425,27 +424,31 @@ <div class="results-section"> <div class="record-section"> <div class="section-header"> - <h3>Generated DMARC Record</h3> + <h3>{$t('tools/dmarc-builder.output.title')}</h3> <div class="actions"> <button type="button" class="copy-btn" class:success={buttonStates['copy-dmarc']} onclick={() => copyToClipboard(dmarcRecord, 'copy-dmarc')} - use:tooltip={'Copy DMARC record to clipboard'} + use:tooltip={$t('tools/dmarc-builder.output.copyTooltip')} > <Icon name={buttonStates['copy-dmarc'] ? 'check' : 'copy'} size="sm" /> - {buttonStates['copy-dmarc'] ? 'Copied!' : 'Copy'} + {buttonStates['copy-dmarc'] + ? $t('tools/dmarc-builder.output.copied') + : $t('tools/dmarc-builder.output.copyButton')} </button> <button type="button" class="export-btn" class:success={buttonStates['export-zone']} onclick={exportAsZoneFile} - use:tooltip={'Download as zone file'} + use:tooltip={$t('tools/dmarc-builder.output.exportTooltip')} > <Icon name={buttonStates['export-zone'] ? 'check' : 'download'} size="sm" /> - {buttonStates['export-zone'] ? 'Downloaded!' : 'Export'} + {buttonStates['export-zone'] + ? $t('tools/dmarc-builder.output.downloaded') + : $t('tools/dmarc-builder.output.exportButton')} </button> </div> </div> @@ -457,7 +460,7 @@ </div> <div class="zone-file-output"> - <h4>DNS TXT Record:</h4> + <h4>{$t('tools/dmarc-builder.output.txtRecordLabel')}</h4> <div class="code-block"> <code>{txtRecord}</code> </div> @@ -468,13 +471,13 @@ <div class="section-header"> <h3> <Icon name="bar-chart" size="sm" /> - Policy Validation + {$t('tools/dmarc-builder.validation.title')} </h3> </div> <div class="validation-stats"> <div class="stat-item"> - <span class="stat-label">Record Length:</span> + <span class="stat-label">{$t('tools/dmarc-builder.validation.recordLengthLabel')}</span> <span class="stat-value" class:warning={validation.recordLength > 200} @@ -484,9 +487,11 @@ </span> </div> <div class="stat-item"> - <span class="stat-label">Status:</span> + <span class="stat-label">{$t('tools/dmarc-builder.validation.statusLabel')}</span> <span class="stat-value" class:success={validation.isValid} class:error={!validation.isValid}> - {validation.isValid ? 'Valid' : 'Invalid'} + {validation.isValid + ? $t('tools/dmarc-builder.validation.valid') + : $t('tools/dmarc-builder.validation.invalid')} </span> </div> </div> @@ -516,7 +521,7 @@ {#if validation.isValid && validation.errors.length === 0 && validation.warnings.length === 0} <div class="validation-messages success"> <Icon name="check-circle" size="sm" /> - <div class="message">DMARC policy is valid and ready to deploy!</div> + <div class="message">{$t('tools/dmarc-builder.validation.success')}</div> </div> {/if} </div> @@ -525,7 +530,7 @@ <div class="section-header"> <h3> <Icon name="info" size="sm" /> - Deployment Guide + {$t('tools/dmarc-builder.deployment.title')} </h3> </div> @@ -550,7 +555,7 @@ <details class="examples-toggle"> <summary> <Icon name="lightbulb" size="sm" /> - Example Policies + {$t('tools/dmarc-builder.examples.title')} </summary> <div class="examples-grid"> {#each examplePolicies as example (example.name)} @@ -565,10 +570,10 @@ </div> <p class="example-description">{example.description}</p> <div class="example-config"> - <div>Policy: <code>{example.config.policy}</code></div> - <div>Percentage: <code>{example.config.percentage}%</code></div> + <div>{$t('tools/dmarc-builder.examples.policyLabel')} <code>{example.config.policy}</code></div> + <div>{$t('tools/dmarc-builder.examples.percentageLabel')} <code>{example.config.percentage}%</code></div> {#if example.config.reportingURI} - <div>Reports: <code>{example.config.reportingURI}</code></div> + <div>{$t('tools/dmarc-builder.examples.reportsLabel')} <code>{example.config.reportingURI}</code></div> {/if} </div> </button> diff --git a/src/lib/components/tools/DNSAAAAABulk.svelte b/src/lib/components/tools/DNSAAAAABulk.svelte index 31f62fe7..15b4938b 100644 --- a/src/lib/components/tools/DNSAAAAABulk.svelte +++ b/src/lib/components/tools/DNSAAAAABulk.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { tooltip } from '$lib/actions/tooltip'; import Icon from '$lib/components/global/Icon.svelte'; + import { t } from '$lib/stores/language'; let hostnameInput = $state(''); let ipInput = $state(''); @@ -11,23 +12,23 @@ let zoneFileContent = $state(''); let showExamples = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Web Servers', + label: $t('tools/dns-aaaa-bulk.examples.webServers'), hostnames: 'www\napi\ncdn\nstatic', ips: '192.168.1.10\n192.168.1.11\n192.168.1.12\n192.168.1.13', }, { - label: 'Mail Servers', + label: $t('tools/dns-aaaa-bulk.examples.mailServers'), hostnames: 'mail\nimap\nsmtp\npop3', ips: '10.0.1.20\n10.0.1.21\n10.0.1.22\n10.0.1.23', }, { - label: 'IPv6 Services', + label: $t('tools/dns-aaaa-bulk.examples.ipv6Services'), hostnames: 'web6\napi6\nmail6', ips: '2001:db8::1\n2001:db8::2\n2001:db8::3', }, - ]; + ]); function isIPv4(ip: string): boolean { const parts = ip.split('.'); @@ -160,41 +161,41 @@ $ORIGIN ${zoneName}. <div class="card"> <div class="card-header"> - <h1>A/AAAA Bulk Generator</h1> + <h1>{$t('tools/dns-aaaa-bulk.title')}</h1> <p class="card-subtitle"> - Bulk create A and AAAA record sets from hostname and IP lists with TTL controls and zone file generation. + {$t('tools/dns-aaaa-bulk.description')} </p> </div> <div class="grid-layout"> <div class="input-section"> <div class="input-group"> - <label for="hostnames" use:tooltip={'Enter hostnames, one per line'}> + <label for="hostnames" use:tooltip={$t('tools/dns-aaaa-bulk.input.hostnames.tooltip')}> <Icon name="server" size="sm" /> - Hostnames + {$t('tools/dns-aaaa-bulk.input.hostnames.label')} </label> - <textarea id="hostnames" bind:value={hostnameInput} placeholder="www api mail ftp" rows="8" + <textarea + id="hostnames" + bind:value={hostnameInput} + placeholder={$t('tools/dns-aaaa-bulk.input.hostnames.placeholder')} + rows="8" ></textarea> </div> <div class="input-group"> - <label for="ips" use:tooltip={'Enter IP addresses (IPv4 or IPv6), one per line'}> + <label for="ips" use:tooltip={$t('tools/dns-aaaa-bulk.input.ips.tooltip')}> <Icon name="networking" size="sm" /> - IP Addresses + {$t('tools/dns-aaaa-bulk.input.ips.label')} </label> - <textarea - id="ips" - bind:value={ipInput} - placeholder="192.168.1.10 192.168.1.11 2001:db8::1 2001:db8::2" - rows="8" + <textarea id="ips" bind:value={ipInput} placeholder={$t('tools/dns-aaaa-bulk.input.ips.placeholder')} rows="8" ></textarea> </div> <div class="controls-row"> <div class="input-group"> - <label for="ttl" use:tooltip={'Time To Live in seconds'}> + <label for="ttl" use:tooltip={$t('tools/dns-aaaa-bulk.input.ttl.tooltip')}> <Icon name="clock" size="sm" /> - TTL (seconds) + {$t('tools/dns-aaaa-bulk.input.ttl.label')} </label> <input type="number" id="ttl" bind:value={ttl} min="60" max="86400" /> </div> @@ -202,17 +203,22 @@ $ORIGIN ${zoneName}. <label class="checkbox-option"> <input type="checkbox" bind:checked={generateZoneFile} /> <span class="checkmark"></span> - Generate zone file + {$t('tools/dns-aaaa-bulk.input.generateZoneFile')} </label> </div> {#if generateZoneFile} <div class="input-group"> - <label for="zoneName" use:tooltip={'Domain name for the zone file'}> + <label for="zoneName" use:tooltip={$t('tools/dns-aaaa-bulk.input.zoneName.tooltip')}> <Icon name="globe" size="sm" /> - Zone Name + {$t('tools/dns-aaaa-bulk.input.zoneName.label')} </label> - <input type="text" id="zoneName" bind:value={zoneName} placeholder="example.com" /> + <input + type="text" + id="zoneName" + bind:value={zoneName} + placeholder={$t('tools/dns-aaaa-bulk.input.zoneName.placeholder')} + /> </div> {/if} </div> @@ -221,13 +227,13 @@ $ORIGIN ${zoneName}. <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Quick Examples + {$t('tools/dns-aaaa-bulk.examples.title')} </summary> <div class="examples-grid"> {#each examples as example (example.label)} <button class="example-card" onclick={() => loadExample(example)}> <h4>{example.label}</h4> - <p>{example.hostnames.split('\n').length} hosts</p> + <p>{$t('tools/dns-aaaa-bulk.examples.hostsCount', { count: example.hostnames.split('\n').length })}</p> </button> {/each} </div> @@ -238,18 +244,18 @@ $ORIGIN ${zoneName}. {#if results.length > 0} <div class="results-section"> <div class="results-header"> - <h2>Generated Records</h2> + <h2>{$t('tools/dns-aaaa-bulk.results.title')}</h2> <div class="export-buttons"> <button onclick={() => copyToClipboard(results.map((r) => `${r.name} ${r.ttl} IN ${r.type} ${r.value}`).join('\n'))} > <Icon name="copy" size="sm" /> - Copy Records + {$t('tools/dns-aaaa-bulk.results.copyRecordsButton')} </button> {#if generateZoneFile && zoneFileContent} <button onclick={downloadZoneFile}> <Icon name="download" size="sm" /> - Download Zone + {$t('tools/dns-aaaa-bulk.results.downloadZoneButton')} </button> {/if} </div> @@ -257,10 +263,10 @@ $ORIGIN ${zoneName}. <div class="records-table"> <div class="table-header"> - <div>Name</div> - <div>TTL</div> - <div>Type</div> - <div>Value</div> + <div>{$t('tools/dns-aaaa-bulk.results.tableHeaders.name')}</div> + <div>{$t('tools/dns-aaaa-bulk.results.tableHeaders.ttl')}</div> + <div>{$t('tools/dns-aaaa-bulk.results.tableHeaders.type')}</div> + <div>{$t('tools/dns-aaaa-bulk.results.tableHeaders.value')}</div> </div> {#each results as record, index (index)} <div class="table-row" class:invalid={record.type === 'INVALID'}> @@ -284,10 +290,10 @@ $ORIGIN ${zoneName}. {#if generateZoneFile && zoneFileContent} <div class="zone-file-section"> <div class="zone-file-header"> - <h3>Zone File Preview</h3> + <h3>{$t('tools/dns-aaaa-bulk.zoneFile.title')}</h3> <button onclick={() => copyToClipboard(zoneFileContent)}> <Icon name="copy" size="sm" /> - Copy Zone File + {$t('tools/dns-aaaa-bulk.zoneFile.copyButton')} </button> </div> <pre class="zone-file-content">{zoneFileContent}</pre> diff --git a/src/lib/components/tools/DNSCNAMEBuilder.svelte b/src/lib/components/tools/DNSCNAMEBuilder.svelte index 79181015..bd46b3d2 100644 --- a/src/lib/components/tools/DNSCNAMEBuilder.svelte +++ b/src/lib/components/tools/DNSCNAMEBuilder.svelte @@ -2,6 +2,7 @@ import { tooltip } from '$lib/actions/tooltip'; import Icon from '$lib/components/global/Icon.svelte'; import { SvelteSet } from 'svelte/reactivity'; + import { t } from '$lib/stores/language'; let aliasInput = $state(''); let targetInput = $state(''); @@ -17,23 +18,23 @@ >([]); let showExamples = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Web Aliases', + label: $t('tools/dns-cname-builder.examples.webAliases.label'), aliases: 'www\nblog\nshop\napi', targets: 'server1.example.com.\nwordpress.hosting.com.\necommerce.platform.com.\napi-gateway.example.com.', }, { - label: 'Service Redirects', + label: $t('tools/dns-cname-builder.examples.serviceRedirects.label'), aliases: 'mail\nftp\nvpn', targets: 'mailserver.example.com.\nftpserver.example.com.\nvpngateway.example.com.', }, { - label: 'CDN Configuration', + label: $t('tools/dns-cname-builder.examples.cdnConfiguration.label'), aliases: 'cdn\nstatic\nassets\nimages', targets: 'cdn.cloudflare.com.\nstatic.fastly.com.\nassets.cloudfront.net.\nimg.amazonaws.com.', }, - ]; + ]); function isValidHostname(hostname: string): boolean { if (!hostname || hostname.length > 253) return false; @@ -170,17 +171,25 @@ function getStatusInfo(status: (typeof results)[0]['status']) { switch (status) { case 'valid': - return { icon: 'check-circle', class: 'success', text: 'Valid' }; + return { icon: 'check-circle', class: 'success', text: $t('tools/dns-cname-builder.validation.status.valid') }; case 'loop': - return { icon: 'alert-triangle', class: 'error', text: 'Loop Detected' }; + return { icon: 'alert-triangle', class: 'error', text: $t('tools/dns-cname-builder.validation.status.loop') }; case 'self-target': - return { icon: 'alert-triangle', class: 'error', text: 'Self Target' }; + return { + icon: 'alert-triangle', + class: 'error', + text: $t('tools/dns-cname-builder.validation.status.selfTarget'), + }; case 'invalid-format': - return { icon: 'x-circle', class: 'error', text: 'Invalid Format' }; + return { + icon: 'x-circle', + class: 'error', + text: $t('tools/dns-cname-builder.validation.status.invalidFormat'), + }; case 'missing-dot': - return { icon: 'info', class: 'warning', text: 'Missing FQDN Dot' }; + return { icon: 'info', class: 'warning', text: $t('tools/dns-cname-builder.validation.status.missingDot') }; default: - return { icon: 'help-circle', class: 'info', text: 'Unknown' }; + return { icon: 'help-circle', class: 'info', text: $t('tools/dns-cname-builder.validation.status.unknown') }; } } @@ -191,8 +200,8 @@ <div class="card"> <div class="card-header"> - <h1>CNAME Builder</h1> - <p class="card-subtitle">Build valid CNAME records with loop detection, self-target checks, and FQDN validation.</p> + <h1>{$t('tools/dns-cname-builder.title')}</h1> + <p class="card-subtitle">{$t('tools/dns-cname-builder.description')}</p> </div> <div class="grid-layout"> @@ -201,55 +210,69 @@ <label class="checkbox-option"> <input type="checkbox" bind:checked={generateMultiple} /> <span class="checkmark"></span> - Bulk mode (multiple records) + {$t('tools/dns-cname-builder.mode.bulkMode')} </label> </div> {#if generateMultiple} <div class="input-group"> - <label for="aliases" use:tooltip={'Enter alias names, one per line'}> + <label for="aliases" use:tooltip={$t('tools/dns-cname-builder.input.aliases.tooltip')}> <Icon name="alias" size="sm" /> - Alias Names + {$t('tools/dns-cname-builder.input.aliases.label')} </label> - <textarea id="aliases" bind:value={aliasInput} placeholder="www blog mail ftp" rows="6" + <textarea + id="aliases" + bind:value={aliasInput} + placeholder={$t('tools/dns-cname-builder.input.aliases.placeholder')} + rows="6" ></textarea> </div> <div class="input-group"> - <label for="targets" use:tooltip={'Enter target FQDNs, one per line. Must end with dot (.).'}> + <label for="targets" use:tooltip={$t('tools/dns-cname-builder.input.targets.tooltip')}> <Icon name="target" size="sm" /> - Target FQDNs + {$t('tools/dns-cname-builder.input.targets.label')} </label> <textarea id="targets" bind:value={targetInput} - placeholder="server1.example.com. server2.example.com. mailserver.example.com. ftpserver.example.com." + placeholder={$t('tools/dns-cname-builder.input.targets.placeholder')} rows="6" ></textarea> </div> {:else} <div class="input-group"> - <label for="alias" use:tooltip={'Enter the alias name (left side of CNAME)'}> + <label for="alias" use:tooltip={$t('tools/dns-cname-builder.input.alias.tooltip')}> <Icon name="alias" size="sm" /> - Alias Name + {$t('tools/dns-cname-builder.input.alias.label')} </label> - <input type="text" id="alias" bind:value={aliasInput} placeholder="www" /> + <input + type="text" + id="alias" + bind:value={aliasInput} + placeholder={$t('tools/dns-cname-builder.input.alias.placeholder')} + /> </div> <div class="input-group"> - <label for="target" use:tooltip={'Enter the target FQDN (right side of CNAME). Must end with dot (.).'}> + <label for="target" use:tooltip={$t('tools/dns-cname-builder.input.target.tooltip')}> <Icon name="target" size="sm" /> - Target FQDN + {$t('tools/dns-cname-builder.input.target.label')} </label> - <input type="text" id="target" bind:value={targetInput} placeholder="server1.example.com." /> + <input + type="text" + id="target" + bind:value={targetInput} + placeholder={$t('tools/dns-cname-builder.input.target.placeholder')} + /> </div> {/if} <div class="controls-row"> <div class="input-group"> - <label for="ttl" use:tooltip={'Time To Live in seconds'}> + <label for="ttl" use:tooltip={$t('tools/dns-cname-builder.input.ttl.tooltip')}> <Icon name="clock" size="sm" /> - TTL (seconds) + {$t('tools/dns-cname-builder.input.ttl.label')} </label> <input type="number" id="ttl" bind:value={ttl} min="60" max="86400" /> </div> @@ -260,25 +283,26 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Quick Examples + {$t('tools/dns-cname-builder.examples.title')} </summary> <div class="examples-grid"> {#each examples as example (example.label)} <button class="example-card" onclick={() => loadExample(example)}> <h4>{example.label}</h4> - <p>{example.aliases.split('\n').length} records</p> + <p> + {$t('tools/dns-cname-builder.examples.recordsCount', { count: example.aliases.split('\n').length })} + </p> </button> {/each} </div> </details> <div class="info-panel"> - <h4>CNAME Best Practices</h4> + <h4>{$t('tools/dns-cname-builder.bestPractices.title')}</h4> <ul> - <li>Target must be a Fully Qualified Domain Name (FQDN) ending with a dot</li> - <li>CNAME records cannot coexist with other record types</li> - <li>Avoid CNAME chains longer than 3-4 hops</li> - <li>Never point a CNAME to another CNAME if possible</li> + {#each $t('tools/dns-cname-builder.bestPractices.rules') as rule, index (index)} + <li>{rule}</li> + {/each} </ul> </div> </div> @@ -287,24 +311,24 @@ {#if results.length > 0} <div class="results-section"> <div class="results-header"> - <h2>Generated CNAME Records</h2> + <h2>{$t('tools/dns-cname-builder.results.title')}</h2> <div class="export-buttons"> <button onclick={() => copyToClipboard(results.map((r) => `${r.alias} ${r.ttl} IN CNAME ${r.target}`).join('\n'))} > <Icon name="copy" size="sm" /> - Copy Records + {$t('tools/dns-cname-builder.results.copyButton')} </button> </div> </div> <div class="records-table"> <div class="table-header"> - <div>Alias</div> - <div>TTL</div> - <div>Type</div> - <div>Target</div> - <div>Status</div> + <div>{$t('tools/dns-cname-builder.results.tableHeaders.alias')}</div> + <div>{$t('tools/dns-cname-builder.results.tableHeaders.ttl')}</div> + <div>{$t('tools/dns-cname-builder.results.tableHeaders.type')}</div> + <div>{$t('tools/dns-cname-builder.results.tableHeaders.target')}</div> + <div>{$t('tools/dns-cname-builder.results.tableHeaders.status')}</div> </div> {#each results as record, index (index)} {@const statusInfo = getStatusInfo(record.status)} @@ -329,7 +353,7 @@ <div class="validation-warnings"> <h3> <Icon name="alert-triangle" size="sm" /> - Validation Issues + {$t('tools/dns-cname-builder.validation.title')} </h3> <ul> {#each results.filter((r) => r.status !== 'valid') as record, index (index)} @@ -337,13 +361,13 @@ <li class="warning-item {statusInfo.class}"> <strong>{record.alias}</strong>: {statusInfo.text} {#if record.status === 'missing-dot'} - - Target should end with '.' to be a proper FQDN + - {$t('tools/dns-cname-builder.validation.messages.missingDot')} {/if} {#if record.status === 'loop'} - - Creates a circular reference that will cause DNS resolution to fail + - {$t('tools/dns-cname-builder.validation.messages.loop')} {/if} {#if record.status === 'self-target'} - - Points to itself, which is not allowed + - {$t('tools/dns-cname-builder.validation.messages.selfTarget')} {/if} </li> {/each} diff --git a/src/lib/components/tools/DNSKEYKeyTag.svelte b/src/lib/components/tools/DNSKEYKeyTag.svelte index 968ce650..e7ebfa5a 100644 --- a/src/lib/components/tools/DNSKEYKeyTag.svelte +++ b/src/lib/components/tools/DNSKEYKeyTag.svelte @@ -3,32 +3,33 @@ import Icon from '$lib/components/global/Icon.svelte'; import { parseDNSKEYRecord, calculateKeyTag, validateDNSKEY, DNSSEC_ALGORITHMS } from '$lib/utils/dnssec'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let dnskeyInput = $state('example.org. 3600 IN DNSKEY 257 3 8 AwEAAag'); let activeExampleIndex = $state<number | null>(null); let isActiveExample = $state(true); const clipboard = useClipboard(); - const examples = [ + const examples = $derived([ { - title: 'KSK Example (Algorithm 8 - RSASHA256)', + title: $t('tools/dnskey-key-tag.examples.ksk.title'), dnskey: 'example.org. 3600 IN DNSKEY 257 3 8 AwEAAag/8pPvt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuPt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuPt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuP', - description: 'Key Signing Key with SEP flag set', + description: $t('tools/dnskey-key-tag.examples.ksk.description'), }, { - title: 'ZSK Example (Algorithm 13 - ECDSAP256SHA256)', + title: $t('tools/dnskey-key-tag.examples.zsk.title'), dnskey: 'example.org. 3600 IN DNSKEY 256 3 13 kC1gJ+0qtVgdl0VAO/6t9vRaB15v4PclEV9h4n9JfCuPt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuP', - description: 'Zone Signing Key for data signing', + description: $t('tools/dnskey-key-tag.examples.zsk.description'), }, { - title: 'RDATA Only Format', + title: $t('tools/dnskey-key-tag.examples.rdataOnly.title'), dnskey: '257 3 8 AwEAAag/8pPvt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuPt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuPt1p1YKzY7mD5oCwrTDQeF3jhFV9h4n9JfCuP', - description: 'DNSKEY record data without owner name', + description: $t('tools/dnskey-key-tag.examples.rdataOnly.description'), }, - ]; + ]); const result = $derived.by(() => { if (!dnskeyInput.trim()) return null; @@ -40,7 +41,7 @@ const dnskey = parseDNSKEYRecord(dnskeyInput); if (!dnskey) { - return { error: 'Failed to parse DNSKEY record' }; + return { error: $t('tools/dnskey-key-tag.errors.failedParse') }; } const keyTag = calculateKeyTag(dnskey); @@ -74,10 +75,9 @@ <div class="card"> <header class="card-header"> - <h1>DNSKEY Key Tag Calculator</h1> + <h1>{$t('tools/dnskey-key-tag.title')}</h1> <p> - Compute the DNSKEY key tag from a DNSKEY RR (RFC 4034 algorithm) and display it alongside key metadata for DNSSEC - validation purposes. + {$t('tools/dnskey-key-tag.description')} </p> </header> @@ -86,7 +86,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>DNSKEY Examples</h4> + <h4>{$t('tools/dnskey-key-tag.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, index (index)} @@ -106,23 +106,20 @@ <!-- Input Form --> <div class="card input-card"> <div class="form-group"> - <label - for="dnskey-input" - use:tooltip={"Enter a DNSKEY record in standard format (e.g., 'example.org. 3600 IN DNSKEY 257 3 8 AwEAAag') to calculate its key tag"} - > + <label for="dnskey-input" use:tooltip={$t('tools/dnskey-key-tag.input.tooltip')}> <Icon name="key" size="sm" /> - DNSKEY Record + {$t('tools/dnskey-key-tag.input.label')} </label> <textarea id="dnskey-input" bind:value={dnskeyInput} oninput={handleInputChange} - placeholder="example.org. 3600 IN DNSKEY 257 3 8 AwEAAc..." + placeholder={$t('tools/dnskey-key-tag.input.placeholder')} rows="4" class="dnskey-input {isActiveExample ? 'example-active' : ''}" ></textarea> {#if isActiveExample} - <p class="field-help">Using example data - modify to see your results</p> + <p class="field-help">{$t('tools/dnskey-key-tag.input.exampleActive')}</p> {/if} </div> </div> @@ -134,7 +131,7 @@ <div class="error-content"> <Icon name="alert-triangle" size="sm" /> <div> - <strong>Validation Error:</strong> + <strong>{$t('tools/dnskey-key-tag.errors.validationError')}</strong> {result.error} </div> </div> @@ -142,74 +139,63 @@ {:else if result.dnskey} <div class="card results-card"> <div class="results-header"> - <h3>Key Tag Calculation</h3> + <h3>{$t('tools/dnskey-key-tag.results.title')}</h3> <button class="copy-button {clipboard.isCopied() ? 'copied' : ''}" onclick={() => result && !result.error && result.keyTag !== undefined && clipboard.copy(result.keyTag.toString())} > <Icon name={clipboard.isCopied() ? 'check' : 'copy'} size="sm" /> - Copy Key Tag + {$t('tools/dnskey-key-tag.results.copyButton')} </button> </div> <!-- Key Tag Display --> <div class="key-tag-display"> - <div - class="key-tag-label" - use:tooltip={'A 16-bit identifier calculated from the DNSKEY used to quickly identify which key was used to generate a signature'} - > - Key Tag + <div class="key-tag-label" use:tooltip={$t('tools/dnskey-key-tag.results.keyTagTooltip')}> + {$t('tools/dnskey-key-tag.results.keyTagLabel')} </div> <div class="key-tag-value">{result.keyTag}</div> </div> <!-- DNSKEY Metadata --> <div class="metadata-section"> - <h4>DNSKEY Metadata</h4> + <h4>{$t('tools/dnskey-key-tag.metadata.title')}</h4> <div class="metadata-grid"> <div class="metadata-item"> - <span - class="metadata-label" - use:tooltip={'KSK (Key Signing Key) signs other keys and has the SEP flag set (257). ZSK (Zone Signing Key) signs zone data and has no SEP flag (256).'} - >Key Type</span + <span class="metadata-label" use:tooltip={$t('tools/dnskey-key-tag.metadata.keyType.tooltip')} + >{$t('tools/dnskey-key-tag.metadata.keyType.label')}</span > <span class="metadata-value key-type-{result.dnskey.keyType?.toLowerCase()}" - >{result.dnskey.keyType || 'Unknown'}</span + >{result.dnskey.keyType || $t('tools/dnskey-key-tag.metadata.keyType.unknown')}</span > </div> <div class="metadata-item"> - <span - class="metadata-label" - use:tooltip={'16-bit flags field. Bit 15 (SEP flag) indicates if this is a Key Signing Key. 257 = KSK, 256 = ZSK.'} - >Flags</span + <span class="metadata-label" use:tooltip={$t('tools/dnskey-key-tag.metadata.flags.tooltip')} + >{$t('tools/dnskey-key-tag.metadata.flags.label')}</span > <span class="metadata-value mono">{result.dnskey.flags}</span> </div> <div class="metadata-item"> - <span class="metadata-label" use:tooltip={'Protocol field for DNSKEY records. Must always be 3 (DNSSEC).'} - >Protocol</span + <span class="metadata-label" use:tooltip={$t('tools/dnskey-key-tag.metadata.protocol.tooltip')} + >{$t('tools/dnskey-key-tag.metadata.protocol.label')}</span > <span class="metadata-value mono">{result.dnskey.protocol}</span> </div> <div class="metadata-item"> - <span - class="metadata-label" - use:tooltip={'Cryptographic algorithm used by this key. Common values: 8 (RSASHA256), 13 (ECDSA P-256), 15 (Ed25519).'} - >Algorithm</span + <span class="metadata-label" use:tooltip={$t('tools/dnskey-key-tag.metadata.algorithm.tooltip')} + >{$t('tools/dnskey-key-tag.metadata.algorithm.label')}</span > <span class="metadata-value mono" >{result.dnskey.algorithm} ({(DNSSEC_ALGORITHMS as Record<number, string>)[result.dnskey.algorithm] || - 'Unknown'})</span + $t('tools/dnskey-key-tag.metadata.algorithm.unknown')})</span > </div> </div> <div class="public-key-section"> - <h5 - use:tooltip={'The actual cryptographic public key data encoded in Base64 format. This is used for signature verification.'} - > - Public Key (Base64) + <h5 use:tooltip={$t('tools/dnskey-key-tag.metadata.publicKey.tooltip')}> + {$t('tools/dnskey-key-tag.metadata.publicKey.label')} </h5> <div class="public-key">{result.dnskey.publicKey}</div> </div> @@ -222,35 +208,30 @@ <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>Key Tag Purpose</h4> + <h4>{$t('tools/dnskey-key-tag.education.purpose.title')}</h4> <p> - The key tag is a short identifier used to quickly identify which DNSKEY was used to generate a signature. It's - calculated using a checksum algorithm defined in RFC 4034 and helps optimize DNSSEC validation by avoiding the - need to test every key. + {$t('tools/dnskey-key-tag.education.purpose.description')} </p> </div> <div class="education-item info-panel"> - <h4>Key Types</h4> + <h4>{$t('tools/dnskey-key-tag.education.keyTypes.title')}</h4> <p> - <strong>KSK (Key Signing Key):</strong> Used to sign other keys (ZSKs). Has the SEP flag set (bit 15). - <strong>ZSK (Zone Signing Key):</strong> Used to sign zone data. Does not have the SEP flag set. + {$t('tools/dnskey-key-tag.education.keyTypes.description')} </p> </div> <div class="education-item info-panel"> - <h4>Algorithm Support</h4> + <h4>{$t('tools/dnskey-key-tag.education.algorithmSupport.title')}</h4> <p> - Supports all modern DNSSEC algorithms including RSASHA256 (8), RSASHA512 (10), ECDSA P-256 (13), ECDSA P-384 - (14), and Ed25519 (15). Legacy algorithms like RSAMD5 are deprecated and should not be used. + {$t('tools/dnskey-key-tag.education.algorithmSupport.description')} </p> </div> <div class="education-item info-panel"> - <h4>Validation Process</h4> + <h4>{$t('tools/dnskey-key-tag.education.validation.title')}</h4> <p> - The tool validates DNSKEY format, checks protocol compliance (must be 3), verifies algorithm support, and - ensures proper base64 encoding of the public key before calculating the key tag. + {$t('tools/dnskey-key-tag.education.validation.description')} </p> </div> </div> diff --git a/src/lib/components/tools/DNSLabelNormalizer.svelte b/src/lib/components/tools/DNSLabelNormalizer.svelte index fbf71d43..232497f2 100644 --- a/src/lib/components/tools/DNSLabelNormalizer.svelte +++ b/src/lib/components/tools/DNSLabelNormalizer.svelte @@ -3,6 +3,7 @@ import { useClipboard } from '$lib/composables'; import Icon from '$lib/components/global/Icon.svelte'; import { normalizeLabel, type LabelAnalysis } from '$lib/utils/dns-validation'; + import { t } from '$lib/stores/language'; let input = $state(''); let results = $state<LabelAnalysis[]>([]); @@ -39,29 +40,29 @@ normalizeInput(); } - const examples = [ + const examples = $derived([ { - label: 'Case Normalization', - value: 'Example.COM\nWWW.GOOGLE.com', - description: 'Mixed case domain labels', + label: $t('tools/dns-label-normalizer.examples.caseNormalization.label'), + value: $t('tools/dns-label-normalizer.examples.caseNormalization.value'), + description: $t('tools/dns-label-normalizer.examples.caseNormalization.description'), }, { - label: 'IDN/Punycode', - value: 'москва.Ρ€Ρ„\nxn--80adxhks.xn--p1ai', - description: 'International domain names', + label: $t('tools/dns-label-normalizer.examples.idnPunycode.label'), + value: $t('tools/dns-label-normalizer.examples.idnPunycode.value'), + description: $t('tools/dns-label-normalizer.examples.idnPunycode.description'), }, { - label: 'Homograph Attack', - value: 'googlΠ΅.com\nexΠ°mple.org', - description: 'Cyrillic characters mixed with Latin', + label: $t('tools/dns-label-normalizer.examples.homographAttack.label'), + value: $t('tools/dns-label-normalizer.examples.homographAttack.value'), + description: $t('tools/dns-label-normalizer.examples.homographAttack.description'), }, - ]; + ]); </script> <div class="card"> <header class="card-header"> - <h2>DNS Label Normalizer</h2> - <p>Normalize domain labels with case conversion, IDN detection, and homograph attack analysis.</p> + <h2>{$t('tools/dns-label-normalizer.title')}</h2> + <p>{$t('tools/dns-label-normalizer.subtitle')}</p> </header> <!-- Overview Section --> @@ -70,22 +71,22 @@ <div class="overview-card"> <Icon name="case" size="sm" /> <div> - <strong>Case Normalization</strong> - <span>Converts labels to lowercase following DNS case-insensitivity</span> + <strong>{$t('tools/dns-label-normalizer.overview.caseNormalization.title')}</strong> + <span>{$t('tools/dns-label-normalizer.overview.caseNormalization.description')}</span> </div> </div> <div class="overview-card"> <Icon name="globe" size="sm" /> <div> - <strong>IDN Detection</strong> - <span>Identifies internationalized domain names and punycode encoding</span> + <strong>{$t('tools/dns-label-normalizer.overview.idnDetection.title')}</strong> + <span>{$t('tools/dns-label-normalizer.overview.idnDetection.description')}</span> </div> </div> <div class="overview-card"> <Icon name="shield" size="sm" /> <div> - <strong>Security Analysis</strong> - <span>Detects homograph attacks and mixed script vulnerabilities</span> + <strong>{$t('tools/dns-label-normalizer.overview.securityAnalysis.title')}</strong> + <span>{$t('tools/dns-label-normalizer.overview.securityAnalysis.description')}</span> </div> </div> </div> @@ -96,7 +97,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Example Labels</h3> + <h3>{$t('tools/dns-label-normalizer.examples.title')}</h3> </summary> <div class="examples-inner"> <div class="examples-grid"> @@ -119,19 +120,17 @@ <!-- Input Section --> <section class="input-section"> - <h3>Domain Labels</h3> + <h3>{$t('tools/dns-label-normalizer.input.title')}</h3> <div class="input-inner"> <div class="form-group"> - <label for="input" use:tooltip={'Enter domain labels separated by spaces, commas, or newlines'}> + <label for="input" use:tooltip={$t('tools/dns-label-normalizer.input.tooltip')}> <Icon name="dns-label-normalize" size="xs" /> - Labels to Normalize + {$t('tools/dns-label-normalizer.input.label')} </label> <textarea id="input" bind:value={input} - placeholder="example.com -xn--e1afmkfd.xn--p1ai -mixed-script-Π΅xample.com" + placeholder={$t('tools/dns-label-normalizer.input.placeholder')} rows="4" class="label-input" ></textarea> @@ -142,27 +141,27 @@ mixed-script-Π΅xample.com" <!-- Results Section --> {#if results.length > 0} <section class="results-section"> - <h3>Normalization Results</h3> + <h3>{$t('tools/dns-label-normalizer.results.title')}</h3> {#each results as result, index (index)} <div class="result-item"> <div class="result-header"> - <h4>Label {index + 1}</h4> + <h4>{$t('tools/dns-label-normalizer.results.labelNumber', { number: index + 1 })}</h4> <div class="badges"> {#if result.isIDN} - <span class="badge info">IDN</span> + <span class="badge info">{$t('tools/dns-label-normalizer.results.badges.idn')}</span> {/if} {#if result.hasHomoglyphs} - <span class="badge warning">Homoglyphs</span> + <span class="badge warning">{$t('tools/dns-label-normalizer.results.badges.homoglyphs')}</span> {/if} {#if result.scripts.length > 1} - <span class="badge error">Mixed Scripts</span> + <span class="badge error">{$t('tools/dns-label-normalizer.results.badges.mixedScripts')}</span> {/if} </div> </div> <div class="label-comparison"> <div class="label-row"> - <span class="label-type">Original:</span> + <span class="label-type">{$t('tools/dns-label-normalizer.results.original')}</span> <code class="label-value">{result.original}</code> <button class="copy-button {clipboard.isCopied(`orig-${index}`) ? 'copied' : ''}" @@ -173,7 +172,7 @@ mixed-script-Π΅xample.com" </div> <div class="label-row"> - <span class="label-type">Normalized:</span> + <span class="label-type">{$t('tools/dns-label-normalizer.results.normalized')}</span> <code class="label-value normalized">{result.normalized}</code> <button class="copy-button {clipboard.isCopied(`norm-${index}`) ? 'copied' : ''}" @@ -186,12 +185,12 @@ mixed-script-Π΅xample.com" {#if result.original !== result.normalized} <div class="change-indicator success"> <Icon name="arrow-right" size="sm" /> - Label was normalized + {$t('tools/dns-label-normalizer.results.labelNormalized')} </div> {:else} <div class="change-indicator neutral"> <Icon name="minus" size="sm" /> - No changes needed + {$t('tools/dns-label-normalizer.results.noChanges')} </div> {/if} </div> @@ -200,7 +199,7 @@ mixed-script-Π΅xample.com" <div class="scripts-section"> <h5> <Icon name="globe" size="sm" /> - Scripts Detected ({result.scripts.length}) + {$t('tools/dns-label-normalizer.results.scriptsDetected', { count: result.scripts.length })} </h5> <div class="script-badges"> {#each result.scripts as script (script)} @@ -214,7 +213,7 @@ mixed-script-Π΅xample.com" <div class="validation-section warnings"> <h5> <Icon name="alert-triangle" size="sm" /> - Security Warnings ({result.warnings.length}) + {$t('tools/dns-label-normalizer.results.securityWarnings', { count: result.warnings.length })} </h5> <ul class="validation-list"> {#each result.warnings as warning, index (`warning-${index}`)} @@ -228,7 +227,7 @@ mixed-script-Π΅xample.com" <div class="validation-section errors"> <h5> <Icon name="x-circle" size="sm" /> - Errors ({result.errors.length}) + {$t('tools/dns-label-normalizer.results.errors', { count: result.errors.length })} </h5> <ul class="validation-list"> {#each result.errors as error, index (`error-${index}`)} @@ -244,44 +243,42 @@ mixed-script-Π΅xample.com" <!-- Educational Section --> <section class="education-section"> - <h3>About DNS Label Normalization</h3> + <h3>{$t('tools/dns-label-normalizer.education.title')}</h3> <div class="education-grid"> <div class="education-item"> - <h4>Case Normalization</h4> + <h4>{$t('tools/dns-label-normalizer.education.caseNormalization.title')}</h4> <p> - DNS labels are case-insensitive. This tool converts all labels to lowercase for consistency and comparison. + {$t('tools/dns-label-normalizer.education.caseNormalization.description')} </p> <div class="code-example"> - <code>Example.COM</code> β†’ <code>example.com</code> + {$t('tools/dns-label-normalizer.education.caseNormalization.example')} </div> </div> <div class="education-item"> - <h4>IDN Processing</h4> + <h4>{$t('tools/dns-label-normalizer.education.idnProcessing.title')}</h4> <p> - Internationalized Domain Names use punycode encoding. This tool detects IDN labels and potential encoding - issues. + {$t('tools/dns-label-normalizer.education.idnProcessing.description')} </p> <div class="code-example"> - <code>москва.Ρ€Ρ„</code> ↔ <code>xn--80adxhks.xn--p1ai</code> + {$t('tools/dns-label-normalizer.education.idnProcessing.example')} </div> </div> <div class="education-item"> - <h4>Security Analysis</h4> - <p>Mixed scripts in labels can indicate homograph attacks. This tool warns about potential security risks.</p> + <h4>{$t('tools/dns-label-normalizer.education.securityAnalysis.title')}</h4> + <p>{$t('tools/dns-label-normalizer.education.securityAnalysis.description')}</p> <div class="code-example"> - <code>googlΠ΅.com</code> (Cyrillic 'Π΅') + {$t('tools/dns-label-normalizer.education.securityAnalysis.example')} </div> </div> <div class="education-item"> - <h4>Best Practices</h4> + <h4>{$t('tools/dns-label-normalizer.education.bestPractices.title')}</h4> <p> - Always normalize labels before comparison. Be cautious of mixed scripts and visually similar characters from - different scripts. + {$t('tools/dns-label-normalizer.education.bestPractices.description')} </p> - <div class="code-example">Normalize β†’ Compare β†’ Validate</div> + <div class="code-example">{$t('tools/dns-label-normalizer.education.bestPractices.example')}</div> </div> </div> </section> diff --git a/src/lib/components/tools/DNSMXPlanner.svelte b/src/lib/components/tools/DNSMXPlanner.svelte index a2aeb66c..5c55455d 100644 --- a/src/lib/components/tools/DNSMXPlanner.svelte +++ b/src/lib/components/tools/DNSMXPlanner.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { tooltip } from '$lib/actions/tooltip'; import Icon from '$lib/components/global/Icon.svelte'; + import { t } from '$lib/stores/language'; type MXRecord = { id: string; @@ -24,9 +25,9 @@ // Derived array for display - sorted or original order based on autoSort const displayMxRecords = $derived(autoSort ? [...mxRecords].sort((a, b) => a.priority - b.priority) : mxRecords); - const examples = [ + const examples = $derived([ { - label: 'Basic Setup', + label: $t('tools/dns-mx-planner.examples.basicSetup'), domain: 'company.com', records: [ { priority: 10, mailserver: 'mail.company.com.', role: 'primary' as const }, @@ -34,7 +35,7 @@ ], }, { - label: 'Google Workspace', + label: $t('tools/dns-mx-planner.examples.googleWorkspace'), domain: 'company.com', records: [ { priority: 1, mailserver: 'aspmx.l.google.com.', role: 'primary' as const }, @@ -45,12 +46,12 @@ ], }, { - label: 'Microsoft 365', + label: $t('tools/dns-mx-planner.examples.microsoft365'), domain: 'company.com', records: [{ priority: 0, mailserver: 'company-com.mail.protection.outlook.com.', role: 'primary' as const }], }, { - label: 'Multi-Provider Setup', + label: $t('tools/dns-mx-planner.examples.multiProvider'), domain: 'company.com', records: [ { priority: 10, mailserver: 'mail1.provider1.com.', role: 'primary' as const }, @@ -58,14 +59,14 @@ { priority: 30, mailserver: 'fallback.provider2.com.', role: 'backup' as const }, ], }, - ]; + ]); - const priorityGuidelines = [ - { range: '0-9', usage: 'Highest priority, primary mail servers', color: 'success' }, - { range: '10-19', usage: 'High priority, secondary mail servers', color: 'info' }, - { range: '20-49', usage: 'Medium priority, backup servers', color: 'warning' }, - { range: '50+', usage: 'Low priority, fallback servers', color: 'error' }, - ]; + const priorityGuidelines = $derived([ + { range: '0-9', usage: $t('tools/dns-mx-planner.guidelines.highest'), color: 'success' }, + { range: '10-19', usage: $t('tools/dns-mx-planner.guidelines.high'), color: 'info' }, + { range: '20-49', usage: $t('tools/dns-mx-planner.guidelines.medium'), color: 'warning' }, + { range: '50+', usage: $t('tools/dns-mx-planner.guidelines.low'), color: 'error' }, + ]); function addMXRecord() { const newId = (Math.max(...mxRecords.map((r) => parseInt(r.id)), 0) + 1).toString(); @@ -110,19 +111,19 @@ const issues: string[] = []; if (!record.mailserver.trim()) { - issues.push('Mail server cannot be empty'); + issues.push($t('tools/dns-mx-planner.validation.emptyMailserver')); } else if (!record.mailserver.endsWith('.')) { - issues.push('Mail server should end with a dot (FQDN)'); + issues.push($t('tools/dns-mx-planner.validation.missingDot')); } if (record.priority < 0 || record.priority > 65535) { - issues.push('Priority must be between 0 and 65535'); + issues.push($t('tools/dns-mx-planner.validation.priorityRange')); } // Check for duplicate priorities const duplicates = mxRecords.filter((r) => r.id !== record.id && r.priority === record.priority); if (duplicates.length > 0) { - issues.push('Duplicate priority values detected'); + issues.push($t('tools/dns-mx-planner.validation.duplicatePriority')); } return { valid: issues.length === 0, issues }; @@ -156,44 +157,43 @@ <div class="card"> <div class="card-header"> - <h1>MX Record Planner</h1> - <p class="card-subtitle"> - Plan MX record priorities with fallback guidance, best practices, and sample configurations for popular email - providers. - </p> + <h1>{$t('tools/dns-mx-planner.title')}</h1> + <p class="card-subtitle">{$t('tools/dns-mx-planner.subtitle')}</p> </div> <div class="grid-layout"> <div class="input-section"> <div class="domain-config"> <div class="input-group"> - <label for="domain" use:tooltip={'Domain name for the MX records'}> + <label for="domain" use:tooltip={$t('tools/dns-mx-planner.input.domain.tooltip')}> <Icon name="globe" size="sm" /> - Domain + {$t('tools/dns-mx-planner.input.domain.label')} </label> <input type="text" id="domain" bind:value={domain} placeholder="example.com" /> </div> <div class="input-group"> - <label for="ttl" use:tooltip={'Default Time To Live in seconds for all MX records'}> + <label for="ttl" use:tooltip={$t('tools/dns-mx-planner.input.ttl.tooltip')}> <Icon name="clock" size="sm" /> - Default TTL (seconds) + {$t('tools/dns-mx-planner.input.ttl.label')} </label> <input type="number" id="ttl" bind:value={ttl} min="60" max="86400" /> </div> <button class="add-record-btn" onclick={addMXRecord}> <Icon name="plus" size="sm" /> - Add MX Record + {$t('tools/dns-mx-planner.input.addRecordButton')} </button> </div> <div class="mx-records-section"> <div class="section-header"> - <h3>MX Records</h3> + <h3>{$t('tools/dns-mx-planner.section.mxRecords')}</h3> <button class="sort-btn" onclick={sortRecords}> <Icon name="sort" size="sm" /> - {autoSort ? 'Original Order' : 'Sort by Priority'} + {autoSort + ? $t('tools/dns-mx-planner.section.sortOriginal') + : $t('tools/dns-mx-planner.section.sortPriority')} </button> </div> @@ -202,7 +202,9 @@ {@const validation = validateMXRecord(record)} <div class="record-row" class:error={!validation.valid}> <div class="priority-input"> - <label for="priority-{record.id}" use:tooltip={'Lower numbers = higher priority'}>Priority</label> + <label for="priority-{record.id}" use:tooltip={$t('tools/dns-mx-planner.input.priority.tooltip')} + >{$t('tools/dns-mx-planner.input.priority.label')}</label + > <input id="priority-{record.id}" type="number" @@ -218,7 +220,7 @@ </div> <div class="mailserver-input"> - <label for="mailserver-{record.id}">Mail Server (FQDN)</label> + <label for="mailserver-{record.id}">{$t('tools/dns-mx-planner.input.mailserver')}</label> <input id="mailserver-{record.id}" type="text" @@ -229,15 +231,15 @@ </div> <div class="role-select"> - <label for="role-{record.id}">Role</label> + <label for="role-{record.id}">{$t('tools/dns-mx-planner.input.role.label')}</label> <select id="role-{record.id}" value={record.role} onchange={(e) => updateRecord(record.id, 'role', (e.target as HTMLSelectElement).value)} > - <option value="primary">Primary</option> - <option value="backup">Backup</option> - <option value="custom">Custom</option> + <option value="primary">{$t('tools/dns-mx-planner.input.role.primary')}</option> + <option value="backup">{$t('tools/dns-mx-planner.input.role.backup')}</option> + <option value="custom">{$t('tools/dns-mx-planner.input.role.custom')}</option> </select> </div> @@ -265,13 +267,13 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Quick Examples + {$t('tools/dns-mx-planner.examples.title')} </summary> <div class="examples-grid"> {#each examples as example (example.label)} <button class="example-card" onclick={() => loadExample(example)}> <h4>{example.label}</h4> - <p>{example.records.length} MX records</p> + <p>{$t('tools/dns-mx-planner.examples.recordsCount', { count: example.records.length })}</p> </button> {/each} </div> @@ -280,7 +282,7 @@ <details class="guidance-toggle" bind:open={showGuidance}> <summary> <Icon name="info" size="sm" /> - Priority Guidelines + {$t('tools/dns-mx-planner.guidelines.title')} </summary> <div class="priority-guide"> {#each priorityGuidelines as guideline (guideline.range)} @@ -293,13 +295,13 @@ </details> <div class="info-panel"> - <h4>MX Best Practices</h4> + <h4>{$t('tools/dns-mx-planner.bestPractices.title')}</h4> <ul> - <li>Always have at least two MX records for redundancy</li> - <li>Use different priority values to control mail flow</li> - <li>Ensure all mail servers are properly configured</li> - <li>Test mail delivery to all configured servers</li> - <li>Consider geographic distribution for better performance</li> + <li>{$t('tools/dns-mx-planner.bestPractices.redundancy')}</li> + <li>{$t('tools/dns-mx-planner.bestPractices.priorities')}</li> + <li>{$t('tools/dns-mx-planner.bestPractices.configuration')}</li> + <li>{$t('tools/dns-mx-planner.bestPractices.testing')}</li> + <li>{$t('tools/dns-mx-planner.bestPractices.geographic')}</li> </ul> </div> </div> @@ -308,23 +310,23 @@ {#if mxRecords.length > 0} <div class="results-section"> <div class="results-header"> - <h2>Generated MX Records</h2> + <h2>{$t('tools/dns-mx-planner.results.title')}</h2> <div class="export-buttons"> <button onclick={() => copyToClipboard(generateZoneFileRecords())}> <Icon name="copy" size="sm" /> - Copy Zone Records + {$t('tools/dns-mx-planner.results.copyButton')} </button> </div> </div> <div class="records-table"> <div class="table-header"> - <div>Domain</div> - <div>TTL</div> - <div>Type</div> - <div>Priority</div> - <div>Mail Server</div> - <div>Status</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.domain')}</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.ttl')}</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.type')}</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.priority')}</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.mailServer')}</div> + <div>{$t('tools/dns-mx-planner.results.tableHeaders.status')}</div> </div> {#each displayMxRecords as record (record.id)} {@const validation = validateMXRecord(record)} @@ -343,7 +345,9 @@ <div class="status"> <span class="status-badge {validation.valid ? 'success' : 'error'}"> <Icon name={validation.valid ? 'check-circle' : 'x-circle'} size="xs" /> - {validation.valid ? 'Valid' : 'Issues'} + {validation.valid + ? $t('tools/dns-mx-planner.results.statusValid') + : $t('tools/dns-mx-planner.results.statusIssues')} </span> </div> </div> @@ -354,13 +358,18 @@ <div class="validation-summary"> <h3> <Icon name="alert-triangle" size="sm" /> - Configuration Issues + {$t('tools/dns-mx-planner.results.configurationIssues')} </h3> <ul> {#each mxRecords.filter((r) => !validateMXRecord(r).valid) as record (record.id)} {@const validation = validateMXRecord(record)} <li> - <strong>Priority {record.priority}</strong>: {validation.issues.join(', ')} + <strong + >{$t('tools/dns-mx-planner.results.priorityIssueFormat', { + priority: record.priority, + issues: validation.issues.join(', '), + })}</strong + > </li> {/each} </ul> diff --git a/src/lib/components/tools/DNSOptions6And15.svelte b/src/lib/components/tools/DNSOptions6And15.svelte index 4e38ba36..6c399684 100644 --- a/src/lib/components/tools/DNSOptions6And15.svelte +++ b/src/lib/components/tools/DNSOptions6And15.svelte @@ -13,6 +13,7 @@ import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables/useClipboard.svelte'; import { tooltip } from '$lib/actions/tooltip'; + import { t } from '$lib/stores/language'; const clipboard = useClipboard(); @@ -32,10 +33,10 @@ let decodeResult = $state<any>(null); let decodeError = $state<string>(''); - const navOptions = [ - { value: 'build', label: 'Build Options' }, - { value: 'decode', label: 'Decode Options' }, - ]; + const navOptions = $derived([ + { value: 'build', label: $t('tools/dhcp-options-6-15.nav.build') }, + { value: 'decode', label: $t('tools/dhcp-options-6-15.nav.decode') }, + ]); const decodeExamples = [ { @@ -152,8 +153,8 @@ </script> <ToolContentContainer - title="DHCP Options 6 & 15 - DNS Servers and Domain" - description="Option 6 specifies DNS servers for name resolution, while Option 15 provides the domain name for client hostname resolution. These options work together for complete DNS configuration." + title={$t('tools/dhcp-options-6-15.title')} + description={$t('tools/dhcp-options-6-15.subtitle')} {navOptions} bind:selectedNav={activeTab} > @@ -166,30 +167,45 @@ /> <div class="card input-card"> - <h3>DNS Configuration</h3> + <h3>{$t('tools/dhcp-options-6-15.build.title')}</h3> <fieldset class="form-group"> - <legend>DNS Servers (Option 6)</legend> + <legend>{$t('tools/dhcp-options-6-15.build.dnsServers.legend')}</legend> {#each dnsServers as _server, i (i)} <div class="server-row"> - <input type="text" bind:value={dnsServers[i]} placeholder="e.g., 8.8.8.8" class="input" /> + <input + type="text" + bind:value={dnsServers[i]} + placeholder={$t('tools/dhcp-options-6-15.build.dnsServers.placeholder')} + class="input" + /> {#if dnsServers.length > 1} - <button class="btn btn-danger btn-sm" onclick={() => removeDNSServer(i)}>Remove</button> + <button class="btn btn-danger btn-sm" onclick={() => removeDNSServer(i)} + >{$t('tools/dhcp-options-6-15.build.dnsServers.removeButton')}</button + > {/if} </div> {/each} - <button class="btn btn-secondary btn-sm" onclick={addDNSServer}>Add DNS Server</button> + <button class="btn btn-secondary btn-sm" onclick={addDNSServer} + >{$t('tools/dhcp-options-6-15.build.dnsServers.addButton')}</button + > </fieldset> <div class="form-group"> - <label for="domain-name">Domain Name (Option 15)</label> - <input id="domain-name" type="text" bind:value={domainName} placeholder="e.g., example.com" class="input" /> - <span class="hint">Domain name for client hostname resolution</span> + <label for="domain-name">{$t('tools/dhcp-options-6-15.build.domainName.label')}</label> + <input + id="domain-name" + type="text" + bind:value={domainName} + placeholder={$t('tools/dhcp-options-6-15.build.domainName.placeholder')} + class="input" + /> + <span class="hint">{$t('tools/dhcp-options-6-15.build.domainName.hint')}</span> </div> {#if buildErrors.length > 0} <div class="error-card"> - <strong>Validation Errors:</strong> + <strong>{$t('tools/dhcp-options-6-15.build.validationErrors')}</strong> <ul> {#each buildErrors as error, i (i)} <li>{error}</li> @@ -201,14 +217,14 @@ {#if buildResult} <div class="card result-card"> - <h3>DHCP DNS Options</h3> + <h3>{$t('tools/dhcp-options-6-15.results.title')}</h3> {#if buildResult.option6} <div class="option-section"> - <h4>Option 6 - DNS Servers</h4> + <h4>{$t('tools/dhcp-options-6-15.results.option6.title')}</h4> <div class="result-grid"> <div class="result-item"> - <span class="label">DNS Servers:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.servers')}</span> <div class="servers-list"> {#each buildResult.option6.servers as srv, i (i)} <span class="server-badge">{srv}</span> @@ -217,34 +233,42 @@ </div> <div class="result-item"> - <span class="label">Hex Encoded:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.hexEncoded')}</span> <code class="code-value">{buildResult.option6.hexEncoded}</code> <button class="btn-copy" class:copied={clipboard.isCopied('option6-hex')} onclick={() => clipboard.copy(buildResult!.option6!.hexEncoded, 'option6-hex')} - aria-label="Copy hex" + aria-label={$t('tools/dhcp-options-6-15.results.copyHexLabel')} > - {clipboard.isCopied('option6-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('option6-hex') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <div class="result-item"> - <span class="label">Wire Format:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.wireFormat')}</span> <code class="code-value">{buildResult.option6.wireFormat}</code> <button class="btn-copy" class:copied={clipboard.isCopied('option6-wire')} onclick={() => clipboard.copy(buildResult!.option6!.wireFormat, 'option6-wire')} - aria-label="Copy wire format" + aria-label={$t('tools/dhcp-options-6-15.results.copyWireLabel')} > - {clipboard.isCopied('option6-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('option6-wire') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{buildResult.option6.totalLength} bytes</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.totalLength')}</span> + <span class="value" + >{$t('tools/dhcp-options-6-15.results.option6.bytes', { + length: buildResult.option6.totalLength, + })}</span + > </div> </div> </div> @@ -252,59 +276,69 @@ {#if buildResult.option15} <div class="option-section"> - <h4>Option 15 - Domain Name</h4> + <h4>{$t('tools/dhcp-options-6-15.results.option15.title')}</h4> <div class="result-grid"> <div class="result-item"> - <span class="label">Domain:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.domain')}</span> <span class="value">{buildResult.option15.domain}</span> </div> <div class="result-item"> - <span class="label">Hex Encoded:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.hexEncoded')}</span> <code class="code-value">{buildResult.option15.hexEncoded}</code> <button class="btn-copy" class:copied={clipboard.isCopied('option15-hex')} onclick={() => clipboard.copy(buildResult!.option15!.hexEncoded, 'option15-hex')} - aria-label="Copy hex" + aria-label={$t('tools/dhcp-options-6-15.results.copyHexLabel')} > - {clipboard.isCopied('option15-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('option15-hex') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <div class="result-item"> - <span class="label">Wire Format:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.wireFormat')}</span> <code class="code-value">{buildResult.option15.wireFormat}</code> <button class="btn-copy" class:copied={clipboard.isCopied('option15-wire')} onclick={() => clipboard.copy(buildResult!.option15!.wireFormat, 'option15-wire')} - aria-label="Copy wire format" + aria-label={$t('tools/dhcp-options-6-15.results.copyWireLabel')} > - {clipboard.isCopied('option15-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('option15-wire') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{buildResult.option15.totalLength} bytes</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.totalLength')}</span> + <span class="value" + >{$t('tools/dhcp-options-6-15.results.option15.bytes', { + length: buildResult.option15.totalLength, + })}</span + > </div> </div> </div> {/if} <div class="config-section"> - <h4>Configuration Examples</h4> + <h4>{$t('tools/dhcp-options-6-15.results.config.title')}</h4> <div class="output-group"> <div class="output-header"> - <h5>ISC DHCPd</h5> + <h5>{$t('tools/dhcp-options-6-15.results.config.iscDhcpd')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('isc')} onclick={() => clipboard.copy(buildResult!.configExamples.iscDhcpd, 'isc')} > - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.iscDhcpd}</code></pre> @@ -312,13 +346,15 @@ <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv4</h5> + <h5>{$t('tools/dhcp-options-6-15.results.config.keaDhcp4')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('kea')} onclick={() => clipboard.copy(buildResult!.configExamples.keaDhcp4, 'kea')} > - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.keaDhcp4}</code></pre> @@ -326,13 +362,15 @@ <div class="output-group"> <div class="output-header"> - <h5>dnsmasq</h5> + <h5>{$t('tools/dhcp-options-6-15.results.config.dnsmasq')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('dnsmasq')} onclick={() => clipboard.copy(buildResult!.configExamples.dnsmasq, 'dnsmasq')} > - {clipboard.isCopied('dnsmasq') ? 'Copied' : 'Copy'} + {clipboard.isCopied('dnsmasq') + ? $t('tools/dhcp-options-6-15.results.copiedButton') + : $t('tools/dhcp-options-6-15.results.copyButton')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.dnsmasq}</code></pre> @@ -349,47 +387,47 @@ /> <div class="card input-card"> - <h3>Decode DNS Options</h3> + <h3>{$t('tools/dhcp-options-6-15.decode.title')}</h3> <fieldset class="form-group"> - <legend>Option to Decode</legend> + <legend>{$t('tools/dhcp-options-6-15.decode.optionSelect.legend')}</legend> <div class="option-select"> <label class="radio-label" class:selected={decodeOption === 'option6'} - use:tooltip={{ text: 'Decode Option 6 to extract DNS server addresses from hex' }} + use:tooltip={{ text: $t('tools/dhcp-options-6-15.decode.optionSelect.option6Tooltip') }} > <input type="radio" bind:group={decodeOption} value="option6" /> - <span class="radio-text">Option 6 - DNS Servers</span> + <span class="radio-text">{$t('tools/dhcp-options-6-15.decode.optionSelect.option6')}</span> </label> <label class="radio-label" class:selected={decodeOption === 'option15'} - use:tooltip={{ text: 'Decode Option 15 to extract domain name from hex' }} + use:tooltip={{ text: $t('tools/dhcp-options-6-15.decode.optionSelect.option15Tooltip') }} > <input type="radio" bind:group={decodeOption} value="option15" /> - <span class="radio-text">Option 15 - Domain Name</span> + <span class="radio-text">{$t('tools/dhcp-options-6-15.decode.optionSelect.option15')}</span> </label> </div> </fieldset> <div class="form-group"> - <label for="hex-input">Hex String</label> + <label for="hex-input">{$t('tools/dhcp-options-6-15.decode.hexInput.label')}</label> <textarea id="hex-input" bind:value={hexInput} placeholder={decodeOption === 'option6' - ? 'e.g., 08080808 or 08 08 08 08 08 08 04 04' - : 'e.g., 6578616d706c6503636f6d'} + ? $t('tools/dhcp-options-6-15.decode.hexInput.placeholderOption6') + : $t('tools/dhcp-options-6-15.decode.hexInput.placeholderOption15')} rows="3" class="input" ></textarea> - <span class="hint">Enter hex bytes (spaces optional)</span> + <span class="hint">{$t('tools/dhcp-options-6-15.decode.hexInput.hint')}</span> </div> {#if decodeError} <div class="error-card"> - <strong>Decode Error:</strong> + <strong>{$t('tools/dhcp-options-6-15.decode.error')}</strong> <p>{decodeError}</p> </div> {/if} @@ -397,12 +435,16 @@ {#if decodeResult} <div class="card result-card"> - <h3>Decoded {decodeOption === 'option6' ? 'Option 6' : 'Option 15'}</h3> + <h3> + {$t('tools/dhcp-options-6-15.results.decodedTitle', { + option: decodeOption === 'option6' ? 'Option 6' : 'Option 15', + })} + </h3> {#if decodeOption === 'option6'} <div class="result-grid"> <div class="result-item"> - <span class="label">DNS Servers:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.servers')}</span> <div class="servers-list"> {#each decodeResult.servers as srv, i (i)} <span class="server-badge">{srv}</span> @@ -411,25 +453,29 @@ </div> <div class="result-item"> - <span class="label">Server Count:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.serverCount')}</span> <span class="value">{decodeResult.servers.length}</span> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{decodeResult.totalLength} bytes</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option6.totalLength')}</span> + <span class="value" + >{$t('tools/dhcp-options-6-15.results.option6.bytes', { length: decodeResult.totalLength })}</span + > </div> </div> {:else} <div class="result-grid"> <div class="result-item"> - <span class="label">Domain Name:</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.domainName')}</span> <span class="value">{decodeResult.domain}</span> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{decodeResult.totalLength} bytes</span> + <span class="label">{$t('tools/dhcp-options-6-15.results.option15.totalLength')}</span> + <span class="value" + >{$t('tools/dhcp-options-6-15.results.option15.bytes', { length: decodeResult.totalLength })}</span + > </div> </div> {/if} diff --git a/src/lib/components/tools/DNSRecordValidator.svelte b/src/lib/components/tools/DNSRecordValidator.svelte index 3f47bfee..378b713e 100644 --- a/src/lib/components/tools/DNSRecordValidator.svelte +++ b/src/lib/components/tools/DNSRecordValidator.svelte @@ -12,6 +12,7 @@ type ValidationResult, } from '$lib/utils/dns-validation.js'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let recordType = $state('A'); let recordName = $state('example.com'); @@ -30,47 +31,75 @@ let results = $state<ValidationResult | null>(null); const clipboard = useClipboard(); - const recordTypes = [ - { value: 'A', label: 'A (IPv4 Address)', description: 'Maps domain to IPv4 address' }, - { value: 'AAAA', label: 'AAAA (IPv6 Address)', description: 'Maps domain to IPv6 address' }, - { value: 'CNAME', label: 'CNAME (Canonical Name)', description: 'Alias to another domain' }, - { value: 'MX', label: 'MX (Mail Exchange)', description: 'Mail server for domain' }, - { value: 'TXT', label: 'TXT (Text)', description: 'Arbitrary text data' }, - { value: 'SRV', label: 'SRV (Service)', description: 'Service location and port' }, - { value: 'CAA', label: 'CAA (Certificate Authority)', description: 'Certificate authority authorization' }, - ]; - - const examples = [ + const recordTypes = $derived([ + { + value: 'A', + label: $t('tools/dns-record-validator.recordTypes.a.label'), + description: $t('tools/dns-record-validator.recordTypes.a.description'), + }, + { + value: 'AAAA', + label: $t('tools/dns-record-validator.recordTypes.aaaa.label'), + description: $t('tools/dns-record-validator.recordTypes.aaaa.description'), + }, + { + value: 'CNAME', + label: $t('tools/dns-record-validator.recordTypes.cname.label'), + description: $t('tools/dns-record-validator.recordTypes.cname.description'), + }, + { + value: 'MX', + label: $t('tools/dns-record-validator.recordTypes.mx.label'), + description: $t('tools/dns-record-validator.recordTypes.mx.description'), + }, + { + value: 'TXT', + label: $t('tools/dns-record-validator.recordTypes.txt.label'), + description: $t('tools/dns-record-validator.recordTypes.txt.description'), + }, + { + value: 'SRV', + label: $t('tools/dns-record-validator.recordTypes.srv.label'), + description: $t('tools/dns-record-validator.recordTypes.srv.description'), + }, + { + value: 'CAA', + label: $t('tools/dns-record-validator.recordTypes.caa.label'), + description: $t('tools/dns-record-validator.recordTypes.caa.description'), + }, + ]); + + const examples = $derived([ { type: 'A', name: 'www.example.com', value: '192.0.2.1', - description: 'Basic web server A record', + description: $t('tools/dns-record-validator.examples.a'), }, { type: 'AAAA', name: 'www.example.com', value: '2001:db8::1', - description: 'IPv6 web server record', + description: $t('tools/dns-record-validator.examples.aaaa'), }, { type: 'CNAME', name: 'blog.example.com', value: 'www.example.com.', - description: 'Blog subdomain alias', + description: $t('tools/dns-record-validator.examples.cname'), }, { type: 'MX', name: 'example.com', value: 'mail.example.com.', priority: 10, - description: 'Primary mail server', + description: $t('tools/dns-record-validator.examples.mx'), }, { type: 'TXT', name: 'example.com', value: 'v=spf1 include:_spf.google.com ~all', - description: 'SPF policy record', + description: $t('tools/dns-record-validator.examples.txt'), }, { type: 'SRV', @@ -79,9 +108,9 @@ priority: 0, weight: 5, port: 443, - description: 'HTTPS service record', + description: $t('tools/dns-record-validator.examples.srv'), }, - ]; + ]); function loadExample(example: (typeof examples)[0]) { recordType = example.type; @@ -173,8 +202,8 @@ <div class="card"> <header class="card-header"> - <h1>DNS Record Validator</h1> - <p>Validate individual DNS resource record syntax for proper formatting and common issues</p> + <h1>{$t('tools/dns-record-validator.title')}</h1> + <p>{$t('tools/dns-record-validator.subtitle')}</p> </header> <!-- Educational Overview --> @@ -183,19 +212,22 @@ <div class="overview-item"> <Icon name="check-circle" size="sm" /> <div> - <strong>Syntax Validation:</strong> Verify record values match RFC specifications for format and constraints. + <strong>{$t('tools/dns-record-validator.overview.syntaxValidation.title')}</strong> + {$t('tools/dns-record-validator.overview.syntaxValidation.description')} </div> </div> <div class="overview-item"> <Icon name="alert-triangle" size="sm" /> <div> - <strong>Error Detection:</strong> Identify format errors, range violations, and protocol mismatches. + <strong>{$t('tools/dns-record-validator.overview.errorDetection.title')}</strong> + {$t('tools/dns-record-validator.overview.errorDetection.description')} </div> </div> <div class="overview-item"> <Icon name="lightbulb" size="sm" /> <div> - <strong>Best Practices:</strong> Get warnings about potential issues and optimization suggestions. + <strong>{$t('tools/dns-record-validator.overview.bestPractices.title')}</strong> + {$t('tools/dns-record-validator.overview.bestPractices.description')} </div> </div> </div> @@ -206,7 +238,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools/dns-record-validator.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.type + example.name)} @@ -227,9 +259,9 @@ <div class="card input-card"> <!-- Record Type Selection --> <div class="input-group"> - <label for="record-type" use:tooltip={'Select the DNS record type to validate'}> + <label for="record-type" use:tooltip={$t('tools/dns-record-validator.input.recordType.tooltip')}> <Icon name="tag" size="sm" /> - Record Type + {$t('tools/dns-record-validator.input.recordType.label')} </label> <select id="record-type" bind:value={recordType} onchange={handleInputChange} class="record-type-select"> {#each recordTypes as type (type.value)} @@ -243,16 +275,16 @@ <!-- Record Name --> <div class="input-group"> - <label for="record-name" use:tooltip={'The domain name for this DNS record'}> + <label for="record-name" use:tooltip={$t('tools/dns-record-validator.input.recordName.tooltip')}> <Icon name="globe" size="sm" /> - Record Name + {$t('tools/dns-record-validator.input.recordName.label')} </label> <input id="record-name" type="text" bind:value={recordName} oninput={handleInputChange} - placeholder="example.com" + placeholder={$t('tools/dns-record-validator.input.recordName.placeholder')} class="record-name-input" spellcheck="false" /> @@ -260,16 +292,16 @@ <!-- Record Value --> <div class="input-group"> - <label for="record-value" use:tooltip={'The value/data for this DNS record'}> + <label for="record-value" use:tooltip={$t('tools/dns-record-validator.input.recordValue.tooltip')}> <Icon name="edit" size="sm" /> - Record Value + {$t('tools/dns-record-validator.input.recordValue.label')} </label> {#if recordType === 'TXT'} <textarea id="record-value" bind:value={recordValue} oninput={handleInputChange} - placeholder="Enter TXT record content..." + placeholder={$t('tools/dns-record-validator.input.recordValue.placeholderTxt')} class="record-value-textarea" rows="3" spellcheck="false" @@ -280,7 +312,11 @@ type="text" bind:value={recordValue} oninput={handleInputChange} - placeholder={recordType === 'A' ? '192.0.2.1' : recordType === 'AAAA' ? '2001:db8::1' : 'Record value...'} + placeholder={recordType === 'A' + ? $t('tools/dns-record-validator.input.recordValue.placeholderA') + : recordType === 'AAAA' + ? $t('tools/dns-record-validator.input.recordValue.placeholderAAAA') + : $t('tools/dns-record-validator.input.recordValue.placeholderDefault')} class="record-value-input {results?.valid === true ? 'valid' : results?.valid === false ? 'invalid' : ''}" spellcheck="false" /> @@ -291,7 +327,7 @@ {#if recordType === 'MX'} <div class="additional-fields"> <div class="field-group"> - <label for="priority">Priority</label> + <label for="priority">{$t('tools/dns-record-validator.input.priority')}</label> <input id="priority" type="number" @@ -308,7 +344,7 @@ {#if recordType === 'SRV'} <div class="additional-fields"> <div class="field-group"> - <label for="service">Service</label> + <label for="service">{$t('tools/dns-record-validator.input.service')}</label> <input id="service" type="text" @@ -319,14 +355,14 @@ /> </div> <div class="field-group"> - <label for="protocol">Protocol</label> + <label for="protocol">{$t('tools/dns-record-validator.input.protocol')}</label> <select id="protocol" bind:value={protocol} onchange={handleInputChange} class="protocol-select"> <option value="_tcp">_tcp</option> <option value="_udp">_udp</option> </select> </div> <div class="field-group"> - <label for="srv-priority">Priority</label> + <label for="srv-priority">{$t('tools/dns-record-validator.input.priority')}</label> <input id="srv-priority" type="number" @@ -338,7 +374,7 @@ /> </div> <div class="field-group"> - <label for="weight">Weight</label> + <label for="weight">{$t('tools/dns-record-validator.input.weight')}</label> <input id="weight" type="number" @@ -350,7 +386,7 @@ /> </div> <div class="field-group"> - <label for="port">Port</label> + <label for="port">{$t('tools/dns-record-validator.input.port')}</label> <input id="port" type="number" @@ -367,7 +403,7 @@ {#if recordType === 'CAA'} <div class="additional-fields"> <div class="field-group"> - <label for="flags">Flags</label> + <label for="flags">{$t('tools/dns-record-validator.input.flags')}</label> <input id="flags" type="number" @@ -379,7 +415,7 @@ /> </div> <div class="field-group"> - <label for="tag">Tag</label> + <label for="tag">{$t('tools/dns-record-validator.input.tag')}</label> <select id="tag" bind:value={tag} onchange={handleInputChange} class="tag-select"> <option value="issue">issue</option> <option value="issuewild">issuewild</option> @@ -391,9 +427,9 @@ <!-- TTL --> <div class="input-group"> - <label for="ttl" use:tooltip={'Time To Live in seconds (how long record should be cached)'}> + <label for="ttl" use:tooltip={$t('tools/dns-record-validator.input.ttl.tooltip')}> <Icon name="clock" size="sm" /> - TTL (seconds) + {$t('tools/dns-record-validator.input.ttl.label')} </label> <input id="ttl" @@ -402,7 +438,7 @@ oninput={handleInputChange} min="0" max="2147483647" - placeholder="3600" + placeholder={$t('tools/dns-record-validator.input.ttl.placeholder')} class="ttl-input" /> </div> @@ -414,20 +450,25 @@ <div class="results-header"> <div class="validation-status {results.valid ? 'valid' : 'invalid'}"> <Icon name={results.valid ? 'check-circle' : 'x-circle'} size="sm" /> - <span>{results.valid ? 'Valid' : 'Invalid'} DNS Record</span> + <span + >{results.valid + ? $t('tools/dns-record-validator.results.validStatus') + : $t('tools/dns-record-validator.results.invalidStatus')} + {$t('tools/dns-record-validator.results.dnsRecord')}</span + > </div> <button class="copy-button {clipboard.isCopied() ? 'copied' : ''}" onclick={() => clipboard.copy(formatRecord())} > <Icon name={clipboard.isCopied() ? 'check' : 'copy'} size="sm" /> - Copy Zone Line + {$t('tools/dns-record-validator.results.copyButton')} </button> </div> <!-- Formatted Record --> <div class="formatted-record"> - <h4>Zone File Format:</h4> + <h4>{$t('tools/dns-record-validator.results.zoneFileFormat')}</h4> <pre><code>{formatRecord()}</code></pre> </div> @@ -436,7 +477,7 @@ <div class="validation-section errors"> <h4> <Icon name="x-circle" size="sm" /> - Errors ({results.errors.length}) + {$t('tools/dns-record-validator.results.errors', { count: results.errors.length })} </h4> <ul class="validation-list"> {#each results.errors as error, index (index)} @@ -451,7 +492,7 @@ <div class="validation-section warnings"> <h4> <Icon name="alert-triangle" size="sm" /> - Warnings ({results.warnings.length}) + {$t('tools/dns-record-validator.results.warnings', { count: results.warnings.length })} </h4> <ul class="validation-list"> {#each results.warnings as warning, index (index)} @@ -466,7 +507,7 @@ <div class="validation-section normalized"> <h4> <Icon name="check" size="sm" /> - Normalized Value + {$t('tools/dns-record-validator.results.normalizedValue')} </h4> <code class="normalized-value">{results.normalized}</code> </div> @@ -478,35 +519,23 @@ <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>Common Record Types</h4> - <p> - A/AAAA records map domains to IP addresses. CNAME creates aliases. MX directs email. TXT stores arbitrary data - like SPF policies. SRV specifies service locations. - </p> + <h4>{$t('tools/dns-record-validator.education.commonRecordTypes.title')}</h4> + <p>{$t('tools/dns-record-validator.education.commonRecordTypes.description')}</p> </div> <div class="education-item info-panel"> - <h4>Validation Scope</h4> - <p> - This validator checks syntax, format, and common configuration issues. It doesn't verify that targets exist or - are reachable - use DNS lookup tools for connectivity testing. - </p> + <h4>{$t('tools/dns-record-validator.education.validationScope.title')}</h4> + <p>{$t('tools/dns-record-validator.education.validationScope.description')}</p> </div> <div class="education-item info-panel"> - <h4>TTL Guidelines</h4> - <p> - Use shorter TTLs (300-3600s) for records that change frequently. Longer TTLs (3600-86400s) reduce DNS queries - but slow propagation of changes. Balance based on your needs. - </p> + <h4>{$t('tools/dns-record-validator.education.ttlGuidelines.title')}</h4> + <p>{$t('tools/dns-record-validator.education.ttlGuidelines.description')}</p> </div> <div class="education-item info-panel"> - <h4>Best Practices</h4> - <p> - Always use fully qualified domain names (ending with .) in record values. Validate SPF/DMARC policies - carefully. Keep MX priorities consistent. Use descriptive TXT record formatting. - </p> + <h4>{$t('tools/dns-record-validator.education.bestPractices.title')}</h4> + <p>{$t('tools/dns-record-validator.education.bestPractices.description')}</p> </div> </div> </div> diff --git a/src/lib/components/tools/DNSSPFBuilder.svelte b/src/lib/components/tools/DNSSPFBuilder.svelte index fcf83da1..46fcd959 100644 --- a/src/lib/components/tools/DNSSPFBuilder.svelte +++ b/src/lib/components/tools/DNSSPFBuilder.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; + import { t } from '$lib/stores/language'; interface SPFMechanism { type: 'all' | 'include' | 'a' | 'mx' | 'ptr' | 'ip4' | 'ip6' | 'exists'; @@ -52,16 +53,16 @@ '?': 'Neutral', }; - const mechanismDescriptions = { - all: 'Matches all IPs (should be last)', - include: 'Include another domains SPF record', - a: 'Match A/AAAA records of domain', - mx: 'Match MX records of domain', - ptr: 'Match PTR records (discouraged)', - ip4: 'Match specific IPv4 address/range', - ip6: 'Match specific IPv6 address/range', - exists: 'Check if domain exists', - }; + const mechanismDescriptions = $derived({ + all: $t('tools/spf-builder.mechanisms.types.all.description'), + include: $t('tools/spf-builder.mechanisms.types.include.description'), + a: $t('tools/spf-builder.mechanisms.types.a.description'), + mx: $t('tools/spf-builder.mechanisms.types.mx.description'), + ptr: $t('tools/spf-builder.mechanisms.types.ptr.description'), + ip4: $t('tools/spf-builder.mechanisms.types.ip4.description'), + ip6: $t('tools/spf-builder.mechanisms.types.ip6.description'), + exists: $t('tools/spf-builder.mechanisms.types.exists.description'), + }); const spfRecord = $derived.by(() => { const enabledMechanisms = mechanisms.filter((m) => m.enabled); @@ -110,7 +111,7 @@ // Check for required elements if (enabledMechanisms.length === 0) { - messages.push('At least one mechanism must be enabled'); + messages.push($t('tools/spf-builder.validation.errors.noMechanisms')); } // Count DNS lookups @@ -125,33 +126,33 @@ // Check DNS lookup limit if (dnsLookups > 10) { - messages.push(`Too many DNS lookups (${dnsLookups}). SPF limit is 10.`); + messages.push($t('tools/spf-builder.validation.errors.tooManyLookups', { count: dnsLookups })); } else if (dnsLookups > 8) { - warnings.push(`High DNS lookup count (${dnsLookups}). Consider consolidating.`); + warnings.push($t('tools/spf-builder.validation.warnings.highLookupCount', { count: dnsLookups })); } // Validate mechanism values for (const mech of enabledMechanisms) { if ((mech.type === 'include' || mech.type === 'exists') && !mech.value.trim()) { - messages.push(`${mech.type} mechanism requires a domain value`); + messages.push($t('tools/spf-builder.validation.errors.mechanismRequiresDomain', { type: mech.type })); } if ((mech.type === 'ip4' || mech.type === 'ip6') && !mech.value.trim()) { - messages.push(`${mech.type} mechanism requires an IP address`); + messages.push($t('tools/spf-builder.validation.errors.mechanismRequiresIP', { type: mech.type })); } // Basic IP validation if (mech.type === 'ip4' && mech.value.trim()) { const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}(\/\d{1,2})?$/; if (!ipv4Regex.test(mech.value.trim())) { - messages.push(`Invalid IPv4 address/range: ${mech.value}`); + messages.push($t('tools/spf-builder.validation.errors.invalidIPv4', { value: mech.value })); } } if (mech.type === 'ip6' && mech.value.trim()) { // Basic IPv6 validation (simplified) if (!mech.value.includes(':')) { - messages.push(`Invalid IPv6 address: ${mech.value}`); + messages.push($t('tools/spf-builder.validation.errors.invalidIPv6', { value: mech.value })); } } } @@ -159,26 +160,26 @@ // Check for 'all' mechanism position const allIndex = enabledMechanisms.findIndex((m) => m.type === 'all'); if (allIndex >= 0 && allIndex < enabledMechanisms.length - 1) { - warnings.push("'all' mechanism should typically be last"); + warnings.push($t('tools/spf-builder.validation.warnings.allShouldBeLast')); } // Check for PTR usage if (enabledMechanisms.some((m) => m.type === 'ptr')) { - warnings.push('PTR mechanism is discouraged (slow and unreliable)'); + warnings.push($t('tools/spf-builder.validation.warnings.ptrDiscouraged')); } // Check record length const recordLength = spfRecord.length; if (recordLength > 255) { - messages.push(`SPF record too long (${recordLength} chars). DNS TXT limit is 255.`); + messages.push($t('tools/spf-builder.validation.errors.recordTooLong', { length: recordLength })); } else if (recordLength > 200) { - warnings.push(`SPF record is long (${recordLength} chars). Consider shortening.`); + warnings.push($t('tools/spf-builder.validation.warnings.recordLong', { length: recordLength })); } // Check for conflicting modifiers const redirectEnabled = modifiers.find((m) => m.type === 'redirect' && m.enabled); if (redirectEnabled && enabledMechanisms.length > 0) { - warnings.push('redirect modifier should not be used with mechanisms'); + warnings.push($t('tools/spf-builder.validation.warnings.redirectWithMechanisms')); } return { @@ -246,18 +247,18 @@ selectedExample = example.name; } - const examplePolicies = [ + const examplePolicies = $derived([ { - name: 'Basic Email Provider', - description: 'Simple SPF policy for Google Workspace', + name: $t('tools/spf-builder.examples.basic.name'), + description: $t('tools/spf-builder.examples.basic.description'), mechanisms: [ { type: 'include', qualifier: '+', value: '_spf.google.com', enabled: true }, { type: 'all', qualifier: '~', value: '', enabled: true }, ], }, { - name: 'Multiple Providers', - description: 'SPF policy for multiple email services', + name: $t('tools/spf-builder.examples.multiple.name'), + description: $t('tools/spf-builder.examples.multiple.description'), mechanisms: [ { type: 'include', qualifier: '+', value: '_spf.google.com', enabled: true }, { type: 'include', qualifier: '+', value: 'mailgun.org', enabled: true }, @@ -266,8 +267,8 @@ ], }, { - name: 'Server + Provider', - description: 'Dedicated server with email provider fallback', + name: $t('tools/spf-builder.examples.serverProvider.name'), + description: $t('tools/spf-builder.examples.serverProvider.description'), mechanisms: [ { type: 'ip4', qualifier: '+', value: '203.0.113.1', enabled: true }, { type: 'mx', qualifier: '+', value: '', enabled: true }, @@ -276,36 +277,34 @@ ], }, { - name: 'Strict Policy', - description: 'Restrictive SPF policy with hard fail', + name: $t('tools/spf-builder.examples.strict.name'), + description: $t('tools/spf-builder.examples.strict.description'), mechanisms: [ { type: 'ip4', qualifier: '+', value: '203.0.113.0/24', enabled: true }, { type: 'include', qualifier: '+', value: '_spf.google.com', enabled: true }, { type: 'all', qualifier: '-', value: '', enabled: true }, ], }, - ]; + ]); </script> <div class="card"> <div class="card-header"> - <h1>SPF Policy Builder</h1> - <p class="card-subtitle"> - Craft SPF (Sender Policy Framework) policies with mechanisms, qualifiers, and validation. - </p> + <h1>{$t('tools/spf-builder.title')}</h1> + <p class="card-subtitle">{$t('tools/spf-builder.description')}</p> </div> <div class="grid-layout"> <div class="input-section"> <div class="mechanisms-section"> <div class="section-header"> - <h3 use:tooltip={'Configure SPF mechanisms that define which servers can send email'}> + <h3 use:tooltip={$t('tools/spf-builder.mechanisms.tooltip')}> <Icon name="settings" size="sm" /> - SPF Mechanisms + {$t('tools/spf-builder.mechanisms.title')} </h3> <button type="button" class="add-btn" onclick={addCustomMechanism}> <Icon name="plus" size="sm" /> - Add Custom + {$t('tools/spf-builder.mechanisms.addButton')} </button> </div> @@ -324,10 +323,10 @@ <div class="mechanism-controls"> <div class="qualifier-select"> <select bind:value={mechanism.qualifier} disabled={!mechanism.enabled}> - <option value="+">+ Pass</option> - <option value="-">- Fail</option> - <option value="~">~ SoftFail</option> - <option value="?">? Neutral</option> + <option value="+">{$t('tools/spf-builder.mechanisms.qualifiers.pass')}</option> + <option value="-">{$t('tools/spf-builder.mechanisms.qualifiers.fail')}</option> + <option value="~">{$t('tools/spf-builder.mechanisms.qualifiers.softFail')}</option> + <option value="?">{$t('tools/spf-builder.mechanisms.qualifiers.neutral')}</option> </select> </div> @@ -336,7 +335,7 @@ type="button" class="remove-btn" onclick={() => removeMechanism(index)} - use:tooltip={'Remove this mechanism'} + use:tooltip={$t('tools/spf-builder.mechanisms.removeTooltip')} > <Icon name="x" size="sm" /> </button> @@ -349,15 +348,7 @@ type="text" bind:value={mechanism.value} disabled={!mechanism.enabled} - placeholder={mechanism.type === 'ip4' - ? '203.0.113.1 or 203.0.113.0/24' - : mechanism.type === 'ip6' - ? '2001:db8::1 or 2001:db8::/32' - : mechanism.type === 'include' - ? '_spf.google.com' - : mechanism.type === 'exists' - ? 'check.example.com' - : 'domain.com (optional)'} + placeholder={$t(`tools/spf-builder.mechanisms.types.${mechanism.type}.placeholder`)} class="mechanism-input" /> {/if} @@ -368,9 +359,9 @@ <div class="modifiers-section"> <div class="section-header"> - <h3 use:tooltip={'Optional SPF modifiers for advanced configuration'}> + <h3 use:tooltip={$t('tools/spf-builder.modifiers.tooltip')}> <Icon name="wrench" size="sm" /> - SPF Modifiers + {$t('tools/spf-builder.modifiers.title')} </h3> </div> @@ -386,7 +377,7 @@ type="text" bind:value={modifier.value} disabled={!modifier.enabled} - placeholder={modifier.type === 'redirect' ? 'fallback.example.com' : 'explain.example.com'} + placeholder={$t(`tools/spf-builder.modifiers.${modifier.type}.placeholder`)} class="modifier-input" /> </div> @@ -398,27 +389,31 @@ <div class="results-section"> <div class="spf-record-section"> <div class="section-header"> - <h3>Generated SPF Record</h3> + <h3>{$t('tools/spf-builder.output.title')}</h3> <div class="actions"> <button type="button" class="copy-btn" class:success={buttonStates['copy-spf']} onclick={() => copyToClipboard(spfRecord, 'copy-spf')} - use:tooltip={'Copy SPF record to clipboard'} + use:tooltip={$t('tools/spf-builder.output.copyTooltip')} > <Icon name={buttonStates['copy-spf'] ? 'check' : 'copy'} size="sm" /> - {buttonStates['copy-spf'] ? 'Copied!' : 'Copy'} + {buttonStates['copy-spf'] + ? $t('tools/spf-builder.output.copied') + : $t('tools/spf-builder.output.copyButton')} </button> <button type="button" class="export-btn" class:success={buttonStates['export-spf']} onclick={exportAsZoneFile} - use:tooltip={'Download as zone file'} + use:tooltip={$t('tools/spf-builder.output.exportTooltip')} > <Icon name={buttonStates['export-spf'] ? 'check' : 'download'} size="sm" /> - {buttonStates['export-spf'] ? 'Downloaded!' : 'Export'} + {buttonStates['export-spf'] + ? $t('tools/spf-builder.output.downloaded') + : $t('tools/spf-builder.output.exportButton')} </button> </div> </div> @@ -430,7 +425,7 @@ </div> <div class="zone-file-output"> - <h4>Zone File Format:</h4> + <h4>{$t('tools/spf-builder.output.zoneFileFormat')}</h4> <div class="code-block"> <code>example.com. IN TXT "{spfRecord}"</code> </div> @@ -441,19 +436,19 @@ <div class="section-header"> <h3> <Icon name="certified" size="sm" /> - Policy Validation + {$t('tools/spf-builder.validation.title')} </h3> </div> <div class="stats-grid"> <div class="stat-item"> - <span class="stat-label">DNS Lookups:</span> + <span class="stat-label">{$t('tools/spf-builder.validation.dnsLookupsLabel')}</span> <span class="stat-value" class:warning={validation.dnsLookups > 8} class:error={validation.dnsLookups > 10}> {validation.dnsLookups}/10 </span> </div> <div class="stat-item"> - <span class="stat-label">Record Length:</span> + <span class="stat-label">{$t('tools/spf-builder.validation.recordLengthLabel')}</span> <span class="stat-value" class:warning={validation.recordLength > 200} @@ -463,9 +458,11 @@ </span> </div> <div class="stat-item"> - <span class="stat-label">Status:</span> + <span class="stat-label">{$t('tools/spf-builder.validation.statusLabel')}</span> <span class="stat-value" class:success={validation.isValid} class:error={!validation.isValid}> - {validation.isValid ? 'Valid' : 'Invalid'} + {validation.isValid + ? $t('tools/spf-builder.validation.validStatus') + : $t('tools/spf-builder.validation.invalidStatus')} </span> </div> </div> @@ -495,7 +492,7 @@ {#if validation.isValid && validation.messages.length === 0 && validation.warnings.length === 0} <div class="validation-messages success"> <Icon name="check-circle" size="sm" /> - <div class="message">SPF policy is valid and ready to use!</div> + <div class="message">{$t('tools/spf-builder.validation.successMessage')}</div> </div> {/if} </div> @@ -506,7 +503,7 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Example Policies + {$t('tools/spf-builder.examples.title')} </summary> <div class="examples-grid"> {#each examplePolicies as example (example.name)} diff --git a/src/lib/components/tools/DNSSRVBuilder.svelte b/src/lib/components/tools/DNSSRVBuilder.svelte index aa3cefea..d72e9233 100644 --- a/src/lib/components/tools/DNSSRVBuilder.svelte +++ b/src/lib/components/tools/DNSSRVBuilder.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import { tooltip } from '$lib/actions/tooltip'; import Icon from '$lib/components/global/Icon.svelte'; + import { t } from '$lib/stores/language'; type SRVRecord = { id: string; @@ -30,9 +31,9 @@ ]); let showExamples = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Web Services', + label: $t('tools/dns-srv-builder.examples.webServices'), records: [ { service: '_http', protocol: 'tcp' as const, priority: 10, weight: 5, port: 80, target: 'web1.example.com.' }, { @@ -54,7 +55,7 @@ ], }, { - label: 'Mail Services', + label: $t('tools/dns-srv-builder.examples.mailServices'), records: [ { service: '_smtp', protocol: 'tcp' as const, priority: 10, weight: 5, port: 25, target: 'mail1.example.com.' }, { @@ -76,7 +77,7 @@ ], }, { - label: 'SIP Services', + label: $t('tools/dns-srv-builder.examples.sipServices'), records: [ { service: '_sip', protocol: 'tcp' as const, priority: 10, weight: 5, port: 5060, target: 'sip1.example.com.' }, { service: '_sip', protocol: 'udp' as const, priority: 10, weight: 5, port: 5060, target: 'sip1.example.com.' }, @@ -91,7 +92,7 @@ ], }, { - label: 'XMPP Services', + label: $t('tools/dns-srv-builder.examples.xmppServices'), records: [ { service: '_xmpp-server', @@ -111,7 +112,7 @@ }, ], }, - ]; + ]); const commonServices = [ { service: '_http', port: 80, protocol: 'tcp' as const }, @@ -186,31 +187,31 @@ const issues: string[] = []; if (!record.service.trim()) { - issues.push('Service name cannot be empty'); + issues.push($t('tools/dns-srv-builder.validation.serviceEmpty')); } else if (!record.service.startsWith('_')) { - issues.push('Service name must start with underscore (_)'); + issues.push($t('tools/dns-srv-builder.validation.serviceUnderscore')); } if (!record.name.trim()) { - issues.push('Domain name cannot be empty'); + issues.push($t('tools/dns-srv-builder.validation.domainEmpty')); } if (record.priority < 0 || record.priority > 65535) { - issues.push('Priority must be between 0 and 65535'); + issues.push($t('tools/dns-srv-builder.validation.priorityRange')); } if (record.weight < 0 || record.weight > 65535) { - issues.push('Weight must be between 0 and 65535'); + issues.push($t('tools/dns-srv-builder.validation.weightRange')); } if (record.port < 1 || record.port > 65535) { - issues.push('Port must be between 1 and 65535'); + issues.push($t('tools/dns-srv-builder.validation.portRange')); } if (!record.target.trim()) { - issues.push('Target cannot be empty'); + issues.push($t('tools/dns-srv-builder.validation.targetEmpty')); } else if (!record.target.endsWith('.')) { - issues.push('Target should end with a dot (FQDN)'); + issues.push($t('tools/dns-srv-builder.validation.targetFQDN')); } return { valid: issues.length === 0, issues }; @@ -238,10 +239,9 @@ <div class="card"> <div class="card-header"> - <h1>SRV Record Builder</h1> + <h1>{$t('tools/dns-srv-builder.title')}</h1> <p class="card-subtitle"> - Compose SRV records with service discovery, protocol specification, priority/weight balancing, and target - validation. + {$t('tools/dns-srv-builder.description')} </p> </div> @@ -249,22 +249,22 @@ <div class="input-section"> <div class="controls-header"> <div class="input-group"> - <label for="ttl" use:tooltip={'Default Time To Live in seconds for all SRV records'}> + <label for="ttl" use:tooltip={$t('tools/dns-srv-builder.input.ttl.tooltip')}> <Icon name="clock" size="sm" /> - Default TTL (seconds) + {$t('tools/dns-srv-builder.input.ttl.label')} </label> <input type="number" id="ttl" bind:value={ttl} min="60" max="86400" /> </div> <button class="add-record-btn" onclick={addSRVRecord}> <Icon name="plus" size="sm" /> - Add SRV Record + {$t('tools/dns-srv-builder.input.addRecordButton')} </button> </div> <div class="srv-records-section"> <div class="section-header"> - <h3>SRV Records</h3> + <h3>{$t('tools/dns-srv-builder.input.recordsTitle')}</h3> </div> <div class="records-list"> @@ -274,11 +274,9 @@ <div class="record-fields"> <div class="service-protocol-row"> <div class="service-input"> - <label - for="service-{record.id}" - use:tooltip={'The service name, typically starting with underscore (e.g., _http, _smtp)'} - >Service</label - > + <label for="service-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.service.tooltip')}> + {$t('tools/dns-srv-builder.input.service.label')} + </label> <div class="service-select-wrapper"> <select id="service-{record.id}" @@ -294,14 +292,14 @@ {#each commonServices as service (service.service)} <option value={service.service}>{service.service}</option> {/each} - <option value="custom">Custom</option> + <option value="custom">{$t('tools/dns-srv-builder.input.service.customOption')}</option> </select> {#if record.service === 'custom'} <input type="text" value={record.service} oninput={(e) => updateRecord(record.id, 'service', (e.target as HTMLInputElement).value)} - placeholder="_myservice" + placeholder={$t('tools/dns-srv-builder.input.service.customPlaceholder')} class="custom-service-input" /> {/if} @@ -309,39 +307,40 @@ </div> <div class="protocol-input"> - <label - for="protocol-{record.id}" - use:tooltip={'Transport protocol used by the service (TCP/UDP/TLS/SCTP)'}>Protocol</label - > + <label for="protocol-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.protocol.tooltip')}> + {$t('tools/dns-srv-builder.input.protocol.label')} + </label> <select id="protocol-{record.id}" value={record.protocol} onchange={(e) => updateRecord(record.id, 'protocol', (e.target as HTMLSelectElement).value)} > - <option value="tcp">TCP</option> - <option value="udp">UDP</option> - <option value="tls">TLS</option> - <option value="sctp">SCTP</option> + <option value="tcp">{$t('tools/dns-srv-builder.input.protocol.tcp')}</option> + <option value="udp">{$t('tools/dns-srv-builder.input.protocol.udp')}</option> + <option value="tls">{$t('tools/dns-srv-builder.input.protocol.tls')}</option> + <option value="sctp">{$t('tools/dns-srv-builder.input.protocol.sctp')}</option> </select> </div> <div class="name-input"> - <label for="domain-{record.id}" use:tooltip={'The domain name where this service is located'} - >Domain</label - > + <label for="domain-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.domain.tooltip')}> + {$t('tools/dns-srv-builder.input.domain.label')} + </label> <input id="domain-{record.id}" type="text" value={record.name} oninput={(e) => updateRecord(record.id, 'name', (e.target as HTMLInputElement).value)} - placeholder="example.com" + placeholder={$t('tools/dns-srv-builder.input.domain.placeholder')} /> </div> </div> <div class="priority-weight-row"> <div class="priority-input"> - <label for="priority-{record.id}" use:tooltip={'Lower numbers = higher priority'}>Priority</label> + <label for="priority-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.priority.tooltip')}> + {$t('tools/dns-srv-builder.input.priority.label')} + </label> <input id="priority-{record.id}" type="number" @@ -354,9 +353,9 @@ </div> <div class="weight-input"> - <label for="weight-{record.id}" use:tooltip={'Load balancing weight for same priority'} - >Weight</label - > + <label for="weight-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.weight.tooltip')}> + {$t('tools/dns-srv-builder.input.weight.label')} + </label> <input id="weight-{record.id}" type="number" @@ -368,9 +367,9 @@ </div> <div class="port-input"> - <label for="port-{record.id}" use:tooltip={'Port number where the service is listening (1-65535)'} - >Port</label - > + <label for="port-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.port.tooltip')}> + {$t('tools/dns-srv-builder.input.port.label')} + </label> <input id="port-{record.id}" type="number" @@ -382,17 +381,15 @@ </div> <div class="target-input"> - <label - for="target-{record.id}" - use:tooltip={'Fully Qualified Domain Name of the server hosting the service (must end with dot)'} - >Target (FQDN)</label - > + <label for="target-{record.id}" use:tooltip={$t('tools/dns-srv-builder.input.target.tooltip')}> + {$t('tools/dns-srv-builder.input.target.label')} + </label> <input id="target-{record.id}" type="text" value={record.target} oninput={(e) => updateRecord(record.id, 'target', (e.target as HTMLInputElement).value)} - placeholder="server.example.com." + placeholder={$t('tools/dns-srv-builder.input.target.placeholder')} /> </div> </div> @@ -422,30 +419,48 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Service Examples + {$t('tools/dns-srv-builder.examples.title')} </summary> <div class="examples-grid"> {#each examples as example (example.label)} <button class="example-card" onclick={() => loadExample(example)}> <h4>{example.label}</h4> - <p>{example.records.length} SRV records</p> + <p>{$t('tools/dns-srv-builder.examples.recordsCount', { count: example.records.length })}</p> </button> {/each} </div> </details> <div class="info-panel"> - <h4>SRV Record Structure</h4> + <h4>{$t('tools/dns-srv-builder.info.title')}</h4> <div class="srv-format"> - <code>_service._protocol.domain. TTL IN SRV priority weight port target.</code> + <code>{$t('tools/dns-srv-builder.info.format')}</code> </div> <ul> - <li><strong>Service:</strong> Must start with underscore (e.g., _http, _sip)</li> - <li><strong>Protocol:</strong> Usually tcp, udp, tls, or sctp</li> - <li><strong>Priority:</strong> Lower values = higher priority (0-65535)</li> - <li><strong>Weight:</strong> Load balancing within same priority (0-65535)</li> - <li><strong>Port:</strong> Service port number (1-65535)</li> - <li><strong>Target:</strong> FQDN of the server (must end with dot)</li> + <li> + <strong>{$t('tools/dns-srv-builder.info.serviceLabel')}</strong> + {$t('tools/dns-srv-builder.info.serviceDescription')} + </li> + <li> + <strong>{$t('tools/dns-srv-builder.info.protocolLabel')}</strong> + {$t('tools/dns-srv-builder.info.protocolDescription')} + </li> + <li> + <strong>{$t('tools/dns-srv-builder.info.priorityLabel')}</strong> + {$t('tools/dns-srv-builder.info.priorityDescription')} + </li> + <li> + <strong>{$t('tools/dns-srv-builder.info.weightLabel')}</strong> + {$t('tools/dns-srv-builder.info.weightDescription')} + </li> + <li> + <strong>{$t('tools/dns-srv-builder.info.portLabel')}</strong> + {$t('tools/dns-srv-builder.info.portDescription')} + </li> + <li> + <strong>{$t('tools/dns-srv-builder.info.targetLabel')}</strong> + {$t('tools/dns-srv-builder.info.targetDescription')} + </li> </ul> </div> </div> @@ -454,25 +469,41 @@ {#if srvRecords.length > 0} <div class="results-section"> <div class="results-header"> - <h2>Generated SRV Records</h2> + <h2>{$t('tools/dns-srv-builder.results.title')}</h2> <div class="export-buttons"> <button onclick={() => copyToClipboard(generateSRVRecords())}> <Icon name="copy" size="sm" /> - Copy Records + {$t('tools/dns-srv-builder.results.copyButton')} </button> </div> </div> <div class="records-table"> <div class="table-header"> - <div use:tooltip={'Service name and protocol'}>Service</div> - <div use:tooltip={'Time To Live - how long DNS resolvers should cache this record'}>TTL</div> - <div use:tooltip={'DNS record type (always SRV for service records)'}>Type</div> - <div use:tooltip={'Priority - lower values are preferred (0-65535)'}>Priority</div> - <div use:tooltip={'Weight for load balancing among same priority records (0-65535)'}>Weight</div> - <div use:tooltip={'Port number where the service is available'}>Port</div> - <div use:tooltip={'Target server hostname (FQDN)'}>Target</div> - <div use:tooltip={'Validation status of this SRV record'}>Status</div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.serviceTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.service')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.ttlTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.ttl')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.typeTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.type')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.priorityTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.priority')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.weightTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.weight')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.portTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.port')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.targetTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.target')} + </div> + <div use:tooltip={$t('tools/dns-srv-builder.results.tableHeaders.statusTooltip')}> + {$t('tools/dns-srv-builder.results.tableHeaders.status')} + </div> </div> {#each srvRecords as record (record.id)} {@const validation = validateSRVRecord(record)} @@ -489,7 +520,9 @@ <div class="status"> <span class="status-badge {validation.valid ? 'success' : 'error'}"> <Icon name={validation.valid ? 'check-circle' : 'x-circle'} size="xs" /> - {validation.valid ? 'Valid' : 'Issues'} + {validation.valid + ? $t('tools/dns-srv-builder.results.statusValid') + : $t('tools/dns-srv-builder.results.statusIssues')} </span> </div> </div> @@ -500,7 +533,7 @@ <div class="validation-summary"> <h3> <Icon name="alert-triangle" size="sm" /> - Configuration Issues + {$t('tools/dns-srv-builder.results.validationSummaryTitle')} </h3> <ul> {#each srvRecords.filter((r) => !validateSRVRecord(r).valid) as record (record.id)} diff --git a/src/lib/components/tools/DNSTXTEscape.svelte b/src/lib/components/tools/DNSTXTEscape.svelte index b20815f0..adfe8876 100644 --- a/src/lib/components/tools/DNSTXTEscape.svelte +++ b/src/lib/components/tools/DNSTXTEscape.svelte @@ -1,6 +1,7 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; + import { t } from '$lib/stores/language'; interface TXTChunk { chunk: string; @@ -79,7 +80,7 @@ if (!rawText.trim()) { return { isValid: false, - message: 'Please enter text to escape', + message: $t('tools/dns-txt-escape.validation.emptyText'), type: 'error', }; } @@ -87,7 +88,7 @@ if (maxChunkLength < 1 || maxChunkLength > 255) { return { isValid: false, - message: 'Chunk length must be between 1 and 255 characters', + message: $t('tools/dns-txt-escape.validation.invalidChunkLength'), type: 'error', }; } @@ -96,7 +97,7 @@ if (oversizedChunks.length > 0) { return { isValid: false, - message: `${oversizedChunks.length} chunk(s) exceed the maximum length after escaping`, + message: $t('tools/dns-txt-escape.validation.oversizedChunks', { count: oversizedChunks.length }), type: 'error', }; } @@ -104,14 +105,14 @@ if (chunks.length > 10) { return { isValid: true, - message: `Text split into ${chunks.length} chunks (consider splitting across multiple TXT records)`, + message: $t('tools/dns-txt-escape.validation.manyChunks', { count: chunks.length }), type: 'warning', }; } return { isValid: true, - message: `Text successfully split into ${chunks.length} chunk(s)`, + message: $t('tools/dns-txt-escape.validation.success', { count: chunks.length }), type: 'success', }; }); @@ -147,37 +148,35 @@ showExamples = false; } - const exampleTexts = [ + const exampleTexts = $derived([ { - name: 'SPF Record', - description: 'Sender Policy Framework record for email authentication', - value: 'v=spf1 include:_spf.google.com include:mailgun.org include:servers.mcsv.net ~all', + name: $t('tools/dns-txt-escape.examples.spf.name'), + description: $t('tools/dns-txt-escape.examples.spf.description'), + value: $t('tools/dns-txt-escape.examples.spf.value'), }, { - name: 'DKIM Key', - description: 'DomainKeys Identified Mail public key record', - value: - 'k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6', + name: $t('tools/dns-txt-escape.examples.dkim.name'), + description: $t('tools/dns-txt-escape.examples.dkim.description'), + value: $t('tools/dns-txt-escape.examples.dkim.value'), }, { - name: 'Domain Verification', - description: 'Google domain ownership verification token', - value: 'google-site-verification=rXOxyZounnZasA8Z7oaD3c14JdjS9aKSWvsR1EbUSIQ', + name: $t('tools/dns-txt-escape.examples.domainVerification.name'), + description: $t('tools/dns-txt-escape.examples.domainVerification.description'), + value: $t('tools/dns-txt-escape.examples.domainVerification.value'), }, { - name: 'Long Text Sample', - description: 'Text that will need to be split into multiple chunks', - value: - 'This is a very long text string that will definitely exceed the 255 character limit for DNS TXT records and will need to be properly escaped and split into multiple chunks. The escaping tool should handle this automatically and show you exactly how many chunks are created and what the final DNS record format will look like when you publish it to your DNS provider.', + name: $t('tools/dns-txt-escape.examples.longText.name'), + description: $t('tools/dns-txt-escape.examples.longText.description'), + value: $t('tools/dns-txt-escape.examples.longText.value'), }, - ]; + ]); </script> <div class="card"> <div class="card-header"> - <h1>TXT Record Escape Tool</h1> + <h1>{$t('tools/dns-txt-escape.title')}</h1> <p class="card-subtitle"> - Safely escape and split TXT record strings into DNS-compatible chunks (≀255 characters each). + {$t('tools/dns-txt-escape.description')} </p> </div> @@ -185,45 +184,51 @@ <div class="input-section"> <div class="text-input-config"> <div class="input-group"> - <label for="rawText" use:tooltip={'The original text that will be escaped and split into chunks'}> + <label for="rawText" use:tooltip={$t('tools/dns-txt-escape.input.textToEscape.tooltip')}> <Icon name="edit" size="sm" /> - Text to Escape + {$t('tools/dns-txt-escape.input.textToEscape.label')} </label> - <textarea id="rawText" bind:value={rawText} placeholder="Enter your raw text here..." rows="6"></textarea> + <textarea + id="rawText" + bind:value={rawText} + placeholder={$t('tools/dns-txt-escape.input.textToEscape.placeholder')} + rows="6" + ></textarea> </div> <div class="config-row"> <div class="input-group"> - <label - for="maxChunkLength" - use:tooltip={'Maximum length for each chunk (DNS TXT record limit is 255 characters)'} - > + <label for="maxChunkLength" use:tooltip={$t('tools/dns-txt-escape.input.maxChunkLength.tooltip')}> <Icon name="ruler" size="sm" /> - Max Chunk Length + {$t('tools/dns-txt-escape.input.maxChunkLength.label')} </label> <input id="maxChunkLength" type="number" bind:value={maxChunkLength} min="1" max="255" /> </div> <div class="escape-options"> - <h4 use:tooltip={'Configure how the text should be escaped for DNS compatibility'}> + <h4 use:tooltip={$t('tools/dns-txt-escape.input.escapeOptions.tooltip')}> <Icon name="settings" size="sm" /> - Escape Options + {$t('tools/dns-txt-escape.input.escapeOptions.title')} </h4> <div class="checkbox-group"> <label class="checkbox-label"> <input type="checkbox" bind:checked={escapeQuotes} /> - <span>Escape Quotes (")</span> - <span use:tooltip={'Escape double quote characters as \\"'}><Icon name="help" size="sm" /></span> + <span>{$t('tools/dns-txt-escape.input.escapeOptions.escapeQuotes.label')}</span> + <span use:tooltip={$t('tools/dns-txt-escape.input.escapeOptions.escapeQuotes.tooltip')} + ><Icon name="help" size="sm" /></span + > </label> <label class="checkbox-label"> <input type="checkbox" bind:checked={escapeBackslashes} /> - <span>Escape Backslashes (\\)</span> - <span use:tooltip={'Escape backslash characters as \\\\'}><Icon name="help" size="sm" /></span> + <span>{$t('tools/dns-txt-escape.input.escapeOptions.escapeBackslashes.label')}</span> + <span use:tooltip={$t('tools/dns-txt-escape.input.escapeOptions.escapeBackslashes.tooltip')} + ><Icon name="help" size="sm" /></span + > </label> <label class="checkbox-label"> <input type="checkbox" bind:checked={preserveSpaces} /> - <span>Preserve Spacing</span> - <span use:tooltip={'Keep original whitespace formatting instead of normalizing spaces'} + <span>{$t('tools/dns-txt-escape.input.escapeOptions.preserveSpaces.label')}</span> + <span use:tooltip={$t('tools/dns-txt-escape.input.escapeOptions.preserveSpaces.tooltip')} ><Icon name="help" size="sm" /></span > </label> @@ -247,10 +252,10 @@ {#if chunks.length > 0} <div class="chunks-section"> <div class="section-header"> - <h3>Escaped Chunks ({chunks.length})</h3> + <h3>{$t('tools/dns-txt-escape.results.chunksTitle', { count: chunks.length })}</h3> <div class="stats"> - <span class="stat" use:tooltip={'Total length after escaping'}> - {totalLength} chars + <span class="stat" use:tooltip={$t('tools/dns-txt-escape.results.totalLengthTooltip')}> + {$t('tools/dns-txt-escape.results.totalLengthLabel', { length: totalLength })} </span> </div> </div> @@ -259,7 +264,9 @@ {#each chunks as chunk, index (index)} <div class="chunk-item"> <div class="chunk-header"> - <span class="chunk-number">Chunk {index + 1}</span> + <span class="chunk-number" + >{$t('tools/dns-txt-escape.results.chunkNumber', { number: index + 1 })}</span + > <span class="chunk-length">{chunk.escapedLength}/{maxChunkLength}</span> </div> <div class="chunk-content"> @@ -268,7 +275,7 @@ type="button" class="copy-btn" onclick={() => copyToClipboard(`"${chunk.escaped}"`)} - use:tooltip={'Copy this chunk to clipboard'} + use:tooltip={$t('tools/dns-txt-escape.results.copyChunkTooltip')} > <Icon name="copy" size="sm" /> </button> @@ -280,34 +287,39 @@ <div class="output-section"> <div class="section-header"> - <h3>DNS Record Format</h3> + <h3>{$t('tools/dns-txt-escape.results.dnsRecordTitle')}</h3> <div class="actions"> <button type="button" class="copy-btn" onclick={() => copyToClipboard(dnsRecord)} - use:tooltip={'Copy single-line DNS record format'} + use:tooltip={$t('tools/dns-txt-escape.results.copyButtonTooltip')} > <Icon name="copy" size="sm" /> - Copy + {$t('tools/dns-txt-escape.results.copyButton')} </button> - <button type="button" class="export-btn" onclick={exportAsZoneFile} use:tooltip={'Download as zone file'}> + <button + type="button" + class="export-btn" + onclick={exportAsZoneFile} + use:tooltip={$t('tools/dns-txt-escape.results.exportButtonTooltip')} + > <Icon name="download" size="sm" /> - Export + {$t('tools/dns-txt-escape.results.exportButton')} </button> </div> </div> <div class="output-formats"> <div class="format-section"> - <h4>Single Line Format:</h4> + <h4>{$t('tools/dns-txt-escape.results.singleLineFormat')}</h4> <div class="code-block"> <code>{dnsRecord}</code> </div> </div> <div class="format-section"> - <h4>Zone File Format:</h4> + <h4>{$t('tools/dns-txt-escape.results.zoneFileFormat')}</h4> <div class="code-block"> <pre><code>{zoneFileFormat()}</code></pre> </div> @@ -322,7 +334,7 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Example Texts + {$t('tools/dns-txt-escape.examples.title')} </summary> <div class="examples-grid"> {#each exampleTexts as example (example.name)} diff --git a/src/lib/components/tools/DUIDGenerator.svelte b/src/lib/components/tools/DUIDGenerator.svelte index e5884d5c..61de9204 100644 --- a/src/lib/components/tools/DUIDGenerator.svelte +++ b/src/lib/components/tools/DUIDGenerator.svelte @@ -15,6 +15,14 @@ import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; import { tooltip } from '$lib/actions/tooltip'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools/dhcp-duid-generator'); + }); let duidType = $state<DUIDType>('DUID-LLT'); let macAddress = $state(''); @@ -36,11 +44,13 @@ description: string; } - const examples: DUIDExample[] = DUID_EXAMPLES.map((ex) => ({ - label: ex.name, - config: ex, - description: `${ex.type} configuration example`, - })); + const examples: DUIDExample[] = $derived( + DUID_EXAMPLES.map((ex) => ({ + label: ex.name, + config: ex, + description: $t('tools/dhcp-duid-generator.exampleDescription', { type: ex.type }), + })), + ); function loadExample(example: DUIDExample, index: number): void { const cfg = example.config; @@ -138,8 +148,8 @@ </script> <ToolContentContainer - title="DUID Generator" - description="Generate DHCP Unique Identifier (DUID) for DHCPv6 clients per RFC 8415. Supports DUID-LLT, DUID-EN, DUID-LL, and DUID-UUID types with configuration export." + title={$t('tools/dhcp-duid-generator.title')} + description={$t('tools/dhcp-duid-generator.subtitle')} > <ExamplesCard {examples} @@ -151,20 +161,20 @@ <div class="card input-card"> <div class="card-header"> - <h3>DUID Configuration</h3> - <p class="help-text">Configure DHCP Unique Identifier for DHCPv6 client identification</p> + <h3>{$t('tools/dhcp-duid-generator.input.title')}</h3> + <p class="help-text">{$t('tools/dhcp-duid-generator.input.helpText')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="duid-type"> <Icon name="settings" size="sm" /> - DUID Type + {$t('tools/dhcp-duid-generator.input.duidType.label')} </label> <select id="duid-type" bind:value={duidType}> - <option value="DUID-LLT">DUID-LLT (Type 1) - Link-layer address + time</option> - <option value="DUID-EN">DUID-EN (Type 2) - Enterprise number</option> - <option value="DUID-LL">DUID-LL (Type 3) - Link-layer address</option> - <option value="DUID-UUID">DUID-UUID (Type 4) - UUID</option> + <option value="DUID-LLT">{$t('tools/dhcp-duid-generator.input.duidType.options.duidLlt')}</option> + <option value="DUID-EN">{$t('tools/dhcp-duid-generator.input.duidType.options.duidEn')}</option> + <option value="DUID-LL">{$t('tools/dhcp-duid-generator.input.duidType.options.duidLl')}</option> + <option value="DUID-UUID">{$t('tools/dhcp-duid-generator.input.duidType.options.duidUuid')}</option> </select> </div> @@ -172,28 +182,51 @@ <div class="input-group"> <label for="mac-address"> <Icon name="hash" size="sm" /> - MAC Address + {$t('tools/dhcp-duid-generator.input.macAddress.label')} </label> - <input id="mac-address" type="text" bind:value={macAddress} placeholder="00:1A:2B:3C:4D:5E or 001A2B3C4D5E" /> - <small>Enter MAC address in any common format</small> + <input + id="mac-address" + type="text" + bind:value={macAddress} + placeholder={$t('tools/dhcp-duid-generator.input.macAddress.placeholder')} + /> + <small>{$t('tools/dhcp-duid-generator.input.macAddress.hint')}</small> </div> <div class="input-group"> <label for="hardware-type"> <Icon name="cpu" size="sm" /> - Hardware Type + {$t('tools/dhcp-duid-generator.input.hardwareType.label')} </label> <select id="hardware-type" bind:value={hardwareType}> - <option value={HARDWARE_TYPES.ETHERNET}>Ethernet (1)</option> - <option value={HARDWARE_TYPES.EXPERIMENTAL_ETHERNET}>Experimental Ethernet (2)</option> - <option value={HARDWARE_TYPES.IEEE_802}>IEEE 802 (6)</option> - <option value={HARDWARE_TYPES.ARCNET}>ARCNET (7)</option> - <option value={HARDWARE_TYPES.FRAME_RELAY}>Frame Relay (15)</option> - <option value={HARDWARE_TYPES.ATM}>ATM (16)</option> - <option value={HARDWARE_TYPES.HDLC}>HDLC (17)</option> - <option value={HARDWARE_TYPES.FIBRE_CHANNEL}>Fibre Channel (18)</option> - <option value={HARDWARE_TYPES.IEEE_1394}>IEEE 1394 (24)</option> - <option value={HARDWARE_TYPES.INFINIBAND}>InfiniBand (32)</option> + <option value={HARDWARE_TYPES.ETHERNET} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.ethernet')}</option + > + <option value={HARDWARE_TYPES.EXPERIMENTAL_ETHERNET} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.experimentalEthernet')}</option + > + <option value={HARDWARE_TYPES.IEEE_802} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.ieee802')}</option + > + <option value={HARDWARE_TYPES.ARCNET} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.arcnet')}</option + > + <option value={HARDWARE_TYPES.FRAME_RELAY} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.frameRelay')}</option + > + <option value={HARDWARE_TYPES.ATM}>{$t('tools/dhcp-duid-generator.input.hardwareType.options.atm')}</option> + <option value={HARDWARE_TYPES.HDLC} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.hdlc')}</option + > + <option value={HARDWARE_TYPES.FIBRE_CHANNEL} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.fibreChannel')}</option + > + <option value={HARDWARE_TYPES.IEEE_1394} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.ieee1394')}</option + > + <option value={HARDWARE_TYPES.INFINIBAND} + >{$t('tools/dhcp-duid-generator.input.hardwareType.options.infiniband')}</option + > </select> </div> {/if} @@ -202,18 +235,33 @@ <div class="input-group"> <label for="timestamp"> <Icon name="clock" size="sm" /> - Timestamp (seconds since Jan 1, 2000 UTC) + {$t('tools/dhcp-duid-generator.input.timestamp.label')} </label> <div class="timestamp-controls"> - <input id="timestamp" type="number" bind:value={timestamp} placeholder="Leave empty for current time" /> - <button type="button" class="btn-icon" onclick={useCurrentTimestamp} use:tooltip={'Use current timestamp'}> + <input + id="timestamp" + type="number" + bind:value={timestamp} + placeholder={$t('tools/dhcp-duid-generator.input.timestamp.placeholder')} + /> + <button + type="button" + class="btn-icon" + onclick={useCurrentTimestamp} + use:tooltip={$t('tools/dhcp-duid-generator.input.timestamp.useCurrentTooltip')} + > <Icon name="clock" size="sm" /> </button> - <button type="button" class="btn-icon" onclick={clearTimestamp} use:tooltip={'Clear timestamp'}> + <button + type="button" + class="btn-icon" + onclick={clearTimestamp} + use:tooltip={$t('tools/dhcp-duid-generator.input.timestamp.clearTooltip')} + > <Icon name="x" size="sm" /> </button> </div> - <small>Current: {calculateDUIDTimestamp()} seconds since epoch</small> + <small>{$t('tools/dhcp-duid-generator.input.timestamp.hint', { timestamp: calculateDUIDTimestamp() })}</small> </div> {/if} @@ -221,29 +269,29 @@ <div class="input-group"> <label for="enterprise-number"> <Icon name="building" size="sm" /> - Enterprise Number (IANA) + {$t('tools/dhcp-duid-generator.input.enterpriseNumber.label')} </label> <input id="enterprise-number" type="number" bind:value={enterpriseNumber} - placeholder="e.g., 9 for Cisco, 311 for Microsoft" + placeholder={$t('tools/dhcp-duid-generator.input.enterpriseNumber.placeholder')} /> - <small>IANA Private Enterprise Number</small> + <small>{$t('tools/dhcp-duid-generator.input.enterpriseNumber.hint')}</small> </div> <div class="input-group"> <label for="enterprise-identifier"> <Icon name="key" size="sm" /> - Enterprise Identifier (hex) + {$t('tools/dhcp-duid-generator.input.enterpriseIdentifier.label')} </label> <input id="enterprise-identifier" type="text" bind:value={enterpriseIdentifier} - placeholder="e.g., 0123456789abcdef" + placeholder={$t('tools/dhcp-duid-generator.input.enterpriseIdentifier.placeholder')} /> - <small>Custom identifier in hexadecimal format</small> + <small>{$t('tools/dhcp-duid-generator.input.enterpriseIdentifier.hint')}</small> </div> {/if} @@ -251,10 +299,15 @@ <div class="input-group"> <label for="uuid"> <Icon name="fingerprint" size="sm" /> - UUID + {$t('tools/dhcp-duid-generator.input.uuid.label')} </label> - <input id="uuid" type="text" bind:value={uuid} placeholder="e.g., 550e8400-e29b-41d4-a716-446655440000" /> - <small>Standard UUID format (with or without hyphens)</small> + <input + id="uuid" + type="text" + bind:value={uuid} + placeholder={$t('tools/dhcp-duid-generator.input.uuid.placeholder')} + /> + <small>{$t('tools/dhcp-duid-generator.input.uuid.hint')}</small> </div> {/if} </div> @@ -262,7 +315,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-duid-generator.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -274,16 +327,23 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Generated DUID</h3> + <h3>{$t('tools/dhcp-duid-generator.results.title')}</h3> <div class="summary-card"> - <div><strong>Type:</strong> {result.type} (Type {result.typeCode})</div> - <div><strong>Total Length:</strong> {result.totalLength} bytes</div> + <div> + <strong>{$t('tools/dhcp-duid-generator.results.summary.type')}</strong> + {result.type} + {$t('tools/dhcp-duid-generator.results.summary.typeCode', { code: result.typeCode })} + </div> + <div> + <strong>{$t('tools/dhcp-duid-generator.results.summary.totalLength')}</strong> + {$t('tools/dhcp-duid-generator.results.summary.bytes', { length: result.totalLength })} + </div> </div> <div class="output-group"> <div class="output-header"> - <h4>Hex Encoded DUID</h4> + <h4>{$t('tools/dhcp-duid-generator.results.hexEncoded.title')}</h4> <button type="button" class="copy-btn" @@ -291,7 +351,9 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/dhcp-duid-generator.buttons.copied') + : $t('tools/dhcp-duid-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> @@ -299,7 +361,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/dhcp-duid-generator.results.wireFormat.title')}</h4> <button type="button" class="copy-btn" @@ -307,7 +369,9 @@ onclick={() => clipboard.copy(result!.wireFormat, 'wire')} > <Icon name={clipboard.isCopied('wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') + ? $t('tools/dhcp-duid-generator.buttons.copied') + : $t('tools/dhcp-duid-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.wireFormat}</pre> @@ -315,7 +379,7 @@ {#if result.breakdown && result.breakdown.length > 0} <div class="breakdown-section"> - <h4>DUID Breakdown</h4> + <h4>{$t('tools/dhcp-duid-generator.results.breakdown.title')}</h4> {#each result.breakdown as item, i (i)} <div class="breakdown-item"> <div class="breakdown-label">{item.field}</div> @@ -333,7 +397,7 @@ {#if result.examples.keaDhcp6} <div class="card results"> - <h3>Kea DHCPv6 Configuration</h3> + <h3>{$t('tools/dhcp-duid-generator.config.keaDhcp6')}</h3> <div class="output-group"> <div class="output-header"> <button @@ -343,7 +407,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp6!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/dhcp-duid-generator.buttons.copied') + : $t('tools/dhcp-duid-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp6}</pre> @@ -353,7 +419,7 @@ {#if result.examples.iscDhcpd} <div class="card results"> - <h3>ISC DHCPd Configuration</h3> + <h3>{$t('tools/dhcp-duid-generator.config.iscDhcpd')}</h3> <div class="output-group"> <div class="output-header"> <button @@ -363,7 +429,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/dhcp-duid-generator.buttons.copied') + : $t('tools/dhcp-duid-generator.buttons.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> diff --git a/src/lib/components/tools/EDNSSizeEstimator.svelte b/src/lib/components/tools/EDNSSizeEstimator.svelte index 229ffc93..47907577 100644 --- a/src/lib/components/tools/EDNSSizeEstimator.svelte +++ b/src/lib/components/tools/EDNSSizeEstimator.svelte @@ -3,6 +3,13 @@ import Icon from '$lib/components/global/Icon.svelte'; import { estimateEDNSSize, type DNSRecord, type EDNSEstimate } from '$lib/utils/dns-validation.js'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let queryName = $state('example.com'); let queryType = $state('A'); @@ -16,24 +23,24 @@ const recordTypes = ['A', 'AAAA', 'CNAME', 'MX', 'TXT', 'SRV', 'NS', 'SOA', 'CAA', 'DNSKEY', 'RRSIG']; - const examples = [ + const examples = $derived([ { - name: 'Simple A Record', + name: $t('tools.edns_size_estimator.examples.simple.name'), records: [{ name: 'example.com', type: 'A', value: '192.0.2.1', ttl: 3600 }], - description: 'Basic single A record response', + description: $t('tools.edns_size_estimator.examples.simple.description'), }, { - name: 'Multiple A Records', + name: $t('tools.edns_size_estimator.examples.multiple.name'), records: [ { name: 'example.com', type: 'A', value: '192.0.2.1', ttl: 3600 }, { name: 'example.com', type: 'A', value: '192.0.2.2', ttl: 3600 }, { name: 'example.com', type: 'A', value: '192.0.2.3', ttl: 3600 }, { name: 'example.com', type: 'A', value: '192.0.2.4', ttl: 3600 }, ], - description: 'Load-balanced web servers', + description: $t('tools.edns_size_estimator.examples.multiple.description'), }, { - name: 'Long TXT Records', + name: $t('tools.edns_size_estimator.examples.txt.name'), records: [ { name: 'example.com', @@ -48,28 +55,28 @@ ttl: 3600, }, ], - description: 'SPF and DMARC policies', + description: $t('tools.edns_size_estimator.examples.txt.description'), }, { - name: 'MX Records', + name: $t('tools.edns_size_estimator.examples.mx.name'), records: [ { name: 'example.com', type: 'MX', value: 'mail1.example.com.', priority: 10, ttl: 3600 }, { name: 'example.com', type: 'MX', value: 'mail2.example.com.', priority: 20, ttl: 3600 }, { name: 'example.com', type: 'MX', value: 'mail3.example.com.', priority: 30, ttl: 3600 }, ], - description: 'Mail server configuration', + description: $t('tools.edns_size_estimator.examples.mx.description'), }, { - name: 'Large Response', + name: $t('tools.edns_size_estimator.examples.large.name'), records: Array.from({ length: 20 }, (_, i) => ({ name: `server${i + 1}.example.com`, type: 'A' as const, value: `192.0.2.${i + 1}`, ttl: 3600, })), - description: 'Many A records causing fragmentation risk', + description: $t('tools.edns_size_estimator.examples.large.description'), }, - ]; + ]); function loadExample(example: (typeof examples)[0], index: number) { records = [...example.records]; @@ -189,8 +196,8 @@ <div class="card"> <header class="card-header"> - <h1>EDNS Size Estimator</h1> - <p>Estimate DNS message size and UDP fragmentation risk with EDNS buffer recommendations</p> + <h1>{$t('tools.edns_size_estimator.title')}</h1> + <p>{$t('tools.edns_size_estimator.subtitle')}</p> </header> <!-- Educational Overview --> @@ -199,19 +206,22 @@ <div class="overview-item"> <Icon name="ruler" size="sm" /> <div> - <strong>Size Estimation:</strong> Calculate total DNS message size including headers and record data. + <strong>{$t('tools.edns_size_estimator.overview.sizeEstimation.title')}</strong> + {$t('tools.edns_size_estimator.overview.sizeEstimation.description')} </div> </div> <div class="overview-item"> <Icon name="alert-triangle" size="sm" /> <div> - <strong>Fragmentation Risk:</strong> Assess likelihood of UDP packet fragmentation and delivery issues. + <strong>{$t('tools.edns_size_estimator.overview.fragmentationRisk.title')}</strong> + {$t('tools.edns_size_estimator.overview.fragmentationRisk.description')} </div> </div> <div class="overview-item"> <Icon name="settings" size="sm" /> <div> - <strong>EDNS Recommendations:</strong> Get buffer size recommendations for optimal DNS performance. + <strong>{$t('tools.edns_size_estimator.overview.ednsRecommendations.title')}</strong> + {$t('tools.edns_size_estimator.overview.ednsRecommendations.description')} </div> </div> </div> @@ -222,7 +232,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Response Examples</h3> + <h3>{$t('tools.edns_size_estimator.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example, index (index)} @@ -231,7 +241,9 @@ onclick={() => loadExample(example, index)} > <div class="example-name">{example.name}</div> - <div class="example-count">{example.records.length} record{example.records.length !== 1 ? 's' : ''}</div> + <div class="example-count"> + {$t('tools.edns_size_estimator.examples.recordCount', { count: example.records.length })} + </div> <div class="example-description">{example.description}</div> </button> {/each} @@ -241,27 +253,21 @@ <!-- Configuration --> <section class="config-section"> - <h3>Query Configuration</h3> + <h3>{$t('tools.edns_size_estimator.config.title')}</h3> <div class="config-inner"> <div class="query-toggle"> - <label - class="checkbox-label" - use:tooltip={'Include the DNS query section in size calculations. Queries add ~20-50 bytes depending on name length.'} - > + <label class="checkbox-label" use:tooltip={$t('tools.edns_size_estimator.config.includeQuery.tooltip')}> <input type="checkbox" class="primary-checkbox" bind:checked={includeQuery} onchange={handleInputChange} /> - <span class="checkbox-text">Include query section in estimate</span> + <span class="checkbox-text">{$t('tools.edns_size_estimator.config.includeQuery.label')}</span> </label> </div> {#if includeQuery} <div class="query-inputs"> <div class="field-group"> - <label - for="query-name" - use:tooltip={'The domain name being queried. Longer names result in larger message sizes.'} - > + <label for="query-name" use:tooltip={$t('tools.edns_size_estimator.config.queryName.tooltip')}> <Icon name="globe" size="xs" /> - Query Name + {$t('tools.edns_size_estimator.config.queryName.label')} </label> <input id="query-name" @@ -273,9 +279,9 @@ /> </div> <div class="field-group"> - <label for="query-type" use:tooltip={'The type of DNS record being requested (A, AAAA, MX, etc.).'}> + <label for="query-type" use:tooltip={$t('tools.edns_size_estimator.config.queryType.tooltip')}> <Icon name="list" size="xs" /> - Query Type + {$t('tools.edns_size_estimator.config.queryType.label')} </label> <select id="query-type" bind:value={queryType} onchange={handleInputChange} class="query-select"> {#each recordTypes as type (type)} @@ -291,10 +297,10 @@ <!-- Records Editor --> <div class="card records-card"> <div class="records-header"> - <h3>Response Records</h3> + <h3>{$t('tools.edns_size_estimator.records.title')}</h3> <button class="add-record-btn" onclick={addRecord}> <Icon name="plus" size="sm" /> - Add Record + {$t('tools.edns_size_estimator.records.addRecord')} </button> </div> @@ -303,7 +309,7 @@ <div class="record-item"> <div class="record-fields"> <div class="field-group"> - <label for="name-{index}">Name</label> + <label for="name-{index}">{$t('tools.edns_size_estimator.records.fields.name')}</label> <input id="name-{index}" type="text" @@ -313,7 +319,7 @@ /> </div> <div class="field-group"> - <label for="type-{index}">Type</label> + <label for="type-{index}">{$t('tools.edns_size_estimator.records.fields.type')}</label> <select id="type-{index}" bind:value={record.type} @@ -325,18 +331,18 @@ </select> </div> <div class="field-group"> - <label for="value-{index}">Value</label> + <label for="value-{index}">{$t('tools.edns_size_estimator.records.fields.value')}</label> <input id="value-{index}" type="text" bind:value={record.value} oninput={() => updateRecord(index, 'value', record.value)} - placeholder="Record value" + placeholder={$t('tools.edns_size_estimator.records.fields.valuePlaceholder')} /> </div> {#if record.type === 'MX'} <div class="field-group"> - <label for="priority-{index}">Priority</label> + <label for="priority-{index}">{$t('tools.edns_size_estimator.records.fields.priority')}</label> <input id="priority-{index}" type="number" @@ -348,7 +354,7 @@ </div> {/if} <div class="field-group"> - <label for="ttl-{index}">TTL</label> + <label for="ttl-{index}">{$t('tools.edns_size_estimator.records.fields.ttl')}</label> <input id="ttl-{index}" type="number" @@ -358,7 +364,11 @@ /> </div> </div> - <button class="remove-record-btn" onclick={() => removeRecord(index)} use:tooltip={'Remove this record'}> + <button + class="remove-record-btn" + onclick={() => removeRecord(index)} + use:tooltip={$t('tools.edns_size_estimator.records.removeTooltip')} + > <Icon name="trash" size="sm" /> </button> </div> @@ -366,7 +376,7 @@ {#if records.length === 0} <div class="empty-records"> - <p>No records added. Click "Add Record" to start building your DNS response.</p> + <p>{$t('tools.edns_size_estimator.records.empty')}</p> </div> {/if} </div> @@ -375,39 +385,44 @@ <!-- Results --> {#if results} <section class="results-section"> - <h3>Size Analysis</h3> + <h3>{$t('tools.edns_size_estimator.results.title')}</h3> <div class="results-inner"> <!-- Size Breakdown --> <div class="analysis-card"> <div class="card-header-with-actions"> <h4> <Icon name="ruler" size="sm" /> - Size Breakdown + {$t('tools.edns_size_estimator.results.sizeBreakdown.title')} </h4> <button class="copy-button {clipboard.isCopied() ? 'copied' : ''}" onclick={() => - clipboard.copy(`DNS Message Size Estimate: -Total Size: ${results?.totalSize || 0} bytes -UDP Safe: ${results?.udpSafe ? 'Yes' : 'No'} -Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} + clipboard.copy( + $t('tools.edns_size_estimator.results.copyText', { + totalSize: results?.totalSize || 0, + udpSafe: results?.udpSafe + ? $t('tools.edns_size_estimator.results.yes') + : $t('tools.edns_size_estimator.results.no'), + fragmentationRisk: results?.fragmentationRisk || $t('tools.edns_size_estimator.results.unknown'), + }), + )} > <Icon name={clipboard.isCopied() ? 'check' : 'copy'} size="sm" /> - Copy Summary + {$t('tools.edns_size_estimator.results.copySummary')} </button> </div> <div class="size-breakdown"> <div class="size-item"> - <div class="size-label">DNS Header</div> + <div class="size-label">{$t('tools.edns_size_estimator.results.sizeBreakdown.dnsHeader')}</div> <div class="size-value">{results.baseSize} bytes</div> </div> <div class="size-item"> - <div class="size-label">Records Data</div> + <div class="size-label">{$t('tools.edns_size_estimator.results.sizeBreakdown.recordsData')}</div> <div class="size-value">{results.recordsSize} bytes</div> </div> <div class="size-item total"> - <div class="size-label">Total Size</div> + <div class="size-label">{$t('tools.edns_size_estimator.results.sizeBreakdown.totalSize')}</div> <div class="size-value" style="color: {getSizeColor(results.totalSize)}">{results.totalSize} bytes</div> </div> </div> @@ -417,7 +432,9 @@ Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} <div class="safety-status {results.udpSafe ? 'safe' : 'unsafe'}"> <Icon name={results.udpSafe ? 'check-circle' : 'alert-triangle'} size="sm" /> <span> - {results.udpSafe ? 'UDP Safe' : 'Requires EDNS0'} + {results.udpSafe + ? $t('tools.edns_size_estimator.results.udpSafe') + : $t('tools.edns_size_estimator.results.requiresEdns')} ({results.totalSize} / 512 bytes) </span> </div> @@ -433,25 +450,28 @@ Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} > <h4 style="color: {getRiskColor(results.fragmentationRisk)};"> <Icon name="alert-triangle" size="sm" /> - Fragmentation Analysis + {$t('tools.edns_size_estimator.results.fragmentation.title')} </h4> <div class="fragmentation-risk"> <div class="risk-indicator" style="color: {getRiskColor(results.fragmentationRisk)}"> - <span class="risk-level">{results.fragmentationRisk.toUpperCase()} RISK</span> + <span class="risk-level" + >{$t(`tools.edns_size_estimator.results.fragmentation.riskLevels.${results.fragmentationRisk}`)} + {$t('tools.edns_size_estimator.results.fragmentation.risk')}</span + > </div> <div class="risk-details"> <div class="size-thresholds"> <div class="threshold-item {results.totalSize <= 512 ? 'passed' : 'failed'}"> <Icon name={results.totalSize <= 512 ? 'check' : 'x'} size="sm" /> - ≀ 512 bytes (Classic DNS) + {$t('tools.edns_size_estimator.results.fragmentation.thresholds.classic')} </div> <div class="threshold-item {results.totalSize <= 1232 ? 'passed' : 'failed'}"> <Icon name={results.totalSize <= 1232 ? 'check' : 'x'} size="sm" /> - ≀ 1232 bytes (Safe for most networks) + {$t('tools.edns_size_estimator.results.fragmentation.thresholds.safe')} </div> <div class="threshold-item {results.totalSize <= 4096 ? 'passed' : 'failed'}"> <Icon name={results.totalSize <= 4096 ? 'check' : 'x'} size="sm" /> - ≀ 4096 bytes (Common EDNS buffer) + {$t('tools.edns_size_estimator.results.fragmentation.thresholds.edns')} </div> </div> </div> @@ -463,7 +483,7 @@ Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} <div class="recommendations-card"> <h4> <Icon name="lightbulb" size="sm" /> - Recommendations + {$t('tools.edns_size_estimator.results.recommendations.title')} </h4> <ul class="recommendations-list"> {#each results.recommendations as recommendation (recommendation)} @@ -471,13 +491,13 @@ Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} {/each} {#if results.totalSize > 4096} - <li class="recommendation-item">Consider using TCP for queries expecting large responses</li> + <li class="recommendation-item">{$t('tools.edns_size_estimator.results.recommendations.tcp')}</li> {/if} {#if results.totalSize > 1232} - <li class="recommendation-item">Some networks may fragment packets - monitor delivery success</li> + <li class="recommendation-item">{$t('tools.edns_size_estimator.results.recommendations.fragment')}</li> {/if} {#if !results.udpSafe} - <li class="recommendation-item">EDNS0 support required - advertise appropriate buffer size</li> + <li class="recommendation-item">{$t('tools.edns_size_estimator.results.recommendations.edns0')}</li> {/if} </ul> </div> @@ -490,34 +510,30 @@ Fragmentation Risk: ${results?.fragmentationRisk || 'unknown'}`)} <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>UDP Limitations</h4> + <h4>{$t('tools.edns_size_estimator.education.udpLimitations.title')}</h4> <p> - Classic DNS over UDP is limited to 512 bytes. Larger responses require EDNS0 extension to advertise bigger - buffer sizes. Without EDNS0, servers must truncate responses. + {$t('tools.edns_size_estimator.education.udpLimitations.description')} </p> </div> <div class="education-item info-panel"> - <h4>Fragmentation Issues</h4> + <h4>{$t('tools.edns_size_estimator.education.fragmentationIssues.title')}</h4> <p> - UDP packets larger than ~1232 bytes may be fragmented by network devices. Fragmented packets are more likely - to be dropped, causing DNS resolution failures. + {$t('tools.edns_size_estimator.education.fragmentationIssues.description')} </p> </div> <div class="education-item info-panel"> - <h4>EDNS Buffer Sizes</h4> + <h4>{$t('tools.edns_size_estimator.education.ednsBufferSizes.title')}</h4> <p> - Common EDNS buffer sizes are 1232, 4096, and 8192 bytes. Larger buffers allow bigger responses but increase - fragmentation risk. Choose based on your network environment. + {$t('tools.edns_size_estimator.education.ednsBufferSizes.description')} </p> </div> <div class="education-item info-panel"> - <h4>Optimization Strategies</h4> + <h4>{$t('tools.edns_size_estimator.education.optimizationStrategies.title')}</h4> <p> - Minimize record sizes with shorter names and values. Use separate queries for large responses. Consider TCP - for consistently large responses like DNSSEC-signed zones. + {$t('tools.edns_size_estimator.education.optimizationStrategies.description')} </p> </div> </div> diff --git a/src/lib/components/tools/EUI64.svelte b/src/lib/components/tools/EUI64.svelte index df5047e4..95f601ca 100644 --- a/src/lib/components/tools/EUI64.svelte +++ b/src/lib/components/tools/EUI64.svelte @@ -3,6 +3,14 @@ import { tooltip } from '$lib/actions/tooltip.js'; import { useClipboard } from '$lib/composables'; import Icon from '$lib/components/global/Icon.svelte'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let inputText = $state('00:1A:2B:3C:4D:5E\n02:1A:2B:FF:FE:3C:4D:5F\n08:00:27:12:34:56\n0A:00:27:FF:FE:12:34:57'); let globalPrefix = $state('2001:db8::/64'); @@ -26,7 +34,7 @@ result = { conversions: [], summary: { totalInputs: 0, validInputs: 0, invalidInputs: 0, macToEUI64: 0, eui64ToMAC: 0 }, - errors: [error instanceof Error ? error.message : 'Unknown error'], + errors: [error instanceof Error ? error.message : $t('tools.eui64.errors.unknownError')], }; } finally { isLoading = false; @@ -41,8 +49,18 @@ let filename = ''; if (format === 'csv') { - const headers = - 'Input,Type,MAC Address,EUI-64,IPv6 Link-Local,IPv6 Global,Universal/Local,Unicast/Multicast,Valid,Error'; + const headers = [ + $t('tools.eui64.csvHeaders.input'), + $t('tools.eui64.csvHeaders.type'), + $t('tools.eui64.csvHeaders.macAddress'), + $t('tools.eui64.csvHeaders.eui64'), + $t('tools.eui64.csvHeaders.ipv6LinkLocal'), + $t('tools.eui64.csvHeaders.ipv6Global'), + $t('tools.eui64.csvHeaders.universalLocal'), + $t('tools.eui64.csvHeaders.unicastMulticast'), + $t('tools.eui64.csvHeaders.valid'), + $t('tools.eui64.csvHeaders.error'), + ].join(','); const rows = result.conversions.map( (conv) => `"${conv.input}","${conv.inputType.toUpperCase()}","${conv.macAddress}","${conv.eui64Address}","${conv.ipv6LinkLocal}","${conv.ipv6Global}","${conv.details.universalLocal}","${conv.details.unicastMulticast}","${conv.isValid}","${conv.error || ''}"`, @@ -74,29 +92,25 @@ <div class="card"> <header class="card-header"> - <h2>EUI-64 Converter</h2> - <p>Convert between MAC addresses and IPv6 EUI-64 interface identifiers with automatic IPv6 address generation</p> + <h2>{$t('tools.eui64.title')}</h2> + <p>{$t('tools.eui64.description')}</p> </header> <div class="input-section"> <div class="inputs-section"> - <h3>Address Conversion</h3> + <h3>{$t('tools.eui64.input.title')}</h3> <div class="input-group"> - <label - for="inputs" - use:tooltip={{ text: 'Enter MAC addresses (48-bit) or EUI-64 identifiers (64-bit)', position: 'top' }} - > - MAC Addresses or EUI-64 Identifiers + <label for="inputs" use:tooltip={{ text: $t('tools.eui64.input.addresses.tooltip'), position: 'top' }}> + {$t('tools.eui64.input.addresses.label')} </label> <textarea id="inputs" bind:value={inputText} - placeholder="00:1A:2B:3C:4D:5E 02:1A:2B:FF:FE:3C:4D:5F 08:00:27:12:34:56" + placeholder={$t('tools.eui64.input.addresses.placeholder')} rows="6" ></textarea> <div class="input-help"> - Enter MAC addresses (48-bit) or EUI-64 identifiers (64-bit) one per line. Various formats supported: - xx:xx:xx:xx:xx:xx or xx-xx-xx-xx-xx-xx + {$t('tools.eui64.input.addresses.help')} </div> </div> @@ -104,31 +118,35 @@ <label for="prefix" use:tooltip={{ - text: 'IPv6 network prefix for generating global addresses (e.g., 2001:db8::/64)', + text: $t('tools.eui64.input.globalPrefix.tooltip'), position: 'top', }} > - IPv6 Global Prefix (Optional) + {$t('tools.eui64.input.globalPrefix.label')} </label> - <input id="prefix" type="text" bind:value={globalPrefix} placeholder="2001:db8::/64" /> + <input + id="prefix" + type="text" + bind:value={globalPrefix} + placeholder={$t('tools.eui64.input.globalPrefix.placeholder')} + /> <div class="input-help"> - IPv6 prefix for generating global unicast addresses. Leave empty to use example prefix. + {$t('tools.eui64.input.globalPrefix.help')} </div> </div> </div> <div class="info-section"> - <h3>EUI-64 Information</h3> + <h3>{$t('tools.eui64.info.title')}</h3> <div class="info-content"> <p> - <strong>EUI-64</strong> (Extended Unique Identifier 64-bit) is used to generate IPv6 interface identifiers from - MAC addresses: + {$t('tools.eui64.info.description')} </p> <ul> - <li>Split MAC address: OUI (24 bits) + Device ID (24 bits)</li> - <li>Insert FFFE between OUI and Device ID</li> - <li>Flip the Universal/Local bit (bit 1) in the first octet</li> - <li>Result: 64-bit interface identifier for IPv6</li> + <li>{$t('tools.eui64.info.steps.split')}</li> + <li>{$t('tools.eui64.info.steps.insert')}</li> + <li>{$t('tools.eui64.info.steps.flip')}</li> + <li>{$t('tools.eui64.info.steps.result')}</li> </ul> </div> </div> @@ -137,7 +155,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Converting addresses... + {$t('tools.eui64.processing')} </div> {/if} @@ -145,7 +163,7 @@ <div class="results"> {#if result.errors.length > 0} <div class="errors"> - <h3><Icon name="alert-triangle" /> Errors</h3> + <h3><Icon name="alert-triangle" /> {$t('tools.eui64.results.errors.title')}</h3> {#each result.errors as error (error)} <div class="error-item">{error}</div> {/each} @@ -154,42 +172,42 @@ {#if result.conversions.length > 0} <div class="summary"> - <h3>Conversion Summary</h3> + <h3>{$t('tools.eui64.results.summary.title')}</h3> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{result.summary.totalInputs}</span> - <span class="stat-label">Total Inputs</span> + <span class="stat-label">{$t('tools.eui64.results.summary.totalInputs')}</span> </div> <div class="stat valid"> <span class="stat-value">{result.summary.validInputs}</span> - <span class="stat-label">Valid</span> + <span class="stat-label">{$t('tools.eui64.results.summary.valid')}</span> </div> <div class="stat invalid"> <span class="stat-value">{result.summary.invalidInputs}</span> - <span class="stat-label">Invalid</span> + <span class="stat-label">{$t('tools.eui64.results.summary.invalid')}</span> </div> <div class="stat mac-to-eui"> <span class="stat-value">{result.summary.macToEUI64}</span> - <span class="stat-label">MAC β†’ EUI-64</span> + <span class="stat-label">{$t('tools.eui64.results.summary.macToEUI64')}</span> </div> <div class="stat eui-to-mac"> <span class="stat-value">{result.summary.eui64ToMAC}</span> - <span class="stat-label">EUI-64 β†’ MAC</span> + <span class="stat-label">{$t('tools.eui64.results.summary.eui64ToMAC')}</span> </div> </div> </div> <div class="conversions"> <div class="conversions-header"> - <h3>Address Conversions</h3> + <h3>{$t('tools.eui64.results.conversions.title')}</h3> <div class="export-buttons"> <button onclick={() => exportResults('csv')}> <Icon name="download" /> - Export CSV + {$t('tools.eui64.actions.exportCSV')} </button> <button onclick={() => exportResults('json')}> <Icon name="download" /> - Export JSON + {$t('tools.eui64.actions.exportJSON')} </button> </div> </div> diff --git a/src/lib/components/tools/FreeSpaceFinder.svelte b/src/lib/components/tools/FreeSpaceFinder.svelte index 66cdf1fb..ee652813 100644 --- a/src/lib/components/tools/FreeSpaceFinder.svelte +++ b/src/lib/components/tools/FreeSpaceFinder.svelte @@ -4,8 +4,16 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import '../../../styles/diagnostics-pages.scss'; + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); + let pools = $state(`192.168.0.0/16 10.0.0.0/8`); let allocations = $state(`192.168.1.0/24 @@ -26,9 +34,9 @@ let selectedExampleIndex = $state<number | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Office Network Gaps', + label: $t('tools.free_space_finder.examples.officeNetworkGaps.label'), pools: '192.168.0.0/16', allocations: `192.168.1.0/24 192.168.10.0/24 @@ -36,7 +44,7 @@ targetPrefix: 24, }, { - label: 'Large Pool Analysis', + label: $t('tools.free_space_finder.examples.largePoolAnalysis.label'), pools: '10.0.0.0/8', allocations: `10.0.0.0/16 10.1.0.0/16 @@ -44,7 +52,7 @@ targetPrefix: null, }, { - label: 'Multi-Pool Setup', + label: $t('tools.free_space_finder.examples.homeNetworkSpace.label'), pools: `172.16.0.0/12 192.168.0.0/16`, allocations: `172.16.1.0/24 @@ -52,7 +60,7 @@ targetPrefix: 28, }, { - label: 'Campus Network Planning', + label: $t('tools.free_space_finder.examples.ipv6Planning.label'), pools: `10.10.0.0/16 10.20.0.0/16`, allocations: `10.10.1.0/24 @@ -61,23 +69,14 @@ targetPrefix: 25, }, { - label: 'Data Center Allocation', + label: $t('tools.free_space_finder.examples.datacenterInventory.label'), pools: '172.20.0.0/14', allocations: `172.20.0.0/16 172.21.0.0/16 172.23.128.0/17`, targetPrefix: 20, }, - { - label: 'Service Provider Space', - pools: `203.0.113.0/24 -198.51.100.0/24`, - allocations: `203.0.113.0/26 -203.0.113.128/25 -198.51.100.64/26`, - targetPrefix: 27, - }, - ]; + ]); function loadExample(example: (typeof examples)[0], index: number) { pools = example.pools; @@ -148,7 +147,7 @@ } catch (error) { result = { success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + error: error instanceof Error ? error.message : $t('tools.free_space_finder.errors.unknownError'), availableBlocks: [], totalBlocks: 0, totalAddresses: 0, @@ -199,8 +198,8 @@ <div class="card"> <header class="card-header"> - <h2>Free Space Finder</h2> - <p>Discover all available address blocks within network pools</p> + <h2>{$t('tools.free_space_finder.title')}</h2> + <p>{$t('tools.free_space_finder.description')}</p> </header> <!-- Examples --> @@ -208,7 +207,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools.free_space_finder.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (i)} @@ -232,28 +231,28 @@ <section class="input-section"> <div class="input-grid"> <div class="input-group"> - <label for="pools" use:tooltip={'Enter network pools - one CIDR block per line (e.g., 192.168.0.0/16)'}> - Network Pools + <label for="pools" use:tooltip={$t('tools.free_space_finder.input.pools.tooltip')}> + {$t('tools.free_space_finder.input.pools.label')} </label> <textarea id="pools" bind:value={pools} oninput={handleInputChange} - placeholder="192.168.0.0/16 10.0.0.0/8" + placeholder={$t('tools.free_space_finder.input.pools.placeholder')} rows="4" required ></textarea> </div> <div class="input-group"> - <label for="allocations" use:tooltip={'Enter allocated/used blocks - one CIDR block per line'}> - Allocated Blocks + <label for="allocations" use:tooltip={$t('tools.free_space_finder.input.allocations.tooltip')}> + {$t('tools.free_space_finder.input.allocations.label')} </label> <textarea id="allocations" bind:value={allocations} oninput={handleInputChange} - placeholder="192.168.1.0/24 192.168.10.0/24" + placeholder={$t('tools.free_space_finder.input.allocations.placeholder')} rows="4" ></textarea> </div> @@ -261,11 +260,8 @@ <div class="filter-section"> <div class="input-group"> - <label - for="target-prefix" - use:tooltip={'Filter results to show only blocks that can accommodate the target prefix length'} - > - Target Prefix Length (Optional) + <label for="target-prefix" use:tooltip={$t('tools.free_space_finder.input.targetPrefix.tooltip')}> + {$t('tools.free_space_finder.input.targetPrefix.label')} </label> <div class="prefix-input-wrapper"> <input @@ -275,7 +271,7 @@ oninput={handleInputChange} min="1" max="32" - placeholder="e.g., 24" + placeholder={$t('tools.free_space_finder.input.targetPrefix.placeholder')} /> <span class="prefix-hint">/{targetPrefix || 'xx'}</span> <button @@ -284,7 +280,7 @@ targetPrefix = null; handleInputChange(); }} - aria-label="Clear filter" + aria-label={$t('tools.free_space_finder.actions.clearFilter')} > <Icon name="x" size="xs" /> </button> @@ -298,15 +294,15 @@ <section class="results-section"> {#if result.success} <div class="results-header"> - <h3>Available Free Space</h3> + <h3>{$t('tools.free_space_finder.results.title')}</h3> <div class="results-summary"> <span class="metric"> <Icon name="free-blocks" size="sm" /> - {result.totalBlocks} free blocks + {$t('tools.free_space_finder.results.blocks', { count: result.totalBlocks })} </span> <span class="metric"> <Icon name="network" size="sm" /> - {formatNumber(result.totalAddresses)} addresses + {$t('tools.free_space_finder.results.addresses', { count: formatNumber(result.totalAddresses) })} </span> </div> </div> @@ -314,7 +310,7 @@ <!-- Address Space Visualization --> {#if result.availableBlocks.length > 0 && result.visualization} <div class="visualization-section"> - <h4>Address Space Visualization</h4> + <h4>{$t('tools.free_space_finder.visualization.title')}</h4> <div class="visualization-container"> <div class="viz-legend"> <div class="legend-item"> diff --git a/src/lib/components/tools/FreeformTLVBuilder.svelte b/src/lib/components/tools/FreeformTLVBuilder.svelte index 38a18c3c..15d3236c 100644 --- a/src/lib/components/tools/FreeformTLVBuilder.svelte +++ b/src/lib/components/tools/FreeformTLVBuilder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { type TLVOption, type TLVResult, @@ -37,17 +38,53 @@ description: `Option ${ex.optionCode}: ${ex.items.length} item${ex.items.length > 1 ? 's' : ''} - ${ex.items.map((i) => i.dataType).join(', ')}`, })); - const dataTypeOptions: Array<{ value: TLVDataType; label: string; description: string }> = [ - { value: 'ipv4', label: 'IPv4 Address', description: '4 bytes, e.g., 192.168.1.1' }, - { value: 'ipv6', label: 'IPv6 Address', description: '16 bytes, e.g., 2001:db8::1' }, - { value: 'fqdn', label: 'Domain Name (FQDN)', description: 'DNS wire format with length prefixes' }, - { value: 'string', label: 'String (UTF-8)', description: 'Text encoded as UTF-8 bytes' }, - { value: 'hex', label: 'Raw Hex', description: 'Direct hex bytes' }, - { value: 'uint8', label: 'UInt8', description: '1 byte unsigned integer (0-255)' }, - { value: 'uint16', label: 'UInt16', description: '2 byte unsigned integer (0-65535)' }, - { value: 'uint32', label: 'UInt32', description: '4 byte unsigned integer (0-4294967295)' }, - { value: 'boolean', label: 'Boolean', description: '1 byte (0 or 1)' }, - ]; + const dataTypeOptions = $derived([ + { + value: 'ipv4' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv4.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv4.description'), + }, + { + value: 'ipv6' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv6.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv6.description'), + }, + { + value: 'fqdn' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.fqdn.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.fqdn.description'), + }, + { + value: 'string' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.string.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.string.description'), + }, + { + value: 'hex' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.hex.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.hex.description'), + }, + { + value: 'uint8' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint8.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint8.description'), + }, + { + value: 'uint16' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint16.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint16.description'), + }, + { + value: 'uint32' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint32.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint32.description'), + }, + { + value: 'boolean' as const, + label: $t('tools/freeform-tlv-builder.dataItems.dataTypes.boolean.label'), + description: $t('tools/freeform-tlv-builder.dataItems.dataTypes.boolean.description'), + }, + ]); function loadExample(example: TLVExample, index: number): void { // Deep copy the option to avoid reference issues @@ -93,23 +130,23 @@ function getPlaceholder(dataType: TLVDataType): string { switch (dataType) { case 'ipv4': - return '192.168.1.1'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv4.placeholder'); case 'ipv6': - return '2001:db8::1'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.ipv6.placeholder'); case 'fqdn': - return 'example.com'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.fqdn.placeholder'); case 'string': - return 'Enter text'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.string.placeholder'); case 'hex': - return 'deadbeef or DE AD BE EF'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.hex.placeholder'); case 'uint8': - return '0-255'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint8.placeholder'); case 'uint16': - return '0-65535'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint16.placeholder'); case 'uint32': - return '0-4294967295'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.uint32.placeholder'); case 'boolean': - return '0, 1, true, or false'; + return $t('tools/freeform-tlv-builder.dataItems.dataTypes.boolean.placeholder'); default: return ''; } @@ -159,8 +196,8 @@ </script> <ToolContentContainer - title="Freeform TLV Composer" - description="Build custom DHCP options using Type-Length-Value encoding. Support for IPv4, IPv6, FQDN, strings, hex data, and numeric types with live hex preview." + title={$t('tools/freeform-tlv-builder.title')} + description={$t('tools/freeform-tlv-builder.subtitle')} > <ExamplesCard {examples} @@ -172,28 +209,40 @@ <div class="card input-card"> <div class="card-header"> - <h3>Option Configuration</h3> + <h3>{$t('tools/freeform-tlv-builder.optionConfig.title')}</h3> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="option-code"> <Icon name="hash" size="sm" /> - Option Code - <span class="required">*</span> + {$t('tools/freeform-tlv-builder.optionConfig.optionCode.label')} + <span class="required">{$t('tools/freeform-tlv-builder.common.required')}</span> </label> - <input id="option-code" type="number" bind:value={option.optionCode} min="0" max="255" placeholder="224" /> - <span class="help-text">DHCP option number (0-255, recommend 224-254 for custom)</span> + <input + id="option-code" + type="number" + bind:value={option.optionCode} + min="0" + max="255" + placeholder={$t('tools/freeform-tlv-builder.optionConfig.optionCode.placeholder')} + /> + <span class="help-text">{$t('tools/freeform-tlv-builder.optionConfig.optionCode.hint')}</span> </div> <div class="input-group"> <label for="option-name"> <Icon name="tag" size="sm" /> - Option Name - <span class="required">*</span> + {$t('tools/freeform-tlv-builder.optionConfig.optionName.label')} + <span class="required">{$t('tools/freeform-tlv-builder.common.required')}</span> </label> - <input id="option-name" type="text" bind:value={option.optionName} placeholder="e.g., Custom Server Option" /> - <span class="help-text">Descriptive name for this option</span> + <input + id="option-name" + type="text" + bind:value={option.optionName} + placeholder={$t('tools/freeform-tlv-builder.optionConfig.optionName.placeholder')} + /> + <span class="help-text">{$t('tools/freeform-tlv-builder.optionConfig.optionName.hint')}</span> </div> </div> </div> @@ -201,16 +250,16 @@ <div class="card input-card"> <div class="card-header"> - <h3>Data Items</h3> + <h3>{$t('tools/freeform-tlv-builder.dataItems.title')}</h3> <p class="help-text"> - Add multiple data items to build the option payload. Each item will be encoded sequentially. + {$t('tools/freeform-tlv-builder.dataItems.hint')} </p> </div> <div class="card-content items-container"> {#each option.items as item, i (item.id)} <div class="item-card"> <div class="item-header"> - <h4>Item {i + 1}</h4> + <h4>{$t('tools/freeform-tlv-builder.dataItems.item', { number: i + 1 })}</h4> <button type="button" class="btn-icon btn-remove" @@ -225,7 +274,7 @@ <div class="input-group"> <label for="datatype-{item.id}"> <Icon name="binary" size="sm" /> - Data Type + {$t('tools/freeform-tlv-builder.dataItems.dataType')} </label> <select id="datatype-{item.id}" bind:value={item.dataType}> {#each dataTypeOptions as typeOption (typeOption.value)} @@ -240,7 +289,7 @@ <div class="input-group"> <label for="value-{item.id}"> <Icon name="edit" size="sm" /> - Value + {$t('tools/freeform-tlv-builder.dataItems.value')} </label> <input id="value-{item.id}" @@ -254,14 +303,14 @@ <button type="button" class="btn-add" onclick={addItem}> <Icon name="plus" size="sm" /> - Add Data Item + {$t('tools/freeform-tlv-builder.dataItems.addButton')} </button> </div> </div> {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/freeform-tlv-builder.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -273,18 +322,30 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Encoded Option</h3> + <h3>{$t('tools/freeform-tlv-builder.results.title')}</h3> <div class="summary-card"> - <div><strong>Option Code:</strong> {result.option.optionCode}</div> - <div><strong>Option Name:</strong> {result.option.optionName}</div> - <div><strong>Data Length:</strong> {result.dataLength} bytes</div> - <div><strong>Items:</strong> {result.option.items.length}</div> + <div> + <strong>{$t('tools/freeform-tlv-builder.results.summary.optionCode')}</strong> + {result.option.optionCode} + </div> + <div> + <strong>{$t('tools/freeform-tlv-builder.results.summary.optionName')}</strong> + {result.option.optionName} + </div> + <div> + <strong>{$t('tools/freeform-tlv-builder.results.summary.dataLength')}</strong> + {$t('tools/freeform-tlv-builder.results.summary.bytes', { length: result.dataLength })} + </div> + <div> + <strong>{$t('tools/freeform-tlv-builder.results.summary.items')}</strong> + {result.option.items.length} + </div> </div> <div class="output-group"> <div class="output-header"> - <h4>Hex-Encoded (Compact)</h4> + <h4>{$t('tools/freeform-tlv-builder.results.hexEncoded')}</h4> <button type="button" class="copy-btn" @@ -292,7 +353,9 @@ onclick={() => clipboard.copy(result!.hexEncoded, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/freeform-tlv-builder.common.copied') + : $t('tools/freeform-tlv-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.hexEncoded}</pre> @@ -300,7 +363,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Wire Format (Spaced)</h4> + <h4>{$t('tools/freeform-tlv-builder.results.wireFormat')}</h4> <button type="button" class="copy-btn" @@ -308,7 +371,9 @@ onclick={() => clipboard.copy(result!.wireFormat, 'wire')} > <Icon name={clipboard.isCopied('wire') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') + ? $t('tools/freeform-tlv-builder.common.copied') + : $t('tools/freeform-tlv-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.wireFormat}</pre> @@ -316,7 +381,7 @@ {#if result.breakdown.length > 0} <div class="breakdown-section"> - <h4>Byte Breakdown</h4> + <h4>{$t('tools/freeform-tlv-builder.results.byteBreakdown')}</h4> {#each result.breakdown as item, i (i)} <div class="breakdown-item"> <div class="breakdown-label">{item.label}</div> @@ -329,12 +394,12 @@ </div> <div class="card results"> - <h3>Configuration Examples</h3> + <h3>{$t('tools/freeform-tlv-builder.results.configExamples')}</h3> {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration</h4> + <h4>{$t('tools/freeform-tlv-builder.results.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -342,7 +407,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/freeform-tlv-builder.common.copied') + : $t('tools/freeform-tlv-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -352,7 +419,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration</h4> + <h4>{$t('tools/freeform-tlv-builder.results.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -360,7 +427,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/freeform-tlv-builder.common.copied') + : $t('tools/freeform-tlv-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -369,22 +438,38 @@ </div> <div class="card results info-card"> - <h3>About TLV Encoding</h3> + <h3>{$t('tools/freeform-tlv-builder.about.title')}</h3> <p> - Type-Length-Value (TLV) is a common encoding scheme used in DHCP options. This tool allows you to compose custom - DHCP options by combining multiple data items of different types. + {$t('tools/freeform-tlv-builder.about.intro')} </p> <ul> - <li><strong>IPv4/IPv6:</strong> Network addresses encoded as raw bytes</li> - <li><strong>FQDN:</strong> Domain names in DNS wire format (length-prefixed labels)</li> - <li><strong>String:</strong> UTF-8 encoded text</li> - <li><strong>UInt8/16/32:</strong> Unsigned integers of various sizes</li> - <li><strong>Boolean:</strong> Single byte (0x00 or 0x01)</li> - <li><strong>Hex:</strong> Raw hexadecimal bytes for custom data</li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.ipv4Ipv6')}</strong> + {$t('tools/freeform-tlv-builder.about.types.ipv4Ipv6Desc')} + </li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.fqdn')}</strong> + {$t('tools/freeform-tlv-builder.about.types.fqdnDesc')} + </li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.string')}</strong> + {$t('tools/freeform-tlv-builder.about.types.stringDesc')} + </li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.uintTypes')}</strong> + {$t('tools/freeform-tlv-builder.about.types.uintTypesDesc')} + </li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.boolean')}</strong> + {$t('tools/freeform-tlv-builder.about.types.booleanDesc')} + </li> + <li> + <strong>{$t('tools/freeform-tlv-builder.about.types.hex')}</strong> + {$t('tools/freeform-tlv-builder.about.types.hexDesc')} + </li> </ul> <p> - The generated hex output represents the option data only. DHCP servers will automatically add the option code - and length fields when sending the option to clients. + {$t('tools/freeform-tlv-builder.about.outro')} </p> </div> {/if} diff --git a/src/lib/components/tools/GatewayOption3.svelte b/src/lib/components/tools/GatewayOption3.svelte index a370e19c..690d2833 100644 --- a/src/lib/components/tools/GatewayOption3.svelte +++ b/src/lib/components/tools/GatewayOption3.svelte @@ -11,9 +11,17 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables/useClipboard.svelte'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; const clipboard = useClipboard(); + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools.gateway-option3'); + }); + type Tab = 'build' | 'decode'; let activeTab = $state<Tab>('build'); @@ -28,17 +36,33 @@ let decodeResult = $state<GatewayResult | null>(null); let decodeError = $state<string>(''); - const navOptions = [ - { value: 'build', label: 'Build Option' }, - { value: 'decode', label: 'Decode Option' }, - ]; - - const decodeExamples = [ - { label: 'Single Gateway', hexValue: 'c0a80101', description: '192.168.1.1' }, - { label: 'Dual Gateways', hexValue: 'c0a80101c0a80102', description: '192.168.1.1, 192.168.1.2' }, - { label: 'Google DNS Primary', hexValue: '08080808', description: '8.8.8.8' }, - { label: 'Common Home Router', hexValue: 'c0a8000a', description: '192.168.0.10' }, - ]; + const navOptions = $derived([ + { value: 'build', label: $t('tools/gateway-option3.nav.build') }, + { value: 'decode', label: $t('tools/gateway-option3.nav.decode') }, + ]); + + const decodeExamples = $derived([ + { + label: $t('tools/gateway-option3.examples.decode.singleGateway.label'), + hexValue: 'c0a80101', + description: $t('tools/gateway-option3.examples.decode.singleGateway.description'), + }, + { + label: $t('tools/gateway-option3.examples.decode.dualGateways.label'), + hexValue: 'c0a80101c0a80102', + description: $t('tools/gateway-option3.examples.decode.dualGateways.description'), + }, + { + label: $t('tools/gateway-option3.examples.decode.googleDNS.label'), + hexValue: '08080808', + description: $t('tools/gateway-option3.examples.decode.googleDNS.description'), + }, + { + label: $t('tools/gateway-option3.examples.decode.commonRouter.label'), + hexValue: 'c0a8000a', + description: $t('tools/gateway-option3.examples.decode.commonRouter.description'), + }, + ]); function loadExample(example: (typeof GATEWAY_EXAMPLES)[0]) { activeTab = 'build'; @@ -122,8 +146,8 @@ </script> <ToolContentContainer - title="DHCP Option 3 - Router/Default Gateway" - description="Build and decode default gateway configuration. Multiple gateways can be specified for redundancy or load balancing, listed in order of preference." + title={$t('tools/gateway-option3.title')} + description={$t('tools/gateway-option3.subtitle')} {navOptions} bind:selectedNav={activeTab} > @@ -136,37 +160,47 @@ /> <div class="card input-card"> - <h3>Gateway Configuration</h3> + <h3>{$t('tools/gateway-option3.build.title')}</h3> <div class="form-group"> - <label for="subnet">Subnet (Optional - for validation)</label> - <input id="subnet" type="text" bind:value={subnet} placeholder="e.g., 192.168.1.0/24" class="input" /> - <span class="hint">If provided, gateways will be validated against this subnet</span> + <label for="subnet">{$t('tools/gateway-option3.build.subnet.label')}</label> + <input + id="subnet" + type="text" + bind:value={subnet} + placeholder={$t('tools/gateway-option3.build.subnet.placeholder')} + class="input" + /> + <span class="hint">{$t('tools/gateway-option3.build.subnet.hint')}</span> </div> <div class="form-group"> - <label for="gateway-0">Gateway Addresses (in order of preference)</label> + <label for="gateway-0">{$t('tools/gateway-option3.build.gateways.label')}</label> {#each gateways as _gateway, i (i)} <div class="gateway-row"> <input id={i === 0 ? 'gateway-0' : undefined} type="text" bind:value={gateways[i]} - placeholder="e.g., 192.168.1.1" + placeholder={$t('tools/gateway-option3.build.gateways.placeholder')} class="input" aria-label={i > 0 ? `Gateway ${i + 1}` : undefined} /> {#if gateways.length > 1} - <button class="btn btn-danger btn-sm" onclick={() => removeGateway(i)}>Remove</button> + <button class="btn btn-danger btn-sm" onclick={() => removeGateway(i)} + >{$t('tools/gateway-option3.build.gateways.removeButton')}</button + > {/if} </div> {/each} - <button class="btn btn-secondary btn-sm" onclick={addGateway}>Add Gateway</button> + <button class="btn btn-secondary btn-sm" onclick={addGateway} + >{$t('tools/gateway-option3.build.gateways.addButton')}</button + > </div> {#if buildErrors.length > 0} <div class="error-card"> - <strong>Validation Errors:</strong> + <strong>{$t('tools/gateway-option3.build.errors.title')}</strong> <ul> {#each buildErrors as error, i (i)} <li>{error}</li> @@ -178,11 +212,11 @@ {#if buildResult} <div class="card result-card"> - <h3>Option 3 - Router</h3> + <h3>{$t('tools/gateway-option3.results.buildTitle')}</h3> <div class="result-grid"> <div class="result-item"> - <span class="label">Gateways:</span> + <span class="label">{$t('tools/gateway-option3.results.gateways')}</span> <div class="gateway-list"> {#each buildResult.gateways as gw, i (i)} <span class="gateway-badge"> @@ -193,7 +227,7 @@ </div> <div class="result-item"> - <span class="label">Hex Encoded:</span> + <span class="label">{$t('tools/gateway-option3.results.hexEncoded')}</span> <code class="code-value">{buildResult.hexEncoded}</code> <button class="btn-copy" @@ -201,12 +235,14 @@ onclick={() => clipboard.copy(buildResult!.hexEncoded, 'hex')} aria-label="Copy hex" > - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <div class="result-item"> - <span class="label">Wire Format:</span> + <span class="label">{$t('tools/gateway-option3.results.wireFormat')}</span> <code class="code-value">{buildResult.wireFormat}</code> <button class="btn-copy" @@ -214,28 +250,34 @@ onclick={() => clipboard.copy(buildResult!.wireFormat, 'wire')} aria-label="Copy wire format" > - {clipboard.isCopied('wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('wire') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{buildResult.totalLength} bytes</span> + <span class="label">{$t('tools/gateway-option3.results.totalLength')}</span> + <span class="value" + >{$t('tools/gateway-option3.results.lengthBytes', { length: buildResult.totalLength })}</span + > </div> </div> <div class="config-section"> - <h4>Configuration Examples</h4> + <h4>{$t('tools/gateway-option3.results.configExamples')}</h4> <div class="output-group"> <div class="output-header"> - <h5>ISC DHCPd</h5> + <h5>{$t('tools/gateway-option3.results.iscDhcpd')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('isc')} onclick={() => clipboard.copy(buildResult!.configExamples.iscDhcpd, 'isc')} > - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.iscDhcpd}</code></pre> @@ -243,13 +285,15 @@ <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv4</h5> + <h5>{$t('tools/gateway-option3.results.keaDhcp4')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('kea')} onclick={() => clipboard.copy(buildResult!.configExamples.keaDhcp4, 'kea')} > - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.keaDhcp4}</code></pre> @@ -257,13 +301,15 @@ <div class="output-group"> <div class="output-header"> - <h5>dnsmasq</h5> + <h5>{$t('tools/gateway-option3.results.dnsmasq')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('dnsmasq')} onclick={() => clipboard.copy(buildResult!.configExamples.dnsmasq, 'dnsmasq')} > - {clipboard.isCopied('dnsmasq') ? 'Copied' : 'Copy'} + {clipboard.isCopied('dnsmasq') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.dnsmasq}</code></pre> @@ -280,23 +326,23 @@ /> <div class="card input-card"> - <h3>Decode Option 3</h3> + <h3>{$t('tools/gateway-option3.decode.title')}</h3> <div class="form-group"> - <label for="hex-input">Hex String</label> + <label for="hex-input">{$t('tools/gateway-option3.decode.hexInput.label')}</label> <textarea id="hex-input" bind:value={hexInput} - placeholder="e.g., c0a80101 or c0 a8 01 01" + placeholder={$t('tools/gateway-option3.decode.hexInput.placeholder')} rows="3" class="input" ></textarea> - <span class="hint">Enter hex bytes (spaces optional)</span> + <span class="hint">{$t('tools/gateway-option3.decode.hexInput.hint')}</span> </div> {#if decodeError} <div class="error-card"> - <strong>Decode Error:</strong> + <strong>{$t('tools/gateway-option3.decode.error.title')}</strong> <p>{decodeError}</p> </div> {/if} @@ -304,11 +350,11 @@ {#if decodeResult} <div class="card result-card"> - <h3>Decoded Option 3</h3> + <h3>{$t('tools/gateway-option3.results.decodeTitle')}</h3> <div class="result-grid"> <div class="result-item"> - <span class="label">Gateways:</span> + <span class="label">{$t('tools/gateway-option3.results.gateways')}</span> <div class="gateway-list"> {#each decodeResult.gateways as gw, i (i)} <span class="gateway-badge"> @@ -319,28 +365,32 @@ </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{decodeResult.totalLength} bytes</span> + <span class="label">{$t('tools/gateway-option3.results.totalLength')}</span> + <span class="value" + >{$t('tools/gateway-option3.results.lengthBytes', { length: decodeResult.totalLength })}</span + > </div> <div class="result-item"> - <span class="label">Gateway Count:</span> + <span class="label">{$t('tools/gateway-option3.results.gatewayCount')}</span> <span class="value">{decodeResult.gateways.length}</span> </div> </div> <div class="config-section"> - <h4>Configuration Examples</h4> + <h4>{$t('tools/gateway-option3.results.configExamples')}</h4> <div class="output-group"> <div class="output-header"> - <h5>ISC DHCPd</h5> + <h5>{$t('tools/gateway-option3.results.iscDhcpd')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-isc')} onclick={() => clipboard.copy(decodeResult!.configExamples.iscDhcpd, 'decode-isc')} > - {clipboard.isCopied('decode-isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-isc') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.iscDhcpd}</code></pre> @@ -348,13 +398,15 @@ <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv4</h5> + <h5>{$t('tools/gateway-option3.results.keaDhcp4')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-kea')} onclick={() => clipboard.copy(decodeResult!.configExamples.keaDhcp4, 'decode-kea')} > - {clipboard.isCopied('decode-kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-kea') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.keaDhcp4}</code></pre> @@ -362,13 +414,15 @@ <div class="output-group"> <div class="output-header"> - <h5>dnsmasq</h5> + <h5>{$t('tools/gateway-option3.results.dnsmasq')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-dnsmasq')} onclick={() => clipboard.copy(decodeResult!.configExamples.dnsmasq, 'decode-dnsmasq')} > - {clipboard.isCopied('decode-dnsmasq') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-dnsmasq') + ? $t('tools/gateway-option3.buttons.copied') + : $t('tools/gateway-option3.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.dnsmasq}</code></pre> diff --git a/src/lib/components/tools/IAIDCalculator.svelte b/src/lib/components/tools/IAIDCalculator.svelte index 845494b6..de0fd5d4 100644 --- a/src/lib/components/tools/IAIDCalculator.svelte +++ b/src/lib/components/tools/IAIDCalculator.svelte @@ -12,6 +12,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let method = $state<'interface-index' | 'interface-name' | 'mac-address' | 'custom'>('interface-index'); let interfaceIndex = $state<number | undefined>(undefined); @@ -114,10 +115,7 @@ }); </script> -<ToolContentContainer - title="IAID Calculator" - description="Calculate Identity Association Identifier (IAID) for DHCPv6 interfaces. Generate IAIDs from interface index, name, MAC address, or custom values with OS-specific conventions." -> +<ToolContentContainer title={$t('tools/iaid-calculator.title')} description={$t('tools/iaid-calculator.subtitle')}> <ExamplesCard {examples} onSelect={loadExample} @@ -128,20 +126,20 @@ <div class="card input-card"> <div class="card-header"> - <h3>IAID Configuration</h3> - <p class="help-text">Select method to generate Identity Association Identifier</p> + <h3>{$t('tools/iaid-calculator.config.title')}</h3> + <p class="help-text">{$t('tools/iaid-calculator.config.hint')}</p> </div> <div class="card-content"> <div class="input-group"> <label for="method"> <Icon name="settings" size="sm" /> - Generation Method + {$t('tools/iaid-calculator.config.method.label')} </label> <select id="method" bind:value={method}> - <option value="interface-index">Interface Index</option> - <option value="interface-name">Interface Name (hash)</option> - <option value="mac-address">MAC Address (hash)</option> - <option value="custom">Custom Value</option> + <option value="interface-index">{$t('tools/iaid-calculator.config.method.options.interfaceIndex')}</option> + <option value="interface-name">{$t('tools/iaid-calculator.config.method.options.interfaceName')}</option> + <option value="mac-address">{$t('tools/iaid-calculator.config.method.options.macAddress')}</option> + <option value="custom">{$t('tools/iaid-calculator.config.method.options.custom')}</option> </select> </div> @@ -149,17 +147,17 @@ <div class="input-group"> <label for="interface-index"> <Icon name="hash" size="sm" /> - Interface Index + {$t('tools/iaid-calculator.config.interfaceIndex.label')} </label> <input id="interface-index" type="number" bind:value={interfaceIndex} - placeholder="e.g., 2 for eth0, 3 for wlan0" + placeholder={$t('tools/iaid-calculator.config.interfaceIndex.placeholder')} min="0" max="4294967295" /> - <small>Network interface index (0-4294967295)</small> + <small>{$t('tools/iaid-calculator.config.interfaceIndex.hint')}</small> </div> {/if} @@ -167,10 +165,15 @@ <div class="input-group"> <label for="interface-name"> <Icon name="network" size="sm" /> - Interface Name + {$t('tools/iaid-calculator.config.interfaceName.label')} </label> - <input id="interface-name" type="text" bind:value={interfaceName} placeholder="e.g., eth0, wlan0, enp3s0" /> - <small>Network interface name (will be hashed to generate IAID)</small> + <input + id="interface-name" + type="text" + bind:value={interfaceName} + placeholder={$t('tools/iaid-calculator.config.interfaceName.placeholder')} + /> + <small>{$t('tools/iaid-calculator.config.interfaceName.hint')}</small> </div> {/if} @@ -178,10 +181,15 @@ <div class="input-group"> <label for="mac-address"> <Icon name="cpu" size="sm" /> - MAC Address + {$t('tools/iaid-calculator.config.macAddress.label')} </label> - <input id="mac-address" type="text" bind:value={macAddress} placeholder="00:0c:29:4f:a3:d2" /> - <small>Hardware address (last 4 bytes used for IAID)</small> + <input + id="mac-address" + type="text" + bind:value={macAddress} + placeholder={$t('tools/iaid-calculator.config.macAddress.placeholder')} + /> + <small>{$t('tools/iaid-calculator.config.macAddress.hint')}</small> </div> {/if} @@ -189,17 +197,17 @@ <div class="input-group"> <label for="custom-value"> <Icon name="edit" size="sm" /> - Custom IAID Value + {$t('tools/iaid-calculator.config.customValue.label')} </label> <input id="custom-value" type="number" bind:value={customValue} - placeholder="Enter value between 0 and 4294967295" + placeholder={$t('tools/iaid-calculator.config.customValue.placeholder')} min="0" max="4294967295" /> - <small>32-bit unsigned integer (0-4294967295)</small> + <small>{$t('tools/iaid-calculator.config.customValue.hint')}</small> </div> {/if} </div> @@ -207,7 +215,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/iaid-calculator.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -219,11 +227,11 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Calculated IAID</h3> + <h3>{$t('tools/iaid-calculator.results.title')}</h3> <div class="summary-card"> - <div><strong>Method:</strong> {result.method}</div> - <div><strong>IAID:</strong> {result.iaid}</div> + <div><strong>{$t('tools/iaid-calculator.results.summary.method')}</strong> {result.method}</div> + <div><strong>{$t('tools/iaid-calculator.results.summary.iaid')}</strong> {result.iaid}</div> </div> {#if result.collisionWarning} @@ -235,7 +243,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Hexadecimal</h4> + <h4>{$t('tools/iaid-calculator.results.hex')}</h4> <button type="button" class="copy-btn" @@ -243,7 +251,9 @@ onclick={() => clipboard.copy(result!.hex, 'hex')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('hex') + ? $t('tools/iaid-calculator.common.copied') + : $t('tools/iaid-calculator.common.copy')} </button> </div> <pre class="output-value code-block">{result.hex}</pre> @@ -251,7 +261,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Decimal</h4> + <h4>{$t('tools/iaid-calculator.results.decimal')}</h4> <button type="button" class="copy-btn" @@ -259,7 +269,9 @@ onclick={() => clipboard.copy(result!.decimal, 'decimal')} > <Icon name={clipboard.isCopied('decimal') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('decimal') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decimal') + ? $t('tools/iaid-calculator.common.copied') + : $t('tools/iaid-calculator.common.copy')} </button> </div> <pre class="output-value code-block">{result.decimal}</pre> @@ -267,7 +279,7 @@ <div class="output-group"> <div class="output-header"> - <h4>Binary</h4> + <h4>{$t('tools/iaid-calculator.results.binary')}</h4> <button type="button" class="copy-btn" @@ -275,7 +287,9 @@ onclick={() => clipboard.copy(result!.binary, 'binary')} > <Icon name={clipboard.isCopied('binary') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('binary') ? 'Copied' : 'Copy'} + {clipboard.isCopied('binary') + ? $t('tools/iaid-calculator.common.copied') + : $t('tools/iaid-calculator.common.copy')} </button> </div> <pre class="output-value code-block">{result.binary}</pre> @@ -283,8 +297,8 @@ </div> <div class="card results"> - <h3>OS-Specific Conventions</h3> - <p class="help-text">How different operating systems typically generate IAIDs</p> + <h3>{$t('tools/iaid-calculator.results.osConventions.title')}</h3> + <p class="help-text">{$t('tools/iaid-calculator.results.osConventions.hint')}</p> <div class="os-conventions"> {#each [{ icon: 'linux', name: 'Linux', text: result.osConventions.linux }, { icon: 'windows', name: 'Windows', text: result.osConventions.windows }, { icon: 'mac', name: 'macOS', text: result.osConventions.macos }, { icon: 'bsd', name: 'FreeBSD', text: result.osConventions.freebsd }] as os (os.name)} @@ -301,7 +315,7 @@ </div> </div> - {#each [{ title: 'Kea DHCPv6 Configuration', content: result?.configExamples?.keaDhcp6, key: 'kea' }, { title: 'ISC DHCPd Configuration', content: result?.configExamples?.iscDhcpd, key: 'isc' }] as config (config.key)} + {#each [{ title: $t('tools/iaid-calculator.results.configExamples.keaDhcp6'), content: result?.configExamples?.keaDhcp6, key: 'kea' }, { title: $t('tools/iaid-calculator.results.configExamples.iscDhcpd'), content: result?.configExamples?.iscDhcpd, key: 'isc' }] as config (config.key)} {#if config.content} <div class="card results"> <div class="card-header-with-action"> @@ -313,7 +327,9 @@ onclick={() => clipboard.copy(config.content!, config.key)} > <Icon name={clipboard.isCopied(config.key) ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied(config.key) ? 'Copied' : 'Copy'} + {clipboard.isCopied(config.key) + ? $t('tools/iaid-calculator.common.copied') + : $t('tools/iaid-calculator.common.copy')} </button> </div> <pre class="output-value code-block">{config.content}</pre> @@ -323,8 +339,8 @@ {/if} <div class="card naming-guide-wrap"> - <h3>Network Interface Naming Guide</h3> - <p class="help-text">Common interface naming conventions across different operating systems</p> + <h3>{$t('tools/iaid-calculator.namingGuide.title')}</h3> + <p class="help-text">{$t('tools/iaid-calculator.namingGuide.hint')}</p> <div class="naming-guide"> {#each [{ icon: 'linux', data: INTERFACE_NAMING_GUIDE.linux }, { icon: 'windows', data: INTERFACE_NAMING_GUIDE.windows }, { icon: 'mac', data: INTERFACE_NAMING_GUIDE.macos }, { icon: 'bsd', data: INTERFACE_NAMING_GUIDE.freebsd }] as osGuide (osGuide.data.title)} diff --git a/src/lib/components/tools/IDNPunycodeConverter.svelte b/src/lib/components/tools/IDNPunycodeConverter.svelte index a09096e4..2e711295 100644 --- a/src/lib/components/tools/IDNPunycodeConverter.svelte +++ b/src/lib/components/tools/IDNPunycodeConverter.svelte @@ -1,7 +1,15 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import { Copy, Download, Check, Globe, Type } from 'lucide-svelte'; import { tooltip } from '$lib/actions/tooltip.js'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools/idn-punycode-converter'); + }); let inputText = $state(''); let mode = $state('unicode-to-punycode'); @@ -9,14 +17,38 @@ const clipboard = useClipboard(); - const domainExamples = [ - { unicode: 'mΓΌnchen.de', punycode: 'xn--mnchen-3ya.de', description: 'German city domain' }, - { unicode: 'ζ—₯本.jp', punycode: 'xn--wgbl6a.jp', description: 'Japanese domain' }, - { unicode: 'россия.Ρ€Ρ„', punycode: 'xn--h1alffa9f.xn--p1ai', description: 'Russian domain' }, - { unicode: 'Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ©.net', punycode: 'xn--mgbah1a3hjkrd.net', description: 'Arabic domain' }, - { unicode: 'ν•œκ΅­.kr', punycode: 'xn--3e0b707e.kr', description: 'Korean domain' }, - { unicode: 'Ρλληνικά.gr', punycode: 'xn--hxajbheg2az3al.gr', description: 'Greek domain' }, - ]; + const domainExamples = $derived([ + { + unicode: 'mΓΌnchen.de', + punycode: 'xn--mnchen-3ya.de', + description: $t('tools.idn-punycode-converter.examples.german'), + }, + { + unicode: 'ζ—₯本.jp', + punycode: 'xn--wgbl6a.jp', + description: $t('tools.idn-punycode-converter.examples.japanese'), + }, + { + unicode: 'россия.Ρ€Ρ„', + punycode: 'xn--h1alffa9f.xn--p1ai', + description: $t('tools.idn-punycode-converter.examples.russian'), + }, + { + unicode: 'Ψ§Ω„ΨΉΨ±Ψ¨ΩŠΨ©.net', + punycode: 'xn--mgbah1a3hjkrd.net', + description: $t('tools.idn-punycode-converter.examples.arabic'), + }, + { + unicode: 'ν•œκ΅­.kr', + punycode: 'xn--3e0b707e.kr', + description: $t('tools.idn-punycode-converter.examples.korean'), + }, + { + unicode: 'Ρλληνικά.gr', + punycode: 'xn--hxajbheg2az3al.gr', + description: $t('tools.idn-punycode-converter.examples.greek'), + }, + ]); // Punycode implementation based on RFC 3492 function punycodeDecode(input: string) { @@ -204,12 +236,16 @@ return convertDomain(inputText.trim(), false); } } catch { - return 'Error: Invalid input'; + return $t('tools.idn-punycode-converter.conversion.error'); } }); let isValid = $derived.by(() => { - return inputText.trim() !== '' && result !== '' && !result.startsWith('Error:'); + return ( + inputText.trim() !== '' && + result !== '' && + !result.startsWith($t('tools.idn-punycode-converter.conversion.error').split(':')[0] + ':') + ); }); let warnings = $derived.by(() => { @@ -217,16 +253,16 @@ if (mode === 'unicode-to-punycode') { if (inputText && !/[\u0080-\uFFFF]/.test(inputText)) { - warns.push('Input contains only ASCII characters - no conversion needed'); + warns.push($t('tools.idn-punycode-converter.warnings.asciiOnly')); } } else { if (inputText && !inputText.includes('xn--')) { - warns.push('Input does not contain punycode domains (xn-- prefix)'); + warns.push($t('tools.idn-punycode-converter.warnings.noPunycode')); } } if (inputText.length > 253) { - warns.push('Domain name exceeds maximum length of 253 characters'); + warns.push($t('tools.idn-punycode-converter.warnings.tooLong')); } return warns; @@ -272,8 +308,8 @@ <div class="idn-converter"> <div class="card"> <div class="card-header"> - <h1>IDN Punycode Converter</h1> - <p>Convert between Unicode domain names and Punycode (ASCII-compatible encoding)</p> + <h1>{$t('tools.idn-punycode-converter.title')}</h1> + <p>{$t('tools.idn-punycode-converter.description')}</p> </div> <div class="card-content"> @@ -286,7 +322,7 @@ onclick={() => (mode = 'unicode-to-punycode')} > <Globe size="16" /> - Unicode β†’ Punycode + {$t('tools.idn-punycode-converter.modes.unicodeToPunycode')} </button> <button class="toggle-btn" @@ -294,7 +330,7 @@ onclick={() => (mode = 'punycode-to-unicode')} > <Type size="16" /> - Punycode β†’ Unicode + {$t('tools.idn-punycode-converter.modes.punycodeToUnicode')} </button> </div> </div> @@ -304,7 +340,7 @@ <summary> <div class="summary-content"> <Globe size="20" /> - <span>Example Domains</span> + <span>{$t('examples.title')}</span> </div> <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path> @@ -326,7 +362,7 @@ <div class="conversion-card"> <div class="conversion-header"> <h2> - {mode === 'unicode-to-punycode' ? 'Unicode β†’ Punycode Conversion' : 'Punycode β†’ Unicode Conversion'} + {mode === 'unicode-to-punycode' ? $t('conversion.unicodeTitle') : $t('conversion.punycodeTitle')} </h2> </div> @@ -334,20 +370,20 @@ <!-- Input --> <div class="input-card"> <div class="input-section"> - <label for="input" use:tooltip={'Enter the domain name you want to convert'}> - {mode === 'unicode-to-punycode' ? 'Unicode Domain Name' : 'Punycode Domain Name'} + <label for="input" use:tooltip={$t('conversion.unicodeTooltip')}> + {mode === 'unicode-to-punycode' ? $t('conversion.unicodeLabel') : $t('conversion.punycodeLabel')} </label> <textarea id="input" bind:value={inputText} rows="4" - placeholder={mode === 'unicode-to-punycode' ? 'mΓΌnchen.de' : 'xn--mnchen-3ya.de'} + placeholder={mode === 'unicode-to-punycode' + ? $t('conversion.unicodePlaceholder') + : $t('conversion.punycodePlaceholder')} class:mono-font={mode === 'punycode-to-unicode'} ></textarea> <small> - {mode === 'unicode-to-punycode' - ? 'Enter Unicode domain names with international characters' - : 'Enter domain names containing xn-- punycode labels'} + {mode === 'unicode-to-punycode' ? $t('conversion.unicodeHelp') : $t('conversion.punycodeHelp')} </small> </div> </div> @@ -357,10 +393,10 @@ <div class="output-section"> <div class="output-header"> <h3> - {mode === 'unicode-to-punycode' ? 'Punycode Result' : 'Unicode Result'} + {mode === 'unicode-to-punycode' ? $t('conversion.punycodeResult') : $t('conversion.unicodeResult')} </h3> {#if isValid} - <button onclick={swapModeAndContent} class="swap-btn" use:tooltip={'Swap input and output'}> + <button onclick={swapModeAndContent} class="swap-btn" use:tooltip={$t('conversion.swapTooltip')}> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"> <path stroke-linecap="round" @@ -369,23 +405,23 @@ d="M8 7h12m0 0l-4-4m4 4l-4 4m0 6H4m0 0l4 4m-4-4l4-4" ></path> </svg> - Reverse + {$t('conversion.reverse')} </button> {/if} </div> <div class="output-display"> {#if isValid} <pre class="result-text" class:mono-font={mode === 'unicode-to-punycode'}>{result}</pre> - {:else if result.startsWith('Error:')} + {:else if result.startsWith($t('tools.idn-punycode-converter.conversion.error').split(':')[0] + ':')} <p class="error-text">{result}</p> {:else} - <p class="placeholder-text">Enter a domain name to see the conversion result</p> + <p class="placeholder-text">{$t('conversion.placeholder')}</p> {/if} </div> {#if warnings.length > 0} <div class="alert alert-warning"> - <h4>Notices</h4> + <h4>{$t('warnings.title')}</h4> <ul> {#each warnings as warning (warning)} <li>{warning}</li> @@ -404,10 +440,10 @@ > {#if clipboard.isCopied('copy')} <Check size="16" /> - Copied! + {$t('actions.copied')} {:else} <Copy size="16" /> - Copy Result + {$t('actions.copyResult')} {/if} </button> <button @@ -418,10 +454,10 @@ > {#if clipboard.isCopied('download')} <Check size="16" /> - Downloaded! + {$t('actions.downloaded')} {:else} <Download size="16" /> - Download + {$t('actions.download')} {/if} </button> </div> @@ -434,55 +470,53 @@ <!-- Information Cards --> <div class="info-section"> <div class="card info-card"> - <h3>About IDN and Punycode</h3> + <h3>{$t('info.aboutTitle')}</h3> <p> - Internationalized Domain Names (IDN) allow domain names to contain Unicode characters from various scripts - and languages. Punycode is the ASCII-compatible encoding that represents Unicode domain labels, allowing - non-ASCII domain names to work with existing DNS infrastructure. + {$t('info.aboutDescription')} </p> </div> <div class="info-grid"> <div class="card"> - <h4>How Punycode Works</h4> + <h4>{$t('info.howItWorksTitle')}</h4> <div class="punycode-example"> - <p>Each Unicode label is encoded separately:</p> + <p>{$t('info.howItWorksDescription')}</p> <div class="code-example"> - <div><strong>Unicode:</strong> <span class="unicode-text">mΓΌnchen</span></div> - <div><strong>Encoded:</strong> <span class="encoded-text">mnchen-3ya</span></div> - <div><strong>Final:</strong> <span class="final-text">xn--mnchen-3ya</span></div> + <div><strong>{$t('info.unicode')}</strong> <span class="unicode-text">mΓΌnchen</span></div> + <div><strong>{$t('info.encoded')}</strong> <span class="encoded-text">mnchen-3ya</span></div> + <div><strong>{$t('info.final')}</strong> <span class="final-text">xn--mnchen-3ya</span></div> </div> - <small>The <code>xn--</code> prefix identifies punycode labels</small> + <small>{$t('info.xnPrefix')}</small> </div> </div> <div class="card"> - <h4>Common Use Cases</h4> + <h4>{$t('info.useCasesTitle')}</h4> <ul class="use-cases"> - <li>International domain registration</li> - <li>Email address internationalization</li> - <li>DNS configuration</li> - <li>Web application development</li> - <li>Security analysis</li> + <li>{$t('info.useCases.registration')}</li> + <li>{$t('info.useCases.email')}</li> + <li>{$t('info.useCases.dns')}</li> + <li>{$t('info.useCases.webDev')}</li> + <li>{$t('info.useCases.security')}</li> </ul> </div> <div class="card"> - <h4>Supported Features</h4> + <h4>{$t('info.featuresTitle')}</h4> <ul class="features"> - <li>RFC 3492 compliant Punycode encoding/decoding</li> - <li>Bidirectional conversion (Unicode ↔ Punycode)</li> - <li>Multiple domain labels support</li> - <li>Mixed ASCII/Unicode domain handling</li> + <li>{$t('info.features.compliant')}</li> + <li>{$t('info.features.bidirectional')}</li> + <li>{$t('info.features.multipleLabels')}</li> + <li>{$t('info.features.mixed')}</li> </ul> </div> <div class="card security-card"> - <h4>Security Considerations</h4> + <h4>{$t('info.securityTitle')}</h4> <ul class="security-list"> - <li><strong>Homograph attacks:</strong> visually similar characters from different scripts</li> - <li>Always validate and normalize IDN input in applications</li> - <li>Consider implementing mixed-script detection</li> - <li>Be aware of browser IDN display policies</li> + <li><strong>{$t('info.security.homograph')}</strong></li> + <li>{$t('info.security.validate')}</li> + <li>{$t('info.security.mixedScript')}</li> + <li>{$t('info.security.browserPolicy')}</li> </ul> </div> </div> @@ -950,18 +984,6 @@ color: var(--color-success); } } - - small { - color: var(--text-secondary); - font-size: var(--font-size-xs); - - code { - background: var(--bg-tertiary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-xs); - font-size: var(--font-size-xs); - } - } } .use-cases, diff --git a/src/lib/components/tools/IPConverter.svelte b/src/lib/components/tools/IPConverter.svelte index 622ddff5..67d7a7d4 100644 --- a/src/lib/components/tools/IPConverter.svelte +++ b/src/lib/components/tools/IPConverter.svelte @@ -5,6 +5,7 @@ import Tooltip from '$lib/components/global/Tooltip.svelte'; import Icon from '../global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let ipAddress = $state('192.168.1.1'); let formats = $state({ @@ -44,12 +45,12 @@ const decimal = parseInt(value); if (isNaN(decimal)) { - formatErrors = { ...formatErrors, decimal: 'Must be a valid number' }; + formatErrors = { ...formatErrors, decimal: $t('tools/ip-converter.errors.mustBeValidNumber') }; return; } if (decimal < 0 || decimal > 4294967295) { - formatErrors = { ...formatErrors, decimal: 'Must be between 0 and 4,294,967,295' }; + formatErrors = { ...formatErrors, decimal: $t('tools/ip-converter.errors.decimalOutOfRange') }; return; } @@ -57,7 +58,7 @@ ipAddress = decimalToIP(decimal); formatErrors = { ...formatErrors, decimal: '' }; } catch (err) { - formatErrors = { ...formatErrors, decimal: 'Invalid decimal value' }; + formatErrors = { ...formatErrors, decimal: $t('tools/ip-converter.errors.invalidDecimal') }; console.error('Invalid decimal conversion:', err); } } @@ -79,26 +80,26 @@ const binaryDigits = cleanBinary.replace(/[.\s]/g, ''); if (cleanBinary !== value) { - formatErrors = { ...formatErrors, binary: 'Only 0, 1, dots, and spaces allowed' }; + formatErrors = { ...formatErrors, binary: $t('tools/ip-converter.errors.binaryOnlyDigits') }; return; } if (binaryDigits.length !== 32) { - formatErrors = { ...formatErrors, binary: 'Must be exactly 32 binary digits (8 digits per octet)' }; + formatErrors = { ...formatErrors, binary: $t('tools/ip-converter.errors.binaryMust32Bits') }; return; } // Validate octet structure (should be 8.8.8.8 format) const parts = cleanBinary.split('.'); if (parts.length !== 4) { - formatErrors = { ...formatErrors, binary: 'Must use dotted format: 8bits.8bits.8bits.8bits' }; + formatErrors = { ...formatErrors, binary: $t('tools/ip-converter.errors.binaryDottedFormat') }; return; } for (let i = 0; i < parts.length; i++) { const part = parts[i].replace(/\s/g, ''); if (part.length !== 8) { - formatErrors = { ...formatErrors, binary: `Octet ${i + 1} must be exactly 8 bits` }; + formatErrors = { ...formatErrors, binary: $t('tools/ip-converter.errors.binaryOctetLength', { octet: i + 1 }) }; return; } } @@ -107,7 +108,7 @@ ipAddress = binaryToIP(cleanBinary); formatErrors = { ...formatErrors, binary: '' }; } catch (err) { - formatErrors = { ...formatErrors, binary: 'Invalid binary format' }; + formatErrors = { ...formatErrors, binary: $t('tools/ip-converter.errors.invalidBinary') }; console.error('Invalid binary conversion:', err); } } @@ -129,19 +130,19 @@ const hexDigits = cleanHex.replace(/[.x]/g, ''); if (cleanHex !== value) { - formatErrors = { ...formatErrors, hex: 'Only hex digits (0-9, A-F), dots, and x allowed' }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.hexOnlyDigits') }; return; } if (hexDigits.length !== 8) { - formatErrors = { ...formatErrors, hex: 'Must be exactly 8 hex digits (2 digits per octet)' }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.hexMust8Digits') }; return; } // Validate format (should be 0xXX.0xXX.0xXX.0xXX or XX.XX.XX.XX) const parts = cleanHex.split('.'); if (parts.length !== 4) { - formatErrors = { ...formatErrors, hex: 'Must use dotted format: 0xXX.0xXX.0xXX.0xXX' }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.hexDottedFormat') }; return; } @@ -154,12 +155,12 @@ } if (hexPart.length !== 2) { - formatErrors = { ...formatErrors, hex: `Octet ${i + 1} must be exactly 2 hex digits` }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.hexOctetLength', { octet: i + 1 }) }; return; } if (!/^[0-9a-fA-F]{2}$/.test(hexPart)) { - formatErrors = { ...formatErrors, hex: `Octet ${i + 1} contains invalid hex digits` }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.hexInvalidDigits', { octet: i + 1 }) }; return; } } @@ -168,7 +169,7 @@ ipAddress = hexToIP(cleanHex); formatErrors = { ...formatErrors, hex: '' }; } catch (err) { - formatErrors = { ...formatErrors, hex: 'Invalid hexadecimal format' }; + formatErrors = { ...formatErrors, hex: $t('tools/ip-converter.errors.invalidHex') }; console.error('Invalid hex conversion:', err); } } @@ -176,31 +177,35 @@ <div class="card"> <header class="card-header"> - <h2>IP Address Converter</h2> - <p>Convert IP addresses between different number formats.</p> + <h2>{$t('tools/ip-converter.title')}</h2> + <p>{$t('tools/ip-converter.description')}</p> </header> <!-- Main IP Input --> <div class="form-group"> - <IPInput bind:value={ipAddress} label="IP Address" placeholder="192.168.1.1" /> + <IPInput + bind:value={ipAddress} + label={$t('tools/ip-converter.input.ipAddress')} + placeholder={$t('tools/ip-converter.input.placeholder')} + /> </div> {#if validateIPv4(ipAddress).valid} <div class="results-section fade-in"> <!-- IP Class Information --> <section class="info-panel info"> - <h3>IP Class Information</h3> + <h3>{$t('tools/ip-converter.ipClasses.title')}</h3> <div class="grid grid-3"> <div class="class-info"> - <span class="info-label">Class</span> + <span class="info-label">{$t('common.labels.type')}</span> <span class="class-value">{ipClass.class}</span> </div> <div class="class-info"> - <span class="info-label">Type</span> + <span class="info-label">{$t('common.labels.type')}</span> <span class="class-value type">{ipClass.type}</span> </div> <div class="class-info"> - <span class="info-label">Usage</span> + <span class="info-label">{$t('common.labels.usage')}</span> <span class="class-description">{ipClass.description}</span> </div> </div> @@ -210,7 +215,7 @@ <div class="grid grid-2 conversions-grid"> <!-- Binary Format --> <div class="format-group"> - <label for="binary-input">Binary Format</label> + <label for="binary-input">{$t('tools/ip-converter.formats.binary.title')}</label> <div class="format-input"> <input id="binary-input" @@ -221,14 +226,16 @@ oninput={handleBinaryInput} /> <Tooltip - text={clipboard.isCopied('binary') ? 'Copied!' : 'Copy binary format to clipboard'} + text={clipboard.isCopied('binary') + ? $t('common.clipboard.copied') + : $t('common.clipboard.copyBinaryFormat')} position="left" > <button type="button" class="copy-btn {clipboard.isCopied('binary') ? 'copied' : ''}" onclick={() => clipboard.copy(formats.binary, 'binary')} - aria-label="Copy binary format to clipboard" + aria-label={$t('common.clipboard.copyBinaryFormat')} > <Icon name={clipboard.isCopied('binary') ? 'check' : 'copy'} size="sm" /> </button> @@ -241,7 +248,7 @@ <!-- Decimal Format --> <div class="format-group"> - <label for="decimal-input">Decimal Format</label> + <label for="decimal-input">{$t('tools/ip-converter.formats.decimal.title')}</label> <div class="format-input"> <input id="decimal-input" @@ -252,14 +259,16 @@ oninput={handleDecimalInput} /> <Tooltip - text={clipboard.isCopied('decimal') ? 'Copied!' : 'Copy decimal format to clipboard'} + text={clipboard.isCopied('decimal') + ? $t('common.clipboard.copied') + : $t('common.clipboard.copyDecimalFormat')} position="left" > <button type="button" class="copy-btn {clipboard.isCopied('decimal') ? 'copied' : ''}" onclick={() => clipboard.copy(formats.decimal, 'decimal')} - aria-label="Copy decimal format to clipboard" + aria-label={$t('common.clipboard.copyDecimalFormat')} > <Icon name={clipboard.isCopied('decimal') ? 'check' : 'copy'} size="sm" /> </button> @@ -272,7 +281,7 @@ <!-- Hexadecimal Format --> <div class="format-group"> - <label for="hex-input">Hexadecimal Format</label> + <label for="hex-input">{$t('tools/ip-converter.formats.hexadecimal.title')}</label> <div class="format-input"> <input id="hex-input" @@ -283,14 +292,14 @@ oninput={handleHexInput} /> <Tooltip - text={clipboard.isCopied('hex') ? 'Copied!' : 'Copy hexadecimal format to clipboard'} + text={clipboard.isCopied('hex') ? $t('common.clipboard.copied') : $t('common.clipboard.copyHexFormat')} position="left" > <button type="button" class="copy-btn {clipboard.isCopied('hex') ? 'copied' : ''}" onclick={() => clipboard.copy(formats.hex, 'hex')} - aria-label="Copy hexadecimal format to clipboard" + aria-label={$t('common.clipboard.copyHexFormat')} > <Icon name={clipboard.isCopied('hex') ? 'check' : 'copy'} size="sm" /> </button> @@ -303,7 +312,7 @@ <!-- Octal Format --> <div class="format-group"> - <label for="octal-input">Octal Format</label> + <label for="octal-input">{$t('tools/ip-converter.formats.octal.title')}</label> <div class="format-input"> <input id="octal-input" @@ -313,12 +322,17 @@ class="format-field octal" readonly /> - <Tooltip text={clipboard.isCopied('octal') ? 'Copied!' : 'Copy octal format to clipboard'} position="left"> + <Tooltip + text={clipboard.isCopied('octal') + ? $t('common.clipboard.copied') + : $t('common.clipboard.copyOctalFormat')} + position="left" + > <button type="button" class="copy-btn {clipboard.isCopied('octal') ? 'copied' : ''}" onclick={() => clipboard.copy(formats.octal, 'octal')} - aria-label="Copy octal format to clipboard" + aria-label={$t('common.clipboard.copyOctalFormat')} > <Icon name={clipboard.isCopied('octal') ? 'check' : 'copy'} size="sm" /> </button> @@ -335,50 +349,92 @@ <div class="card"> <h3> <Icon name="info" size="md" /> - Number Format Explanations + {$t('tools/ip-converter.explanations.title')} </h3> <div class="explainer-content"> <div class="format-explanations"> <!-- Binary Format --> <div class="format-explanation"> - <h4><span class="format-badge binary">Binary (Base-2)</span></h4> + <h4><span class="format-badge binary">{$t('tools/ip-converter.formats.binary.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.whatItIs')}</strong> + {$t('tools/ip-converter.formats.binary.whatItIs')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.example')}</strong> + <code>{$t('tools/ip-converter.formats.binary.example')}</code> + </p> <p> - <strong>What it is:</strong> Uses only digits 0 and 1, representing how computers internally store IP addresses. + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.formats.binary.usage')} </p> - <p><strong>Example:</strong> <code>192.168.1.1 = 11000000.10101000.00000001.00000001</code></p> <p> - <strong>Usage:</strong> Low-level networking, subnet calculations, understanding network/host boundaries. + <strong>{$t('tools/ip-converter.explanations.howToRead')}</strong> + {$t('tools/ip-converter.formats.binary.howToRead')} </p> - <p><strong>How to read:</strong> Each octet is 8 bits. Binary 11000000 = 128+64 = 192 in decimal.</p> </div> <!-- Decimal Format --> <div class="format-explanation"> - <h4><span class="format-badge decimal">Decimal (Base-10)</span></h4> - <p><strong>What it is:</strong> The entire IP as a single large number (0-4,294,967,295).</p> - <p><strong>Example:</strong> <code>192.168.1.1 = 3,232,235,777</code></p> - <p><strong>Usage:</strong> Database storage, mathematical operations, IP range calculations.</p> - <p><strong>Calculation:</strong> (192Γ—256Β³) + (168Γ—256Β²) + (1Γ—256) + 1 = 3,232,235,777</p> + <h4><span class="format-badge decimal">{$t('tools/ip-converter.formats.decimal.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.whatItIs')}</strong> + {$t('tools/ip-converter.formats.decimal.whatItIs')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.example')}</strong> + <code>{$t('tools/ip-converter.formats.decimal.example')}</code> + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.formats.decimal.usage')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.calculation')}</strong> + {$t('tools/ip-converter.formats.decimal.calculation')} + </p> </div> <!-- Hexadecimal Format --> <div class="format-explanation"> - <h4><span class="format-badge hex">Hexadecimal (Base-16)</span></h4> + <h4><span class="format-badge hex">{$t('tools/ip-converter.formats.hexadecimal.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.whatItIs')}</strong> + {$t('tools/ip-converter.formats.hexadecimal.whatItIs')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.example')}</strong> + <code>{$t('tools/ip-converter.formats.hexadecimal.example')}</code> + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.formats.hexadecimal.usage')} + </p> <p> - <strong>What it is:</strong> Uses digits 0-9 and letters A-F, common in programming and system administration. + <strong>{$t('tools/ip-converter.explanations.conversion')}</strong> + {$t('tools/ip-converter.formats.hexadecimal.conversion')} </p> - <p><strong>Example:</strong> <code>192.168.1.1 = 0xC0.0xA8.0x01.0x01</code></p> - <p><strong>Usage:</strong> Programming, system logs, network debugging, firmware configuration.</p> - <p><strong>Conversion:</strong> 192 = C0 hex, 168 = A8 hex. Each hex digit represents 4 bits.</p> </div> <!-- Octal Format --> <div class="format-explanation"> - <h4><span class="format-badge octal">Octal (Base-8)</span></h4> - <p><strong>What it is:</strong> Uses digits 0-7, less common but still found in some Unix systems.</p> - <p><strong>Example:</strong> <code>192.168.1.1 = 0300.0250.001.001</code></p> - <p><strong>Usage:</strong> Legacy Unix configurations, file permissions, some network tools.</p> - <p><strong>Note:</strong> Leading zeros indicate octal format. 0300 octal = 192 decimal.</p> + <h4><span class="format-badge octal">{$t('tools/ip-converter.formats.octal.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.whatItIs')}</strong> + {$t('tools/ip-converter.formats.octal.whatItIs')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.example')}</strong> + <code>{$t('tools/ip-converter.formats.octal.example')}</code> + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.formats.octal.usage')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.note')}</strong> + {$t('tools/ip-converter.formats.octal.note')} + </p> </div> </div> </div> @@ -388,44 +444,80 @@ <div class="card"> <h3> <Icon name="info" size="md" /> - IP Address Classes + {$t('tools/ip-converter.explanations.ipClassesTitle')} </h3> <div class="explainer-content"> - <p>IP address classes are historical categories that determine network size and usage patterns:</p> + <p>{$t('tools/ip-converter.ipClasses.description')}</p> <div class="class-explanations"> <div class="class-explanation"> - <h4><span class="class-badge class-a">Class A</span></h4> - <p><strong>Range:</strong> 1.0.0.0 to 126.255.255.255</p> - <p><strong>Default Mask:</strong> 255.0.0.0 (/8)</p> - <p><strong>Networks:</strong> 126 networks, 16.7 million hosts each</p> - <p><strong>Usage:</strong> Large organizations, ISPs, government networks</p> + <h4><span class="class-badge class-a">{$t('tools/ip-converter.ipClasses.classA.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.range')}</strong> + {$t('tools/ip-converter.ipClasses.classA.range')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.defaultMask')}</strong> + {$t('tools/ip-converter.ipClasses.classA.defaultMask')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.networks')}</strong> + {$t('tools/ip-converter.ipClasses.classA.networks')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.ipClasses.classA.usage')} + </p> </div> <div class="class-explanation"> - <h4><span class="class-badge class-b">Class B</span></h4> - <p><strong>Range:</strong> 128.0.0.0 to 191.255.255.255</p> - <p><strong>Default Mask:</strong> 255.255.0.0 (/16)</p> - <p><strong>Networks:</strong> 16,384 networks, 65,534 hosts each</p> - <p><strong>Usage:</strong> Universities, medium-large organizations</p> + <h4><span class="class-badge class-b">{$t('tools/ip-converter.ipClasses.classB.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.range')}</strong> + {$t('tools/ip-converter.ipClasses.classB.range')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.defaultMask')}</strong> + {$t('tools/ip-converter.ipClasses.classB.defaultMask')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.networks')}</strong> + {$t('tools/ip-converter.ipClasses.classB.networks')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.ipClasses.classB.usage')} + </p> </div> <div class="class-explanation"> - <h4><span class="class-badge class-c">Class C</span></h4> - <p><strong>Range:</strong> 192.0.0.0 to 223.255.255.255</p> - <p><strong>Default Mask:</strong> 255.255.255.0 (/24)</p> - <p><strong>Networks:</strong> 2.1 million networks, 254 hosts each</p> - <p><strong>Usage:</strong> Small businesses, home networks</p> + <h4><span class="class-badge class-c">{$t('tools/ip-converter.ipClasses.classC.badge')}</span></h4> + <p> + <strong>{$t('tools/ip-converter.explanations.range')}</strong> + {$t('tools/ip-converter.ipClasses.classC.range')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.defaultMask')}</strong> + {$t('tools/ip-converter.ipClasses.classC.defaultMask')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.networks')}</strong> + {$t('tools/ip-converter.ipClasses.classC.networks')} + </p> + <p> + <strong>{$t('tools/ip-converter.explanations.usage')}</strong> + {$t('tools/ip-converter.ipClasses.classC.usage')} + </p> </div> </div> <div class="class-notes"> - <h4>Special Ranges</h4> + <h4>{$t('tools/ip-converter.ipClasses.specialRanges.title')}</h4> <ul> - <li><strong>Class D (224-239):</strong> Multicast addresses for group communication</li> - <li><strong>Class E (240-255):</strong> Reserved for experimental use</li> - <li><strong>Private Networks:</strong> 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16</li> - <li><strong>Loopback:</strong> 127.0.0.0/8 (localhost addresses)</li> + <li>{$t('tools/ip-converter.ipClasses.specialRanges.classD')}</li> + <li>{$t('tools/ip-converter.ipClasses.specialRanges.classE')}</li> + <li>{$t('tools/ip-converter.ipClasses.specialRanges.privateNetworks')}</li> + <li>{$t('tools/ip-converter.ipClasses.specialRanges.loopback')}</li> </ul> </div> </div> @@ -435,34 +527,61 @@ <div class="card"> <h3> <Icon name="lightbulb" size="md" /> - When to Use Each Format + {$t('tools/ip-converter.useCases.title')} </h3> <div class="explainer-content"> <div class="usage-scenarios"> <div class="usage-scenario"> - <h4>Network Administration</h4> + <h4>{$t('tools/ip-converter.useCases.networkAdmin.title')}</h4> <ul> - <li><strong>Dotted Decimal:</strong> Daily configuration and documentation</li> - <li><strong>Binary:</strong> Subnet calculations and VLSM planning</li> - <li><strong>Hexadecimal:</strong> Debugging network captures and logs</li> + <li> + <strong>{$t('tools/ip-converter.explanations.dottedDecimal')}</strong> + {$t('tools/ip-converter.useCases.networkAdmin.dottedDecimal')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.binary')}</strong> + {$t('tools/ip-converter.useCases.networkAdmin.binary')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.hexadecimal')}</strong> + {$t('tools/ip-converter.useCases.networkAdmin.hexadecimal')} + </li> </ul> </div> <div class="usage-scenario"> - <h4>Programming & Development</h4> + <h4>{$t('tools/ip-converter.useCases.programming.title')}</h4> <ul> - <li><strong>Decimal:</strong> Database storage and IP range operations</li> - <li><strong>Hexadecimal:</strong> Low-level socket programming</li> - <li><strong>Binary:</strong> Bitwise operations and subnet masking</li> + <li> + <strong>{$t('tools/ip-converter.explanations.decimal')}</strong> + {$t('tools/ip-converter.useCases.programming.decimal')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.hexadecimal')}</strong> + {$t('tools/ip-converter.useCases.programming.hexadecimal')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.binary')}</strong> + {$t('tools/ip-converter.useCases.programming.binary')} + </li> </ul> </div> <div class="usage-scenario"> - <h4>Troubleshooting & Analysis</h4> + <h4>{$t('tools/ip-converter.useCases.troubleshooting.title')}</h4> <ul> - <li><strong>Binary:</strong> Understanding subnet boundaries</li> - <li><strong>Hexadecimal:</strong> Reading network packet captures</li> - <li><strong>Decimal:</strong> Quick IP range calculations</li> + <li> + <strong>{$t('tools/ip-converter.explanations.binary')}</strong> + {$t('tools/ip-converter.useCases.troubleshooting.binary')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.hexadecimal')}</strong> + {$t('tools/ip-converter.useCases.troubleshooting.hexadecimal')} + </li> + <li> + <strong>{$t('tools/ip-converter.explanations.decimal')}</strong> + {$t('tools/ip-converter.useCases.troubleshooting.decimal')} + </li> </ul> </div> </div> diff --git a/src/lib/components/tools/IPEnumerate.svelte b/src/lib/components/tools/IPEnumerate.svelte index 693a147b..7cf81a86 100644 --- a/src/lib/components/tools/IPEnumerate.svelte +++ b/src/lib/components/tools/IPEnumerate.svelte @@ -3,6 +3,14 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let input = $state('192.168.1.0/28'); let maxDisplayLimit = $state(1000); @@ -33,33 +41,33 @@ const ABSOLUTE_MAX_DISPLAY = 10000; const ABSOLUTE_MAX_GENERATION = 100000; - const examples = [ + const examples = $derived([ { - label: 'Small Subnet /28', + label: $t('tools.ip_enumerate.examples.smallSubnet.label'), input: '192.168.1.0/28', - description: '16 addresses', + description: $t('tools.ip_enumerate.examples.smallSubnet.description'), }, { - label: 'Point-to-Point /30', + label: $t('tools.ip_enumerate.examples.standardSubnet.label'), input: '10.0.0.0/30', - description: '4 addresses', + description: $t('tools.ip_enumerate.examples.standardSubnet.description'), }, { - label: 'IP Range', + label: $t('tools.ip_enumerate.examples.ipRange.label'), input: '172.16.1.1-172.16.1.10', - description: '10 addresses', + description: $t('tools.ip_enumerate.examples.ipRange.description'), }, { - label: 'Class C /24', + label: $t('tools.ip_enumerate.examples.singleAddress.label'), input: '192.168.0.0/24', - description: '256 addresses', + description: $t('tools.ip_enumerate.examples.singleAddress.description'), }, { - label: 'IPv6 /126', + label: $t('tools.ip_enumerate.examples.largeBlock.label'), input: '2001:db8::/126', - description: '4 addresses', + description: $t('tools.ip_enumerate.examples.largeBlock.description'), }, - ]; + ]); function loadExample(example: (typeof examples)[0]) { input = example.input; @@ -165,7 +173,11 @@ if (size > BigInt(ABSOLUTE_MAX_GENERATION)) { throw new Error( - `IPv6 /${prefixLength} would generate ${formatNumber(Number(size))} addresses. Maximum allowed: ${formatNumber(ABSOLUTE_MAX_GENERATION)}`, + $t('tools.ip_enumerate.errors.ipv6TooLarge', { + prefix: prefixLength, + count: formatNumber(Number(size)), + max: formatNumber(ABSOLUTE_MAX_GENERATION), + }), ); } @@ -187,7 +199,11 @@ if (size > ABSOLUTE_MAX_GENERATION) { throw new Error( - `/${prefixLength} would generate ${formatNumber(size)} addresses. Maximum allowed: ${formatNumber(ABSOLUTE_MAX_GENERATION)}`, + $t('tools.ip_enumerate.errors.cidrTooLarge', { + prefix: prefixLength, + count: formatNumber(size), + max: formatNumber(ABSOLUTE_MAX_GENERATION), + }), ); } @@ -221,7 +237,10 @@ if (count > ABSOLUTE_MAX_GENERATION) { throw new Error( - `Range would generate ${formatNumber(count)} addresses. Maximum allowed: ${formatNumber(ABSOLUTE_MAX_GENERATION)}`, + $t('tools.ip_enumerate.errors.rangeTooLarge', { + count: formatNumber(count), + max: formatNumber(ABSOLUTE_MAX_GENERATION), + }), ); } @@ -257,7 +276,7 @@ } catch (error) { result = { success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + error: error instanceof Error ? error.message : $t('tools.ip_enumerate.errors.unknownError'), addresses: [], totalCount: 0, displayCount: 0, @@ -338,21 +357,21 @@ <!-- Input Card with everything --> <div class="card main-input-card"> <header class="card-header"> - <h2>IP Enumerate</h2> - <p>Safely enumerate all IP addresses in CIDR blocks and ranges</p> + <h2>{$t('tools.ip_enumerate.title')}</h2> + <p>{$t('tools.ip_enumerate.description')}</p> </header> <input type="text" bind:value={input} oninput={handleInputChange} - placeholder="192.168.1.0/28 or 10.0.0.1-10.0.0.10" + placeholder={$t('tools.ip_enumerate.input.placeholder')} class="network-input" /> <div class="options-row"> <div class="limit-control"> - <label for="max-display">Max Display</label> + <label for="max-display">{$t('tools.ip_enumerate.input.maxDisplay.label')}</label> <input id="max-display" type="number" @@ -368,12 +387,12 @@ <label class="checkbox-option"> <input type="checkbox" bind:checked={includeNetwork} onchange={handleInputChange} /> <span class="checkmark"></span> - Network + {$t('tools.ip_enumerate.input.options.network')} </label> <label class="checkbox-option"> <input type="checkbox" bind:checked={includeBroadcast} onchange={handleInputChange} /> <span class="checkmark"></span> - Broadcast + {$t('tools.ip_enumerate.input.options.broadcast')} </label> </div> </div> @@ -383,7 +402,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h4>Quick Examples</h4> + <h4>{$t('tools.ip_enumerate.examples.title')}</h4> </summary> <div class="examples-list"> {#each examples as example (example.label)} @@ -404,10 +423,11 @@ <div class="safety-warning"> <Icon name="alert-triangle" size="sm" /> <div> - <strong>Safety:</strong> Max {formatNumber(ABSOLUTE_MAX_DISPLAY)} displayed, {formatNumber( - ABSOLUTE_MAX_GENERATION, - )} - generated + <strong>{$t('tools.ip_enumerate.safety.title')}:</strong> + {$t('tools.ip_enumerate.safety.message', { + maxDisplay: formatNumber(ABSOLUTE_MAX_DISPLAY), + maxGeneration: formatNumber(ABSOLUTE_MAX_GENERATION), + })} </div> </div> </div> @@ -417,7 +437,7 @@ <div class="card loading-card"> <div class="loading-content"> <Icon name="loader" size="lg" /> - <span>Generating IP addresses...</span> + <span>{$t('tools.ip_enumerate.actions.generating')}</span> </div> </div> {/if} @@ -431,52 +451,52 @@ <div class="card-header"> <h3> <Icon name="list" size="sm" /> - Results + {$t('tools.ip_enumerate.results.title')} </h3> </div> <div class="export-actions"> <button class="export-btn" onclick={exportToJSON}> <Icon name="json-file" size="sm" /> - JSON + {$t('tools.ip_enumerate.actions.exportJSON')} </button> <button class="export-btn" onclick={exportToCSV}> <Icon name="csv-file" size="sm" /> - CSV + {$t('tools.ip_enumerate.actions.exportCSV')} </button> <button class="copy-btn {clipboard.isCopied('all-addresses') ? 'copied' : ''}" onclick={() => clipboard.copy((result?.addresses || []).join('\n'), 'all-addresses')} > <Icon name={clipboard.isCopied('all-addresses') ? 'check' : 'copy'} size="sm" /> - Copy All + {$t('tools.ip_enumerate.actions.copyAll')} </button> </div> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{formatNumber(result.totalCount)}</span> - <span class="stat-label">Total</span> + <span class="stat-label">{$t('tools.ip_enumerate.results.stats.total')}</span> </div> <div class="stat"> <span class="stat-value">{formatNumber(result.displayCount)}</span> - <span class="stat-label">Shown</span> + <span class="stat-label">{$t('tools.ip_enumerate.results.stats.shown')}</span> </div> <div class="stat"> <span class="stat-value">{estimateMemoryUsage(result.displayCount)}</span> - <span class="stat-label">Memory</span> + <span class="stat-label">{$t('tools.ip_enumerate.results.stats.memory')}</span> </div> </div> {#if result.networkInfo.network} <div class="network-info"> <div class="info-item"> - <span>Network:</span> + <span>{$t('tools.ip_enumerate.results.networkInfo.network')}:</span> <code>{result.networkInfo.network}</code> </div> {#if result.networkInfo.broadcast} <div class="info-item"> - <span>Broadcast:</span> + <span>{$t('tools.ip_enumerate.results.networkInfo.broadcast')}:</span> <code>{result.networkInfo.broadcast}</code> </div> {/if} @@ -489,11 +509,14 @@ <div class="card-header"> <h3> <Icon name="target" size="sm" /> - IP Addresses + {$t('tools.ip_enumerate.results.addresses.title')} </h3> {#if result.truncated} <span class="truncated-notice" - >Showing {formatNumber(result.displayCount)} of {formatNumber(result.totalCount)}</span + >{$t('tools.ip_enumerate.results.addresses.truncatedNotice', { + shown: formatNumber(result.displayCount), + total: formatNumber(result.totalCount), + })}</span > {/if} </div> @@ -517,7 +540,7 @@ <div class="card error-card"> <div class="error-content"> <Icon name="alert-triangle" size="lg" /> - <h3>Error</h3> + <h3>{$t('tools.ip_enumerate.errors.title')}</h3> <p>{result.error}</p> </div> </div> diff --git a/src/lib/components/tools/IPRegexGenerator.svelte b/src/lib/components/tools/IPRegexGenerator.svelte index e913908c..8171d1c9 100644 --- a/src/lib/components/tools/IPRegexGenerator.svelte +++ b/src/lib/components/tools/IPRegexGenerator.svelte @@ -16,6 +16,14 @@ type AdvancedOption, } from '$lib/utils/ip-regex-gen.js'; import { validateRegexInput, type RegexValidation } from '$lib/utils/ip-regex-validator.js'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let mode = $state<Mode>('simple'); let regexType = $state<RegexType>('ipv4'); @@ -136,7 +144,7 @@ function validateEditableRegex() { if (!editablePattern) { - regexValidation = { ok: false, error: 'Pattern cannot be empty' }; + regexValidation = { ok: false, error: $t('tools.ip_regex_generator.errors.patternEmpty') }; return; } @@ -283,8 +291,16 @@ const invalidCases = isEditingTestCases ? editableInvalidCases : result.testCases.invalid; testCaseResults = { - valid: validCases.map((text) => ({ text, matches: false, error: 'Invalid regex' })), - invalid: invalidCases.map((text) => ({ text, matches: false, error: 'Invalid regex' })), + valid: validCases.map((text) => ({ + text, + matches: false, + error: $t('tools.ip_regex_generator.errors.invalidRegex'), + })), + invalid: invalidCases.map((text) => ({ + text, + matches: false, + error: $t('tools.ip_regex_generator.errors.invalidRegex'), + })), }; } } @@ -362,8 +378,8 @@ <div class="card"> <header class="card-header"> - <h2>IP Regex Generator</h2> - <p>Generate safe and reliable regular expressions for IPv4 and IPv6 address validation</p> + <h2>{$t('tools.ip_regex_generator.title')}</h2> + <p>{$t('tools.ip_regex_generator.description')}</p> </header> <!-- Mode Selection --> @@ -377,7 +393,7 @@ }} > <Icon name="zap" size="sm" /> - Simple Mode + {$t('tools.ip_regex_generator.modes.simple')} </button> <button class="mode-button {mode === 'advanced' ? 'active' : ''}" @@ -387,20 +403,20 @@ }} > <Icon name="settings" size="sm" /> - Advanced Mode + {$t('tools.ip_regex_generator.modes.advanced')} </button> </div> </section> <!-- Type Selection --> <section class="type-section"> - <h4>IP Address Type</h4> + <h4>{$t('tools.ip_regex_generator.types.title')}</h4> <div class="type-options"> <label class="type-option"> <input type="radio" bind:group={regexType} value="ipv4" onchange={handleRegexGeneration} /> <div class="option-content"> <Icon name="ipv6-ipv4" size="sm" /> - <span>IPv4 Only</span> + <span>{$t('tools.ip_regex_generator.types.ipv4Only')}</span> </div> </label> @@ -408,7 +424,7 @@ <input type="radio" bind:group={regexType} value="ipv6" onchange={handleRegexGeneration} /> <div class="option-content"> <Icon name="ipv4-ipv6" size="sm" /> - <span>IPv6 Only</span> + <span>{$t('tools.ip_regex_generator.types.ipv6Only')}</span> </div> </label> @@ -416,7 +432,7 @@ <input type="radio" bind:group={regexType} value="both" onchange={handleRegexGeneration} /> <div class="option-content"> <Icon name="network" size="sm" /> - <span>Both IPv4 & IPv6</span> + <span>{$t('tools.ip_regex_generator.types.both')}</span> </div> </label> </div> @@ -425,7 +441,7 @@ <!-- Advanced Options --> {#if mode === 'advanced'} <section class="options-section"> - <h4>Advanced Options</h4> + <h4>{$t('tools.ip_regex_generator.options.title')}</h4> <div class="options-grid"> {#each ADVANCED_OPTIONS as option (`${option.ipClass}-${option.key}`)} {#if option.showForType.includes(regexType)} @@ -456,7 +472,7 @@ {#if result} <section class="results-section"> <div class="results-header"> - <h3>Generated Pattern</h3> + <h3>{$t('tools.ip_regex_generator.results.title')}</h3> </div> <!-- Regex Pattern --> @@ -465,44 +481,50 @@ <!-- Editable Regex --> <div class="regex-editor"> <div class="editor-header"> - <span class="pattern-label">Edit Regular Expression</span> + <span class="pattern-label">{$t('tools.ip_regex_generator.editor.title')}</span> <div class="editor-actions"> <button class="apply-button" onclick={applyRegexEdits} disabled={!regexValidation?.ok} - use:tooltip={regexValidation?.ok ? 'Apply changes' : 'Fix validation errors first'} + use:tooltip={regexValidation?.ok + ? $t('tools.ip_regex_generator.editor.applyChanges') + : $t('tools.ip_regex_generator.editor.fixErrors')} > <Icon name="check" size="sm" /> - Apply + {$t('tools.ip_regex_generator.editor.apply')} </button> - <button class="cancel-button" onclick={cancelRegexEditing} use:tooltip={'Cancel editing'}> + <button + class="cancel-button" + onclick={cancelRegexEditing} + use:tooltip={$t('tools.ip_regex_generator.editor.cancelEditing')} + > <Icon name="x" size="sm" /> - Cancel + {$t('tools.ip_regex_generator.editor.cancel')} </button> </div> </div> <div class="editor-fields"> <div class="field-group"> - <label for="pattern-input">Pattern:</label> + <label for="pattern-input">{$t('tools.ip_regex_generator.editor.patternLabel')}</label> <input id="pattern-input" type="text" bind:value={editablePattern} class="pattern-input {regexValidation && !regexValidation.ok ? 'error' : ''}" - placeholder="Enter regex pattern..." + placeholder={$t('tools.ip_regex_generator.editor.patternPlaceholder')} /> </div> <div class="field-group"> - <label for="flags-input">Flags:</label> + <label for="flags-input">{$t('tools.ip_regex_generator.editor.flagsLabel')}</label> <input id="flags-input" type="text" bind:value={editableFlags} class="flags-input {regexValidation && !regexValidation.ok ? 'error' : ''}" - placeholder="g, i, m, etc." + placeholder={$t('tools.ip_regex_generator.editor.flagsPlaceholder')} maxlength="10" /> </div> @@ -512,7 +534,7 @@ {#if regexValidation.ok} <div class="validation-success"> <Icon name="check-circle" size="sm" /> - Valid regex pattern + {$t('tools.ip_regex_generator.editor.validPattern')} </div> {:else} <div class="validation-error"> @@ -523,7 +545,7 @@ {/if} <div class="pattern-preview"> - <span class="preview-label">Preview:</span> + <span class="preview-label">{$t('tools.ip_regex_generator.editor.previewLabel')}</span> <code class="pattern-code">/{editablePattern || '...'}/{editableFlags}</code> </div> </div> @@ -531,13 +553,17 @@ <!-- Display Mode --> <div class="regex-pattern"> <div class="pattern-header"> - <h4 class="pattern-label">Regular Expression</h4> + <h4 class="pattern-label">{$t('tools.ip_regex_generator.results.patternLabel')}</h4> <div class="results-actions"> {#if !isEditingRegex} - <button class="edit-button" onclick={enableRegexEditing} use:tooltip={'Edit this regex pattern'}> + <button + class="edit-button" + onclick={enableRegexEditing} + use:tooltip={$t('tools.ip_regex_generator.tooltips.editPattern')} + > <Icon name="edit" size="xs" /> - Edit Pattern + {$t('tools.ip_regex_generator.results.edit')} </button> {/if} <a @@ -545,17 +571,19 @@ target="_blank" rel="noopener noreferrer" class="regexr-button" - use:tooltip={'Test this pattern on RegexR.com'} + use:tooltip={$t('tools.ip_regex_generator.tooltips.testRegex')} > <Icon name="regexr" size="xs" /> - Test on RegexR + {$t('tools.ip_regex_generator.results.testUrl')} </a> <button class="copy-button {clipboard.isCopied('pattern') ? 'copied' : ''}" onclick={() => result && clipboard.copy(result.pattern, 'pattern')} > <Icon name={clipboard.isCopied('pattern') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('pattern') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('pattern') + ? $t('tools.ip_regex_generator.results.copied') + : $t('tools.ip_regex_generator.results.copy')} </button> </div> </div> @@ -565,7 +593,7 @@ {#if result.flags} <div class="regex-flags"> - <span class="flags-label">Flags:</span> + <span class="flags-label">{$t('tools.ip_regex_generator.editor.flagsLabel')}</span> <code class="flags-code">{result.flags}</code> <span class="flags-description"> {#if result.flags.includes('i')} @@ -584,23 +612,35 @@ <!-- Test Cases --> <div class="test-cases"> <div class="test-cases-header"> - <h4 class="pattern-label">Test Cases</h4> + <h4 class="pattern-label">{$t('tools.ip_regex_generator.testCases.title')}</h4> {#if !isEditingTestCases} <div class="results-actions"> - <button class="edit-button" onclick={enableTestCaseEditing} use:tooltip={'Edit test cases'}> + <button + class="edit-button" + onclick={enableTestCaseEditing} + use:tooltip={$t('tools.ip_regex_generator.testCases.editTestCases')} + > <Icon name="edit" size="xs" /> - Edit Test Cases + {$t('tools.ip_regex_generator.testCases.editTestCases')} </button> </div> {:else} <div class="editor-actions"> - <button class="apply-button" onclick={applyTestCaseEdits} use:tooltip={'Apply test case changes'}> + <button + class="apply-button" + onclick={applyTestCaseEdits} + use:tooltip={$t('tools.ip_regex_generator.testCases.applyTestChanges')} + > <Icon name="check" size="sm" /> - Apply + {$t('tools.ip_regex_generator.editor.apply')} </button> - <button class="cancel-button" onclick={cancelTestCaseEditing} use:tooltip={'Cancel editing'}> + <button + class="cancel-button" + onclick={cancelTestCaseEditing} + use:tooltip={$t('tools.ip_regex_generator.testCases.cancelTestEditing')} + > <Icon name="x" size="sm" /> - Cancel + {$t('tools.ip_regex_generator.editor.cancel')} </button> </div> {/if} @@ -610,7 +650,9 @@ <div class="test-group valid"> <h5> <Icon name="check-circle" size="sm" /> - Should Match ({isEditingTestCases ? editableValidCases.length : result.testCases.valid.length}) + {$t('tools.ip_regex_generator.testCases.validTitle')} ({isEditingTestCases + ? editableValidCases.length + : result.testCases.valid.length}) </h5> {#if isEditingTestCases} @@ -619,7 +661,7 @@ <input type="text" bind:value={newValidCase} - placeholder="Add new valid test case..." + placeholder={$t('tools.ip_regex_generator.testCases.addValidPlaceholder')} class="test-input" onkeydown={(e) => e.key === 'Enter' && addValidTestCase()} /> @@ -627,7 +669,7 @@ class="add-button" onclick={addValidTestCase} disabled={!newValidCase.trim()} - use:tooltip={'Add test case'} + use:tooltip={$t('tools.ip_regex_generator.testCases.addValid')} > <Icon name="plus" size="sm" /> </button> @@ -664,7 +706,9 @@ <div class="test-group invalid"> <h5> <Icon name="x-circle" size="sm" /> - Should Not Match ({isEditingTestCases ? editableInvalidCases.length : result.testCases.invalid.length}) + {$t('tools.ip_regex_generator.testCases.invalidTitle')} ({isEditingTestCases + ? editableInvalidCases.length + : result.testCases.invalid.length}) </h5> {#if isEditingTestCases} @@ -673,7 +717,7 @@ <input type="text" bind:value={newInvalidCase} - placeholder="Add new invalid test case..." + placeholder={$t('tools.ip_regex_generator.testCases.addInvalidPlaceholder')} class="test-input" onkeydown={(e) => e.key === 'Enter' && addInvalidTestCase()} /> @@ -681,7 +725,7 @@ class="add-button" onclick={addInvalidTestCase} disabled={!newInvalidCase.trim()} - use:tooltip={'Add test case'} + use:tooltip={$t('tools.ip_regex_generator.testCases.addInvalid')} > <Icon name="plus" size="sm" /> </button> @@ -767,7 +811,7 @@ <!-- Implementation Examples --> <div class="language-examples"> - <h4>Implementation Examples</h4> + <h4>{$t('tools.ip_regex_generator.languageExamples.title')}</h4> <div class="language-accordion"> {#each getLanguageExamples(result.pattern, result.flags) as example (example.name)} <div class="language-item"> @@ -787,7 +831,7 @@ <button class="copy-code-btn {clipboard.isCopied(example.name.toLowerCase()) ? 'copied' : ''}" onclick={() => clipboard.copy(example.code, example.name.toLowerCase())} - use:tooltip={'Copy code snippet'} + use:tooltip={$t('tools.ip_regex_generator.languageExamples.copy')} > <Icon name={clipboard.isCopied(example.name.toLowerCase()) ? 'check' : 'copy'} size="xs" /> </button> diff --git a/src/lib/components/tools/IPValidator.svelte b/src/lib/components/tools/IPValidator.svelte index 6144a5b3..ff574568 100644 --- a/src/lib/components/tools/IPValidator.svelte +++ b/src/lib/components/tools/IPValidator.svelte @@ -2,6 +2,14 @@ import { tooltip as _tooltip } from '$lib/actions/tooltip.js'; import Icon from '$lib/components/global/Icon.svelte'; import '../../../styles/diagnostics-pages.scss'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools.ip-validator'); + }); let inputValue = $state(''); let selectedExampleIndex = $state<number | null>(null); @@ -25,14 +33,18 @@ } | null>(null); // Common test cases for quick validation - const testCases = [ - { label: 'Valid IPv4', value: '192.168.1.1', valid: true }, - { label: 'Valid IPv6', value: '2001:db8::1', valid: true }, - { label: 'IPv4 with leading zeros', value: '192.168.001.001', valid: false }, - { label: 'IPv4 octet too large', value: '192.168.1.256', valid: false }, - { label: 'IPv6 with multiple ::', value: '2001::db8::1', valid: false }, - { label: 'IPv6 too many groups', value: '2001:db8:85a3:0000:0000:8a2e:0370:7334:extra', valid: false }, - ]; + const testCases = $derived([ + { label: $t('tools.ip-validator.examples.validIPv4'), value: '192.168.1.1', valid: true }, + { label: $t('tools.ip-validator.examples.validIPv6'), value: '2001:db8::1', valid: true }, + { label: $t('tools.ip-validator.examples.ipv4LeadingZeros'), value: '192.168.001.001', valid: false }, + { label: $t('tools.ip-validator.examples.ipv4OctetTooLarge'), value: '192.168.1.256', valid: false }, + { label: $t('tools.ip-validator.examples.ipv6MultipleDoubleColon'), value: '2001::db8::1', valid: false }, + { + label: $t('tools.ip-validator.examples.ipv6TooManyGroups'), + value: '2001:db8:85a3:0000:0000:8a2e:0370:7334:extra', + valid: false, + }, + ]); function validateIPv4(ip: string): { isValid: boolean; @@ -46,7 +58,7 @@ // Check basic format if (!ip.includes('.')) { - errors.push('IPv4 addresses must contain dots (.) to separate octets'); + errors.push($t('tools.ip-validator.errors.ipv4.mustContainDots')); return { isValid: false, errors, warnings, details }; } @@ -54,7 +66,7 @@ // Check number of octets if (parts.length !== 4) { - errors.push(`IPv4 addresses must have exactly 4 octets, found ${parts.length}`); + errors.push($t('tools.ip-validator.errors.ipv4.wrongOctetCount', { expected: 4, found: parts.length })); return { isValid: false, errors, warnings, details }; } @@ -66,31 +78,31 @@ // Check if empty if (part === '') { - errors.push(`Octet ${octetNum} is empty`); + errors.push($t('tools.ip-validator.errors.ipv4.octetEmpty', { number: octetNum })); continue; } // Check for non-numeric characters if (!/^\d+$/.test(part)) { - errors.push(`Octet ${octetNum} contains non-numeric characters: "${part}"`); + errors.push($t('tools.ip-validator.errors.ipv4.nonNumericCharacters', { number: octetNum, part })); continue; } // Check for leading zeros (except for single zero) if (part.length > 1 && part[0] === '0') { - errors.push(`Octet ${octetNum} has leading zeros: "${part}" (should be "${parseInt(part)})")`); + errors.push($t('tools.ip-validator.errors.ipv4.leadingZeros', { number: octetNum, part })); continue; } // Parse and validate range const value = parseInt(part, 10); if (isNaN(value)) { - errors.push(`Octet ${octetNum} is not a valid number: "${part}"`); + errors.push($t('tools.ip-validator.errors.ipv4.nonNumericCharacters', { number: octetNum, part })); continue; } if (value < 0 || value > 255) { - errors.push(`Octet ${octetNum} out of range: ${value} (must be 0-255)`); + errors.push($t('tools.ip-validator.errors.ipv4.outOfRange', { number: octetNum, value })); continue; } @@ -107,57 +119,57 @@ // Determine address type and scope if (a === 127) { - details.addressType = 'Loopback'; - details.scope = 'Host'; - details.info.push('Used for local loopback communications'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.loopback'); + details.scope = $t('tools.ip-validator.scopes.host'); + details.info.push($t('tools.ip-validator.info.loopbackCommunications')); } else if (a === 10) { - details.addressType = 'Private (Class A)'; - details.scope = 'Private Network'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.privateA'); + details.scope = $t('tools.ip-validator.scopes.privateNetwork'); details.isPrivate = true; - details.info.push('RFC 1918 private address space'); + details.info.push($t('tools.ip-validator.info.rfc1918Private')); } else if (a === 172 && b >= 16 && b <= 31) { - details.addressType = 'Private (Class B)'; - details.scope = 'Private Network'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.privateB'); + details.scope = $t('tools.ip-validator.scopes.privateNetwork'); details.isPrivate = true; - details.info.push('RFC 1918 private address space'); + details.info.push($t('tools.ip-validator.info.rfc1918Private')); } else if (a === 192 && b === 168) { - details.addressType = 'Private (Class C)'; - details.scope = 'Private Network'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.privateC'); + details.scope = $t('tools.ip-validator.scopes.privateNetwork'); details.isPrivate = true; - details.info.push('RFC 1918 private address space'); + details.info.push($t('tools.ip-validator.info.rfc1918Private')); } else if (a === 169 && b === 254) { - details.addressType = 'Link-Local (APIPA)'; - details.scope = 'Link-Local'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.linkLocal'); + details.scope = $t('tools.ip-validator.scopes.linkLocal'); details.isReserved = true; - details.info.push('Automatic Private IP Addressing'); + details.info.push($t('tools.ip-validator.info.apipa')); } else if (a >= 224 && a <= 239) { - details.addressType = 'Multicast (Class D)'; - details.scope = 'Multicast'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.multicast'); + details.scope = $t('tools.ip-validator.scopes.multicast'); details.isReserved = true; - details.info.push('Used for multicast communications'); + details.info.push($t('tools.ip-validator.info.multicastCommunications')); } else if (a >= 240) { - details.addressType = 'Reserved (Class E)'; - details.scope = 'Reserved'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.reserved'); + details.scope = $t('tools.ip-validator.scopes.reserved'); details.isReserved = true; - details.info.push('Reserved for future use'); + details.info.push($t('tools.ip-validator.info.reservedFuture')); } else if (a === 0) { - details.addressType = 'Network Address'; - details.scope = 'Special Use'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.networkAddress'); + details.scope = $t('tools.ip-validator.scopes.specialUse'); details.isReserved = true; - details.info.push('"This network" address'); + details.info.push($t('tools.ip-validator.info.thisNetwork')); } else if (d === 0) { - details.addressType = 'Network Address'; - details.scope = 'Network'; - warnings.push('This appears to be a network address (host portion is 0)'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.networkAddress'); + details.scope = $t('tools.ip-validator.scopes.network'); + warnings.push($t('tools.ip-validator.warnings.ipv4.networkAddress')); } else if (d === 255) { - details.addressType = 'Broadcast Address'; - details.scope = 'Network'; - warnings.push('This appears to be a broadcast address (host portion is all 1s)'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.broadcastAddress'); + details.scope = $t('tools.ip-validator.scopes.network'); + warnings.push($t('tools.ip-validator.warnings.ipv4.broadcastAddress')); } else { - details.addressType = 'Public'; - details.scope = 'Internet'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv4.public'); + details.scope = $t('tools.ip-validator.scopes.internet'); details.isPrivate = false; - details.info.push('Publicly routable address'); + details.info.push($t('tools.ip-validator.info.publiclyRoutable')); } return { isValid: true, errors: [], warnings, details }; @@ -179,19 +191,19 @@ if (ip.includes('%')) { const parts = ip.split('%'); if (parts.length > 2) { - errors.push('Multiple % symbols found - invalid zone ID format'); + errors.push($t('tools.ip-validator.errors.ipv6.invalidCharacter', { char: '%' })); return { isValid: false, errors, warnings, details }; } cleanIP = parts[0]; zoneId = parts[1]; details.zoneId = zoneId; - details.info.push(`Zone ID specified: %${zoneId}`); + details.info.push($t('tools.ip-validator.info.zoneIdSpecified', { zoneId })); } // Check for :: (compression) const doubleColonCount = (cleanIP.match(/::/g) || []).length; if (doubleColonCount > 1) { - errors.push('Multiple :: sequences found - only one :: allowed per address'); + errors.push($t('tools.ip-validator.errors.ipv6.multipleDoubleColon')); return { isValid: false, errors, warnings, details }; } @@ -200,7 +212,7 @@ if (cleanIP.includes('::')) { const parts = cleanIP.split('::'); if (parts.length > 2) { - errors.push('Invalid :: usage - malformed compression'); + errors.push($t('tools.ip-validator.errors.ipv6.invalidFormat')); return { isValid: false, errors, warnings, details }; } @@ -212,7 +224,7 @@ const missingGroups = 8 - totalParts; if (missingGroups < 0) { - errors.push('Too many groups in compressed IPv6 address'); + errors.push($t('tools.ip-validator.errors.ipv6.tooManyGroups', { count: totalParts })); return { isValid: false, errors, warnings, details }; } @@ -231,7 +243,7 @@ const ipv4Result = validateIPv4(ipv4Part); if (!ipv4Result.isValid) { - errors.push(`Invalid embedded IPv4 address: ${ipv4Result.errors.join(', ')}`); + errors.push($t('tools.ip-validator.errors.ipv6.embeddedIPv4Error', { error: ipv4Result.errors.join(', ') })); return { isValid: false, errors, warnings, details }; } @@ -243,7 +255,7 @@ expandedIP = expandedIP.replace(ipv4Pattern, `${group1}:${group2}`); details.hasEmbeddedIPv4 = true; details.embeddedIPv4 = ipv4Part; - details.info.push(`Contains embedded IPv4 address: ${ipv4Part}`); + details.info.push($t('tools.ip-validator.info.embeddedIPv4', { ipv4: ipv4Part })); } // Split into groups and validate @@ -251,9 +263,9 @@ if (groups.length !== 8) { if (!cleanIP.includes('::')) { - errors.push(`IPv6 addresses must have 8 groups, found ${groups.length} (use :: for compression)`); + errors.push($t('tools.ip-validator.errors.ipv6.tooManyGroups', { count: groups.length })); } else { - errors.push(`Invalid IPv6 compression - results in ${groups.length} groups instead of 8`); + errors.push($t('tools.ip-validator.errors.ipv6.tooManyGroups', { count: groups.length })); } return { isValid: false, errors, warnings, details }; } @@ -261,20 +273,20 @@ // Validate each group for (let i = 0; i < groups.length; i++) { const group = groups[i]; - const groupNum = i + 1; + const _groupNum = i + 1; if (group === '') { - errors.push(`Group ${groupNum} is empty`); + errors.push($t('tools.ip-validator.errors.ipv6.emptyGroup')); continue; } if (group.length > 4) { - errors.push(`Group ${groupNum} too long: "${group}" (max 4 hex digits)`); + errors.push($t('tools.ip-validator.errors.ipv6.groupTooLong', { group })); continue; } if (!/^[0-9a-fA-F]+$/.test(group)) { - errors.push(`Group ${groupNum} contains invalid characters: "${group}" (only 0-9, a-f, A-F allowed)`); + errors.push($t('tools.ip-validator.errors.ipv6.invalidHexadecimal', { group })); continue; } } @@ -293,47 +305,47 @@ const firstTwoGroups = normalizedGroups.slice(0, 2).join(':'); if (fullForm === '0000:0000:0000:0000:0000:0000:0000:0001') { - details.addressType = 'Loopback'; - details.scope = 'Host'; - details.info.push('IPv6 loopback address (::1)'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.loopback'); + details.scope = $t('tools.ip-validator.scopes.host'); + details.info.push($t('tools.ip-validator.info.ipv6Loopback')); } else if (fullForm === '0000:0000:0000:0000:0000:0000:0000:0000') { - details.addressType = 'Unspecified'; - details.scope = 'Special Use'; - details.info.push('IPv6 unspecified address (::)'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.unspecified'); + details.scope = $t('tools.ip-validator.scopes.specialUse'); + details.info.push($t('tools.ip-validator.info.ipv6Unspecified')); } else if (firstGroup === 'fe80') { - details.addressType = 'Link-Local'; - details.scope = 'Link-Local'; - details.info.push('IPv6 link-local address'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.linkLocal'); + details.scope = $t('tools.ip-validator.scopes.linkLocal'); + details.info.push($t('tools.ip-validator.info.ipv6LinkLocal')); } else if (firstGroup === 'fec0') { - details.addressType = 'Site-Local (Deprecated)'; - details.scope = 'Site-Local'; - details.info.push('Deprecated site-local address'); - warnings.push('Site-local addresses are deprecated (RFC 3879)'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.siteLocal'); + details.scope = $t('tools.ip-validator.scopes.siteLocal'); + details.info.push($t('tools.ip-validator.info.deprecatedSiteLocal')); + warnings.push($t('tools.ip-validator.warnings.ipv6.siteLocalDeprecated')); } else if (firstTwoGroups === 'fc00' || firstTwoGroups === 'fd00') { - details.addressType = 'Unique Local'; - details.scope = 'Private Network'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.uniqueLocal'); + details.scope = $t('tools.ip-validator.scopes.privateNetwork'); details.isPrivate = true; - details.info.push('RFC 4193 Unique Local Address'); + details.info.push($t('tools.ip-validator.info.rfc4193UniqueLocal')); } else if (firstGroup.startsWith('ff')) { - details.addressType = 'Multicast'; - details.scope = 'Multicast'; - details.info.push('IPv6 multicast address'); + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.multicast'); + details.scope = $t('tools.ip-validator.scopes.multicast'); + details.info.push($t('tools.ip-validator.info.ipv6Multicast')); } else if (firstTwoGroups === '2001' && normalizedGroups[1] === '0db8') { - details.addressType = 'Documentation'; - details.scope = 'Documentation'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.documentation'); + details.scope = $t('tools.ip-validator.scopes.documentation'); details.isReserved = true; - details.info.push('RFC 3849 documentation address'); - warnings.push('This is a documentation address (not for production use)'); + details.info.push($t('tools.ip-validator.info.rfc3849Documentation')); + warnings.push($t('tools.ip-validator.warnings.ipv6.documentationAddress')); } else if (firstGroup >= '2000' && firstGroup <= '3fff') { - details.addressType = 'Global Unicast'; - details.scope = 'Internet'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.globalUnicast'); + details.scope = $t('tools.ip-validator.scopes.internet'); details.isPrivate = false; - details.info.push('Globally routable IPv6 address'); + details.info.push($t('tools.ip-validator.info.globallyRoutable')); } else { - details.addressType = 'Reserved'; - details.scope = 'Reserved'; + details.addressType = $t('tools.ip-validator.addressTypes.ipv6.reserved'); + details.scope = $t('tools.ip-validator.scopes.reserved'); details.isReserved = true; - details.info.push('Reserved address space'); + details.info.push($t('tools.ip-validator.info.reservedAddressSpace')); } // Check for compressed form @@ -341,7 +353,7 @@ const compressedForm = compressIPv6(fullForm); details.compressedForm = compressedForm; if (cleanIP !== compressedForm) { - details.info.push(`Standard compressed form: ${compressedForm}`); + details.info.push($t('tools.ip-validator.info.standardCompressed', { form: compressedForm })); } } @@ -446,7 +458,7 @@ result = { isValid: false, type: null, - errors: ['Input does not appear to be an IP address (no dots or colons found)'], + errors: [$t('tools.ip-validator.errors.general.unknownFormat')], warnings: [], details: {}, }; @@ -478,8 +490,8 @@ <div class="card"> <header class="card-header"> - <h1>IP Address Validator</h1> - <p>Validate IPv4 and IPv6 addresses with detailed error analysis and format checking</p> + <h1>{$t('tools.ip-validator.title')}</h1> + <p>{$t('tools.ip-validator.description')}</p> </header> <!-- Input Section --> @@ -487,19 +499,19 @@ <div class="input-group"> <label for="ip-input"> <Icon name="globe" size="sm" /> - Enter IP Address + {$t('tools.ip-validator.form.enterLabel')} </label> <input id="ip-input" type="text" bind:value={inputValue} oninput={handleInput} - placeholder="e.g., 192.168.1.1 or 2001:db8::1" + placeholder={$t('tools.ip-validator.form.placeholder')} class="ip-input {result?.isValid === true ? 'valid' : result?.isValid === false ? 'invalid' : ''}" autocomplete="off" spellcheck="false" /> - <div class="input-hint">Supports IPv4 (192.168.1.1), IPv6 (2001:db8::1), and various formats</div> + <div class="input-hint">{$t('tools.ip-validator.form.hint')}</div> </div> </section> @@ -508,7 +520,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Test Cases</h4> + <h4>{$t('tools.ip-validator.testCases.title')}</h4> </summary> <div class="examples-grid"> {#each testCases as testCase, index (`test-case-${index}`)} @@ -536,7 +548,10 @@ <div class="result-status"> <Icon name={result.isValid ? 'check-circle' : 'x-circle'} size="lg" /> <div class="status-text"> - <h2>{result.isValid ? 'Valid' : 'Invalid'} IP Address</h2> + <h2> + {result.isValid ? $t('tools.ip-validator.results.valid') : $t('tools.ip-validator.results.invalid')} + {$t('tools.ip-validator.results.ipAddress')} + </h2> {#if result.type} <span class="ip-type">{result.type.toUpperCase()} Format</span> {/if} @@ -545,7 +560,7 @@ {#if result.isValid && result.details.normalizedForm} <div class="normalized-form"> - <span class="normalized-label">Normalized:</span> + <span class="normalized-label">{$t('tools.ip-validator.results.normalized')}</span> <code class="normalized-value">{result.details.normalizedForm}</code> </div> {/if} @@ -556,7 +571,7 @@ <div class="errors-section"> <h4> <Icon name="alert-circle" size="sm" /> - Issues Found ({result.errors.length}) + {$t('tools.ip-validator.results.issuesFound', { count: result.errors.length })} </h4> <ul class="error-list"> {#each result.errors as error (error)} @@ -574,7 +589,7 @@ <div class="warnings-section"> <h4> <Icon name="alert-triangle" size="sm" /> - Warnings ({result.warnings.length}) + {$t('tools.ip-validator.results.warnings', { count: result.warnings.length })} </h4> <ul class="warning-list"> {#each result.warnings as warning (warning)} @@ -592,50 +607,52 @@ <div class="details-section"> <h4> <Icon name="info" size="sm" /> - Address Details + {$t('tools.ip-validator.results.addressDetails')} </h4> <div class="details-grid"> {#if result.details.addressType} <div class="detail-item"> - <span class="detail-label">Type:</span> + <span class="detail-label">{$t('tools.ip-validator.results.type')}</span> <span class="detail-value">{result.details.addressType}</span> </div> {/if} {#if result.details.scope} <div class="detail-item"> - <span class="detail-label">Scope:</span> + <span class="detail-label">{$t('tools.ip-validator.results.scope')}</span> <span class="detail-value">{result.details.scope}</span> </div> {/if} {#if result.details.isPrivate !== undefined} <div class="detail-item"> - <span class="detail-label">Routing:</span> + <span class="detail-label">{$t('tools.ip-validator.results.routing')}</span> <span class="detail-value {result.details.isPrivate ? 'private' : 'public'}"> - {result.details.isPrivate ? 'Private' : 'Public'} + {result.details.isPrivate + ? $t('tools.ip-validator.results.private') + : $t('tools.ip-validator.results.public')} </span> </div> {/if} {#if result.details.compressedForm} <div class="detail-item"> - <span class="detail-label">Compressed:</span> + <span class="detail-label">{$t('tools.ip-validator.results.compressed')}</span> <code class="detail-value compressed">{result.details.compressedForm}</code> </div> {/if} {#if result.details.embeddedIPv4} <div class="detail-item"> - <span class="detail-label">Embedded IPv4:</span> + <span class="detail-label">{$t('tools.ip-validator.results.embeddedIPv4')}</span> <code class="detail-value embedded">{result.details.embeddedIPv4}</code> </div> {/if} {#if result.details.zoneId} <div class="detail-item"> - <span class="detail-label">Zone ID:</span> + <span class="detail-label">{$t('tools.ip-validator.results.zoneId')}</span> <code class="detail-value zone">%{result.details.zoneId}</code> </div> {/if} @@ -643,7 +660,7 @@ {#if result.details.info && result.details.info.length > 0} <div class="info-section"> - <h5>Additional Information</h5> + <h5>{$t('tools.ip-validator.results.additionalInfo')}</h5> <ul class="info-list"> {#each result.details.info as info (info)} <li class="info-item"> @@ -664,31 +681,23 @@ <section class="about-content"> <div class="about-grid"> <div class="about-section"> - <h3>How to Tell if an IP Address is Valid</h3> + <h3>{$t('tools.ip-validator.education.howToTell.title')}</h3> <p> - Valid IP addresses follow specific rules. For IPv4, you need exactly four numbers (0-255) separated by dots, - like 192.168.1.1. For IPv6, you need eight groups of hex digits separated by colons, though you can compress - consecutive zeros with :: (like 2001:db8::1). The validator checks these rules and tells you exactly what's - wrong when something doesn't match. + {$t('tools.ip-validator.education.howToTell.description')} </p> </div> <div class="about-section"> - <h3>What Happens When Addresses Are Invalid</h3> + <h3>{$t('tools.ip-validator.education.whatHappens.title')}</h3> <p> - Invalid IP addresses cause real problems. Your router might reject them, network connections fail, or software - crashes. Common mistakes include typos like "192.168.1.256" (256 is too big), missing parts like "192.168.1", - or extra zeros like "192.168.01.01". This tool catches these errors before they break your network setup. + {$t('tools.ip-validator.education.whatHappens.description')} </p> </div> <div class="about-section"> - <h3>Why Some Addresses Have Warnings</h3> + <h3>{$t('tools.ip-validator.education.whyWarnings.title')}</h3> <p> - Some valid addresses come with warnings because they have special meanings. For example, addresses ending in - .0 are usually network addresses, and ones ending in .255 are broadcast addresses. Private addresses like - 192.168.x.x won't work on the internet. The tool explains what each address type means so you know if it's - right for your use case. + {$t('tools.ip-validator.education.whyWarnings.description')} </p> </div> </div> diff --git a/src/lib/components/tools/IPv6Normalize.svelte b/src/lib/components/tools/IPv6Normalize.svelte index eb54faa1..3c10ce4c 100644 --- a/src/lib/components/tools/IPv6Normalize.svelte +++ b/src/lib/components/tools/IPv6Normalize.svelte @@ -2,6 +2,14 @@ import { normalizeIPv6Addresses, type IPv6NormalizeResult } from '$lib/utils/ipv6-normalize.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let inputText = $state( '2001:0db8:0000:0000:0000:ff00:0042:8329\n2001:db8:0:0:1:0:0:1\n2001:0db8:0001:0000:0000:0ab9:C0A8:0102\n2001:db8::1\nfe80::1%eth0', @@ -25,7 +33,7 @@ result = { normalizations: [], summary: { totalInputs: 0, validInputs: 0, invalidInputs: 0, alreadyNormalizedInputs: 0 }, - errors: [error instanceof Error ? error.message : 'Unknown error'], + errors: [error instanceof Error ? error.message : $t('tools.ipv6_normalize.errors.unknownError')], }; } finally { isLoading = false; @@ -41,7 +49,15 @@ let mimeType = 'text/plain'; if (format === 'csv') { - const headers = 'Input,Normalized,Valid,Compression Applied,Leading Zeros Removed,Lowercase Applied,Error'; + const headers = [ + $t('tools.ipv6_normalize.csvHeaders.input'), + $t('tools.ipv6_normalize.csvHeaders.normalized'), + $t('tools.ipv6_normalize.csvHeaders.valid'), + $t('tools.ipv6_normalize.csvHeaders.compressionApplied'), + $t('tools.ipv6_normalize.csvHeaders.leadingZerosRemoved'), + $t('tools.ipv6_normalize.csvHeaders.lowercaseApplied'), + $t('tools.ipv6_normalize.csvHeaders.error'), + ].join(','); const rows = result.normalizations.map( (norm) => `"${norm.input}","${norm.normalized}","${norm.isValid}","${norm.compressionApplied}","${norm.leadingZerosRemoved}","${norm.lowercaseApplied}","${norm.error || ''}"`, @@ -93,34 +109,30 @@ <div class="card"> <header class="card-header"> - <h2>IPv6 Normalizer</h2> + <h2>{$t('tools.ipv6_normalize.title')}</h2> <p> - Normalize IPv6 addresses to RFC 5952 canonical form with lowercase, zero compression, and leading zero removal + {$t('tools.ipv6_normalize.description')} </p> </header> <div class="input-section"> <div class="input-group"> - <label for="inputs">IPv6 Addresses</label> - <textarea - id="inputs" - bind:value={inputText} - placeholder="2001:0db8:0000:0000:0000:ff00:0042:8329 2001:db8:0:0:1:0:0:1 2001:0db8:0001:0000:0000:0ab9:C0A8:0102 fe80::1%eth0" - rows="6" + <label for="inputs">{$t('tools.ipv6_normalize.input.label')}</label> + <textarea id="inputs" bind:value={inputText} placeholder={$t('tools.ipv6_normalize.input.placeholder')} rows="6" ></textarea> <div class="input-help"> - Enter one IPv6 address per line. Supports zone identifiers (%) and IPv4-mapped addresses + {$t('tools.ipv6_normalize.input.help')} </div> </div> <div class="rfc-info"> - <h3>RFC 5952 Normalization Rules</h3> + <h3>{$t('tools.ipv6_normalize.rfc.title')}</h3> + <p>{$t('tools.ipv6_normalize.rfc.description')}</p> <ul> - <li>Convert hexadecimal to lowercase</li> - <li>Remove leading zeros in each group</li> - <li>Compress longest sequence of consecutive zero groups with ::</li> - <li>Preserve zone identifiers (%)</li> - <li>Support IPv4-mapped IPv6 addresses</li> + <li>{$t('tools.ipv6_normalize.rfc.rules.lowercase')}</li> + <li>{$t('tools.ipv6_normalize.rfc.rules.leadingZeros')}</li> + <li>{$t('tools.ipv6_normalize.rfc.rules.compression')}</li> + <li>{$t('tools.ipv6_normalize.rfc.rules.singleZero')}</li> </ul> </div> </div> @@ -128,7 +140,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Normalizing addresses... + {$t('tools.ipv6_normalize.actions.normalizing')} </div> {/if} @@ -136,7 +148,7 @@ <div class="results"> {#if result.errors.length > 0} <div class="errors"> - <h3><Icon name="alert-triangle" /> Errors</h3> + <h3><Icon name="alert-triangle" /> {$t('tools.ipv6_normalize.errors.title')}</h3> {#each result.errors as error (error)} <div class="error-item">{error}</div> {/each} @@ -145,23 +157,23 @@ {#if result.normalizations.length > 0} <div class="summary"> - <h3>Normalization Summary</h3> + <h3>{$t('tools.ipv6_normalize.summary.title')}</h3> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{result.summary.totalInputs}</span> - <span class="stat-label">Total Inputs</span> + <span class="stat-label">{$t('tools.ipv6_normalize.summary.totalInputs')}</span> </div> <div class="stat valid"> <span class="stat-value">{result.summary.validInputs}</span> - <span class="stat-label">Valid</span> + <span class="stat-label">{$t('tools.ipv6_normalize.summary.validInputs')}</span> </div> <div class="stat invalid"> <span class="stat-value">{result.summary.invalidInputs}</span> - <span class="stat-label">Invalid</span> + <span class="stat-label">{$t('tools.ipv6_normalize.summary.invalidInputs')}</span> </div> <div class="stat already-normalized"> <span class="stat-value">{result.summary.alreadyNormalizedInputs}</span> - <span class="stat-label">Already Normalized</span> + <span class="stat-label">{$t('tools.ipv6_normalize.summary.alreadyNormalized')}</span> </div> </div> </div> diff --git a/src/lib/components/tools/IPv6NotationConverter.svelte b/src/lib/components/tools/IPv6NotationConverter.svelte index 685eb76f..8c3ff500 100644 --- a/src/lib/components/tools/IPv6NotationConverter.svelte +++ b/src/lib/components/tools/IPv6NotationConverter.svelte @@ -3,6 +3,14 @@ import Tooltip from '$lib/components/global/Tooltip.svelte'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); interface Props { mode: 'expand' | 'compress'; @@ -17,18 +25,38 @@ let selectedExampleIndex = $state<number | null>(null); /* Common IPv6 example addresses for testing */ - const exampleAddresses = [ - { label: 'Documentation Prefix', compressed: '2001:db8::', expanded: '2001:0db8:0000:0000:0000:0000:0000:0000' }, - { label: 'Loopback Address', compressed: '::1', expanded: '0000:0000:0000:0000:0000:0000:0000:0001' }, - { label: 'Link-Local Address', compressed: 'fe80::1', expanded: 'fe80:0000:0000:0000:0000:0000:0000:0001' }, + const exampleAddresses = $derived([ + { + label: $t('tools.ipv6_notation_converter.examples.documentationPrefix'), + compressed: '2001:db8::', + expanded: '2001:0db8:0000:0000:0000:0000:0000:0000', + }, + { + label: $t('tools.ipv6_notation_converter.examples.loopbackAddress'), + compressed: '::1', + expanded: '0000:0000:0000:0000:0000:0000:0000:0001', + }, + { + label: $t('tools.ipv6_notation_converter.examples.linkLocalAddress'), + compressed: 'fe80::1', + expanded: 'fe80:0000:0000:0000:0000:0000:0000:0001', + }, { - label: 'Global Unicast', + label: $t('tools.ipv6_notation_converter.examples.globalUnicast'), compressed: '2001:db8:85a3::8a2e:370:7334', expanded: '2001:0db8:85a3:0000:0000:8a2e:0370:7334', }, - { label: 'IPv4-mapped IPv6', compressed: '::ffff:192.0.2.1', expanded: '0000:0000:0000:0000:0000:ffff:c000:0201' }, - { label: 'Multicast Address', compressed: 'ff02::1', expanded: 'ff02:0000:0000:0000:0000:0000:0000:0001' }, - ]; + { + label: $t('tools.ipv6_notation_converter.examples.ipv4MappedIPv6'), + compressed: '::ffff:192.0.2.1', + expanded: '0000:0000:0000:0000:0000:ffff:c000:0201', + }, + { + label: $t('tools.ipv6_notation_converter.examples.multicastAddress'), + compressed: 'ff02::1', + expanded: 'ff02:0000:0000:0000:0000:0000:0000:0001', + }, + ]); /* Set example address */ function setExample(address: string, index: number) { @@ -54,13 +82,13 @@ outputAddress = ''; if (!inputAddress.trim()) { - conversionError = 'Please enter an IPv6 address'; + conversionError = $t('tools.ipv6_notation_converter.errors.enterAddress'); return; } const validation = validateIPv6Address(inputAddress); if (!validation.valid) { - conversionError = validation.error || 'Invalid IPv6 address format'; + conversionError = validation.error || $t('tools.ipv6_notation_converter.errors.invalidFormat'); return; } @@ -71,7 +99,8 @@ outputAddress = compressIPv6(inputAddress); } } catch (error) { - conversionError = error instanceof Error ? error.message : 'Conversion failed'; + conversionError = + error instanceof Error ? error.message : $t('tools.ipv6_notation_converter.errors.conversionFailed'); } } @@ -92,16 +121,16 @@ <!-- Input Section --> <div class="converter-section"> - <h3>Input IPv6 Address</h3> + <h3>{$t('tools.ipv6_notation_converter.input.label')}</h3> <div class="input-group"> <div class="form-group"> <label for="ipv6-input"> - IPv6 Address + {$t('tools.ipv6_notation_converter.input.label')} <Tooltip text={mode === 'expand' - ? 'Enter compressed IPv6 address to expand' - : 'Enter expanded IPv6 address to compress'} + ? $t('tools.ipv6_notation_converter.input.tooltipExpand') + : $t('tools.ipv6_notation_converter.input.tooltipCompress')} > <Icon name="help" size="sm" /> </Tooltip> @@ -131,7 +160,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h3>Common Examples</h3> + <h3>{$t('tools.ipv6_notation_converter.examples.title')}</h3> </summary> <div class="examples-grid"> {#each exampleAddresses as example, index (`example-${index}`)} @@ -154,14 +183,16 @@ <!-- Output Section --> {#if outputAddress && !conversionError} <div class="output-section"> - <h3>Converted Address</h3> + <h3>{$t('tools.ipv6_notation_converter.output.label')}</h3> <div class="conversion-result"> <div class="result-card success"> <div class="result-header"> <h4> <Icon name={mode === 'expand' ? 'maximize' : 'minimize'} size="sm" /> - {mode === 'expand' ? 'Expanded Format' : 'Compressed Format'} + {mode === 'expand' + ? $t('tools.ipv6_notation_converter.output.expandedForm') + : $t('tools.ipv6_notation_converter.output.compressedForm')} </h4> </div> @@ -183,11 +214,25 @@ <!-- Comparison View --> <div class="comparison-view"> <div class="comparison-item"> - <span class="comparison-label">Input ({mode === 'expand' ? 'Compressed' : 'Expanded'}):</span> + <span class="comparison-label" + >{$t('tools.ipv6_notation_converter.comparison.input', { + format: + mode === 'expand' + ? $t('tools.ipv6_notation_converter.modes.compress') + : $t('tools.ipv6_notation_converter.modes.expand'), + })}:</span + > <code class="comparison-address input">{inputAddress}</code> </div> <div class="comparison-item"> - <span class="comparison-label">Output ({mode === 'expand' ? 'Expanded' : 'Compressed'}):</span> + <span class="comparison-label" + >{$t('tools.ipv6_notation_converter.comparison.output', { + format: + mode === 'expand' + ? $t('tools.ipv6_notation_converter.modes.expand') + : $t('tools.ipv6_notation_converter.modes.compress'), + })}:</span + > <code class="comparison-address output">{outputAddress}</code> </div> </div> @@ -195,17 +240,24 @@ <!-- Character Count --> <div class="stats-grid"> <div class="stat-item"> - <span class="stat-label">Input Length</span> - <span class="stat-value">{inputAddress.length} characters</span> + <span class="stat-label">{$t('tools.ipv6_notation_converter.stats.inputLength')}</span> + <span class="stat-value" + >{$t('tools.ipv6_notation_converter.stats.characters', { count: inputAddress.length })}</span + > </div> <div class="stat-item"> - <span class="stat-label">Output Length</span> - <span class="stat-value">{outputAddress.length} characters</span> + <span class="stat-label">{$t('tools.ipv6_notation_converter.stats.outputLength')}</span> + <span class="stat-value" + >{$t('tools.ipv6_notation_converter.stats.characters', { count: outputAddress.length })}</span + > </div> <div class="stat-item"> - <span class="stat-label">Difference</span> + <span class="stat-label">{$t('tools.ipv6_notation_converter.stats.difference')}</span> <span class="stat-value {outputAddress.length > inputAddress.length ? 'expanded' : 'compressed'}"> - {outputAddress.length > inputAddress.length ? '+' : ''}{outputAddress.length - inputAddress.length} characters + {outputAddress.length > inputAddress.length ? '+' : ''}{$t( + 'tools.ipv6_notation_converter.stats.charactersChange', + { count: outputAddress.length - inputAddress.length }, + )} </span> </div> </div> diff --git a/src/lib/components/tools/IPv6SubnetCalculator.svelte b/src/lib/components/tools/IPv6SubnetCalculator.svelte index 739326ad..600ece27 100644 --- a/src/lib/components/tools/IPv6SubnetCalculator.svelte +++ b/src/lib/components/tools/IPv6SubnetCalculator.svelte @@ -10,6 +10,14 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import { useClipboard } from '$lib/composables'; import { goto } from '$app/navigation'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); const versionOptions = [ { value: 'ipv4' as const, label: 'IPv4' }, @@ -79,7 +87,7 @@ /* Get prefix description */ function getPrefixDescription(prefix: number): string { const common = commonPrefixes.find((p) => p.prefix === prefix); - return common?.description || `/${prefix} - Custom prefix length`; + return common?.description || `/${prefix} - ${$t('tools.ipv6_subnet_calculator.form.customPrefixLength')}`; } /* Format large numbers */ @@ -87,7 +95,7 @@ if (num.includes('β‰ˆ')) return num; const cleaned = num.replace(/[,\s]/g, ''); if (cleaned.length > 15) { - return `${cleaned.slice(0, 6)}... (${cleaned.length} digits)`; + return `${cleaned.slice(0, 6)}... (${cleaned.length} ${$t('tools.ipv6_subnet_calculator.format.digits')})`; } return num; } @@ -104,8 +112,8 @@ </script> <ToolContentContainer - title="IPv6 Subnet Calculator" - description="Calculate IPv6 subnet information with 128-bit addressing and modern network prefix notation." + title={$t('tools.ipv6_subnet_calculator.title')} + description={$t('tools.ipv6_subnet_calculator.description')} navOptions={versionOptions} bind:selectedNav={selectedVersion} onNavChange={handleVersionChange} @@ -113,28 +121,28 @@ > <!-- Input Section --> <div class="input-section"> - <h3>Network Configuration</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.networkConfiguration')}</h3> <div class="input-grid"> <div class="form-group"> - <label for="ipv6-input">IPv6 Network Address</label> + <label for="ipv6-input">{$t('tools.ipv6_subnet_calculator.form.networkAddressLabel')}</label> <div class="input-wrapper"> <input id="ipv6-input" type="text" bind:value={networkAddress} oninput={(e) => handleAddressInput((e.target as HTMLInputElement)?.value || '')} - placeholder="2001:db8::/64" + placeholder={$t('tools.ipv6_subnet_calculator.form.networkAddressPlaceholder')} class="ipv6-input" /> - <Tooltip text="Enter IPv6 address with prefix (e.g., 2001:db8::/64) or address only"> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.networkAddressHelp')}> <Icon name="help" size="sm" /> </Tooltip> </div> </div> <div class="form-group"> - <label for="prefix-input">Prefix Length</label> + <label for="prefix-input">{$t('tools.ipv6_subnet_calculator.form.prefixLengthLabel')}</label> <div class="prefix-controls"> <span class="prefix-display">/{prefixLength}</span> <input id="prefix-slider" type="range" min="1" max="128" bind:value={prefixLength} class="prefix-slider" /> @@ -147,49 +155,49 @@ <!-- Common Presets --> <div class="presets-section"> - <h3>Common IPv6 Networks</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.commonNetworks')}</h3> <div class="presets-grid"> <button type="button" class="preset-btn {activePreset === 'doc-48' ? 'active' : ''}" onclick={() => setPreset('2001:db8::', 48)} > - Documentation /48 + {$t('tools.ipv6_subnet_calculator.presets.documentation48')} </button> <button type="button" class="preset-btn {activePreset === 'doc-64' ? 'active' : ''}" onclick={() => setPreset('2001:db8::', 64)} > - Standard Subnet /64 + {$t('tools.ipv6_subnet_calculator.presets.standardSubnet64')} </button> <button type="button" class="preset-btn {activePreset === 'link-local' ? 'active' : ''}" onclick={() => setPreset('fe80::', 64)} > - Link-Local /64 + {$t('tools.ipv6_subnet_calculator.presets.linkLocal64')} </button> <button type="button" class="preset-btn {activePreset === 'loopback' ? 'active' : ''}" onclick={() => setPreset('::1', 128)} > - Loopback /128 + {$t('tools.ipv6_subnet_calculator.presets.loopback128')} </button> <button type="button" class="preset-btn {activePreset === 'google-dns' ? 'active' : ''}" onclick={() => setPreset('2001:4860:4860::', 48)} > - Google DNS /48 + {$t('tools.ipv6_subnet_calculator.presets.googleDns48')} </button> <button type="button" class="preset-btn {activePreset === 'multicast' ? 'active' : ''}" onclick={() => setPreset('ff02::1', 128)} > - Multicast All Nodes + {$t('tools.ipv6_subnet_calculator.presets.multicastAllNodes')} </button> </div> </div> @@ -199,10 +207,10 @@ <div class="results-section"> <!-- Network Information --> <div class="info-panel"> - <h3>IPv6 Subnet Information</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.subnetInfo')}</h3> <div class="info-grid"> <div class="info-item"> - <span class="info-label">Network</span> + <span class="info-label">{$t('tools.ipv6_subnet_calculator.results.network')}</span> <div class="value-copy"> <span class="info-value">{subnetResult.subnet.networkCompressed}/{subnetResult.subnet.prefixLength}</span> <button @@ -221,7 +229,7 @@ </div> <div class="info-item"> - <span class="info-label">Total Addresses</span> + <span class="info-label">{$t('tools.ipv6_subnet_calculator.results.totalAddresses')}</span> <span class="info-value large-number">{formatLargeNumber(subnetResult.subnet.totalAddresses)}</span> </div> </div> @@ -230,11 +238,14 @@ <!-- Detailed Information --> <div class="details-section"> <div class="details-header"> - <h3>Network Details</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.networkDetails')}</h3> <div class="header-actions"> <button type="button" class="btn btn-secondary btn-sm" onclick={() => (showBinaryView = !showBinaryView)}> <Icon name="binary" size="sm" /> - {showBinaryView ? 'Hide' : 'Show'} Binary + {showBinaryView + ? $t('tools.ipv6_subnet_calculator.actions.hide') + : $t('tools.ipv6_subnet_calculator.actions.show')} + {$t('tools.ipv6_subnet_calculator.actions.binary')} </button> </div> </div> @@ -242,8 +253,8 @@ <div class="details-grid"> <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Network Address (Compressed)</span> - <Tooltip text="Compressed IPv6 notation using :: for consecutive zero groups"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.networkCompressed')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.compressedNotation')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -262,8 +273,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Network Address (Expanded)</span> - <Tooltip text="Full 128-bit IPv6 representation with all zero groups shown"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.networkExpanded')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.expandedNotation')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -281,8 +292,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Subnet Mask</span> - <Tooltip text="IPv6 subnet mask showing network portion (compressed format)"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.subnetMask')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.subnetMask')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -300,8 +311,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Address Range</span> - <Tooltip text="First and last assignable addresses in the subnet"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.addressRange')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.addressRange')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -323,8 +334,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Assignable Addresses</span> - <Tooltip text="Number of addresses available for host assignment (excluding network/broadcast concepts)"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.assignableAddresses')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.assignableAddresses')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -343,8 +354,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span class="detail-label">Reverse DNS Zone</span> - <Tooltip text="PTR record zone for reverse DNS lookups"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.reverseDnsZone')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.reverseDns')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -363,8 +374,8 @@ {#if showBinaryView} <div class="detail-item full-width"> <div class="detail-label-wrapper"> - <span class="detail-label">Binary Prefix Representation</span> - <Tooltip text="128-bit binary representation showing network (1) and host (0) bits"> + <span class="detail-label">{$t('tools.ipv6_subnet_calculator.results.binaryPrefix')}</span> + <Tooltip text={$t('tools.ipv6_subnet_calculator.tooltips.binaryRepresentation')}> <Icon name="help" size="sm" /> </Tooltip> </div> @@ -385,26 +396,31 @@ <!-- IPv6 Address Structure Visualization --> <div class="visualization-section"> - <h3>IPv6 Address Structure</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.addressStructure')}</h3> <div class="address-structure"> <div class="structure-header"> - <h4>128-bit Address Breakdown</h4> - <p>Showing network and host portions for {subnetResult.subnet.networkCompressed}/{prefixLength}</p> + <h4>{$t('tools.ipv6_subnet_calculator.sections.addressBreakdown')}</h4> + <p> + {$t('tools.ipv6_subnet_calculator.visualization.showingPortionsFor')} + {subnetResult.subnet.networkCompressed}/{prefixLength} + </p> </div> <div class="bit-visualization"> <div class="bit-section network-bits"> <div class="bit-header"> - <span class="bit-label">Network Portion</span> - <span class="bit-count">{prefixLength} bits</span> + <span class="bit-label">{$t('tools.ipv6_subnet_calculator.visualization.networkPortion')}</span> + <span class="bit-count">{prefixLength} {$t('tools.ipv6_subnet_calculator.visualization.bits')}</span> </div> <div class="bit-bar" style="width: {(prefixLength / 128) * 100}%"></div> </div> <div class="bit-section host-bits"> <div class="bit-header"> - <span class="bit-label">Host Portion</span> - <span class="bit-count">{128 - prefixLength} bits</span> + <span class="bit-label">{$t('tools.ipv6_subnet_calculator.visualization.hostPortion')}</span> + <span class="bit-count" + >{128 - prefixLength} {$t('tools.ipv6_subnet_calculator.visualization.bits')}</span + > </div> <div class="bit-bar" style="width: {((128 - prefixLength) / 128) * 100}%"></div> </div> @@ -426,7 +442,7 @@ <!-- Error Display --> <div class="results-section"> <div class="info-panel error"> - <h3>Calculation Error</h3> + <h3>{$t('tools.ipv6_subnet_calculator.sections.calculationError')}</h3> <p class="error-message">{subnetResult.error}</p> </div> </div> diff --git a/src/lib/components/tools/IPv6Teredo.svelte b/src/lib/components/tools/IPv6Teredo.svelte index 3294539d..da4e0b61 100644 --- a/src/lib/components/tools/IPv6Teredo.svelte +++ b/src/lib/components/tools/IPv6Teredo.svelte @@ -2,6 +2,14 @@ import { tooltip } from '$lib/actions/tooltip.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let input = $state('2001:0000:4136:e378:8000:63bf:3fff:fdd2'); let result = $state<{ @@ -28,28 +36,28 @@ let selectedExample = $state<string | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Microsoft Teredo', + label: $t('tools.ipv6_teredo.examples.microsoftTeredo'), address: '2001:0000:4136:e378:8000:63bf:3fff:fdd2', - description: 'Microsoft Teredo server example', + description: $t('tools.ipv6_teredo.examples.microsoftTeredoDesc'), }, { - label: 'Compressed Form', + label: $t('tools.ipv6_teredo.examples.compressedForm'), address: '2001::4136:e378:8000:63bf:3fff:fdd2', - description: 'Same address in compressed format', + description: $t('tools.ipv6_teredo.examples.compressedFormDesc'), }, { - label: 'Behind NAT (Cone)', + label: $t('tools.ipv6_teredo.examples.behindNATCone'), address: '2001:0000:5ef5:79fb:0000:5efe:c0a8:0101', - description: 'Client behind cone NAT', + description: $t('tools.ipv6_teredo.examples.behindNATConeDesc'), }, { - label: 'Direct Connection', + label: $t('tools.ipv6_teredo.examples.directConnection'), address: '2001:0000:4136:e378:ffff:ffff:ffff:ffff', - description: 'Direct connection without NAT', + description: $t('tools.ipv6_teredo.examples.directConnectionDesc'), }, - ]; + ]); function loadExample(example: (typeof examples)[0]) { input = example.address; @@ -264,8 +272,8 @@ <div class="card"> <header class="card-header"> - <h1>IPv6 Teredo Parser</h1> - <p>Parse Teredo IPv6 addresses to extract server IPv4, flags, mapped port, and client IPv4</p> + <h1>{$t('tools.ipv6_teredo.title')}</h1> + <p>{$t('tools.ipv6_teredo.description')}</p> </header> <!-- Educational Overview --> @@ -274,21 +282,23 @@ <div class="overview-item"> <Icon name="tunnel" size="sm" /> <div> - <strong>Teredo Tunneling:</strong> Allows IPv6 connectivity for hosts behind IPv4 NATs by encapsulating IPv6 packets - in IPv4 UDP. + <strong>{$t('tools.ipv6_teredo.overview.tunneling.title')}</strong> + {$t('tools.ipv6_teredo.overview.tunneling.description')} </div> </div> <div class="overview-item"> <Icon name="globe" size="sm" /> <div> - <strong>Address Format:</strong> <code>2001:0000:SSSS:SSSS:FFFF:PPPP:CCCC:CCCC</code> where components are encoded - and obfuscated. + <strong>{$t('tools.ipv6_teredo.overview.format.title')}</strong> + <code>2001:0000:SSSS:SSSS:FFFF:PPPP:CCCC:CCCC</code> + {$t('tools.ipv6_teredo.overview.format.description')} </div> </div> <div class="overview-item"> <Icon name="shield" size="sm" /> <div> - <strong>Obfuscation:</strong> Client IP and port are XOR'ed to prevent some NATs from interfering with the tunnel. + <strong>{$t('tools.ipv6_teredo.overview.obfuscation.title')}</strong> + {$t('tools.ipv6_teredo.overview.obfuscation.description')} </div> </div> </div> @@ -299,7 +309,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools.ipv6_teredo.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -319,23 +329,20 @@ <!-- Input Section --> <section class="input-section"> <div class="input-group"> - <label - for="teredo-input" - use:tooltip={'Enter a Teredo IPv6 address starting with 2001:0000:: (or 2001::) to parse its components'} - > + <label for="teredo-input" use:tooltip={$t('tools.ipv6_teredo.input.help')}> <Icon name="tunnel" size="sm" /> - Teredo IPv6 Address + {$t('tools.ipv6_teredo.input.label')} </label> <input id="teredo-input" type="text" bind:value={input} oninput={handleInputChange} - placeholder="2001:0000:4136:e378:8000:63bf:3fff:fdd2" + placeholder={$t('tools.ipv6_teredo.input.placeholder')} class="teredo-input {result?.success === true ? 'valid' : result?.success === false ? 'invalid' : ''}" spellcheck="false" /> - <div class="input-hint">Enter any Teredo IPv6 address in compressed or full format</div> + <div class="input-hint">{$t('tools.ipv6_teredo.input.help')}</div> </div> </section> @@ -346,46 +353,55 @@ <div class="results-header"> <h3> <Icon name="check-circle" size="sm" /> - Teredo Components + {$t('tools.ipv6_teredo.results.validAddress')} </h3> </div> <!-- Address Breakdown --> <div class="address-breakdown"> <div class="breakdown-header"> - <h4>Address Structure</h4> + <h4>{$t('tools.ipv6_teredo.breakdown.title')}</h4> <code class="full-address">{result.details.fullAddress}</code> </div> <div class="breakdown-grid"> <div class="breakdown-section prefix"> - <div class="section-label">Prefix</div> + <div class="section-label">{$t('tools.ipv6_teredo.breakdown.prefix.label')}</div> <code class="section-value">{result.components.prefix}</code> - <div class="section-description">Teredo identifier</div> + <div class="section-description">{$t('tools.ipv6_teredo.breakdown.prefix.description')}</div> </div> <div class="breakdown-section server"> - <div class="section-label">Server</div> + <div class="section-label">{$t('tools.ipv6_teredo.breakdown.server.label')}</div> <code class="section-value">{result.details.addressGroups[2]}:{result.details.addressGroups[3]}</code> - <div class="section-description">IPv4: {result.components.serverIPv4}</div> + <div class="section-description"> + {$t('tools.ipv6_teredo.breakdown.server.description')} + {result.components.serverIPv4} + </div> </div> <div class="breakdown-section flags"> - <div class="section-label">Flags</div> + <div class="section-label">{$t('tools.ipv6_teredo.breakdown.flags.label')}</div> <code class="section-value">{result.details.addressGroups[4]}</code> <div class="section-description">{result.components.flags}</div> </div> <div class="breakdown-section port"> - <div class="section-label">Port</div> + <div class="section-label">{$t('tools.ipv6_teredo.breakdown.port.label')}</div> <code class="section-value">{result.details.addressGroups[5]}</code> - <div class="section-description">Actual: {result.components.clientPort}</div> + <div class="section-description"> + {$t('tools.ipv6_teredo.breakdown.port.description')} + {result.components.clientPort} + </div> </div> <div class="breakdown-section client"> - <div class="section-label">Client</div> + <div class="section-label">{$t('tools.ipv6_teredo.breakdown.client.label')}</div> <code class="section-value">{result.details.addressGroups[6]}:{result.details.addressGroups[7]}</code> - <div class="section-description">IPv4: {result.components.clientIPv4}</div> + <div class="section-description"> + {$t('tools.ipv6_teredo.breakdown.client.description')} + {result.components.clientIPv4} + </div> </div> </div> </div> @@ -394,14 +410,14 @@ <div class="components-section"> <h4> <Icon name="layers" size="sm" /> - Extracted Components + {$t('tools.ipv6_teredo.components.title')} </h4> <div class="components-grid"> <div class="component-card server"> <div class="component-header"> <Icon name="server" size="sm" /> - <span class="component-title">Teredo Server</span> + <span class="component-title">{$t('tools.ipv6_teredo.components.teredoServer.title')}</span> </div> <div class="component-content"> <code class="component-value">{result.components.serverIPv4}</code> @@ -412,13 +428,13 @@ <Icon name={clipboard.isCopied('server') ? 'check' : 'copy'} size="sm" /> </button> </div> - <div class="component-description">The Teredo relay server handling this tunnel</div> + <div class="component-description">{$t('tools.ipv6_teredo.components.teredoServer.description')}</div> </div> <div class="component-card client"> <div class="component-header"> <Icon name="user" size="sm" /> - <span class="component-title">Client IPv4</span> + <span class="component-title">{$t('tools.ipv6_teredo.components.clientIPv4.title')}</span> </div> <div class="component-content"> <code class="component-value">{result.components.clientIPv4}</code> @@ -429,13 +445,13 @@ <Icon name={clipboard.isCopied('client') ? 'check' : 'copy'} size="sm" /> </button> </div> - <div class="component-description">The client's external IPv4 address (XOR decoded)</div> + <div class="component-description">{$t('tools.ipv6_teredo.components.clientIPv4.description')}</div> </div> <div class="component-card port"> <div class="component-header"> <Icon name="hash" size="sm" /> - <span class="component-title">Client Port</span> + <span class="component-title">{$t('tools.ipv6_teredo.components.clientPort.title')}</span> </div> <div class="component-content"> <code class="component-value">{result.components.clientPort}</code> @@ -446,20 +462,22 @@ <Icon name={clipboard.isCopied('port') ? 'check' : 'copy'} size="sm" /> </button> </div> - <div class="component-description">The client's external port (XOR decoded with FFFF)</div> + <div class="component-description">{$t('tools.ipv6_teredo.components.clientPort.description')}</div> </div> <div class="component-card flags"> <div class="component-header"> <Icon name="flag" size="sm" /> - <span class="component-title">NAT Type</span> + <span class="component-title">{$t('tools.ipv6_teredo.components.natType.title')}</span> </div> <div class="component-content"> <span class="component-value {result.components.cone ? 'cone' : 'restricted'}" - >{result.components.cone ? 'Cone NAT' : 'Restricted NAT'}</span + >{result.components.cone + ? $t('tools.ipv6_teredo.natTypes.cone') + : $t('tools.ipv6_teredo.natTypes.symmetric')}</span > </div> - <div class="component-description">Indicates the type of NAT the client is behind</div> + <div class="component-description">{$t('tools.ipv6_teredo.components.natType.description')}</div> </div> </div> </div> diff --git a/src/lib/components/tools/IPv6ZoneID.svelte b/src/lib/components/tools/IPv6ZoneID.svelte index bd079d5f..e4da27ed 100644 --- a/src/lib/components/tools/IPv6ZoneID.svelte +++ b/src/lib/components/tools/IPv6ZoneID.svelte @@ -2,6 +2,14 @@ import { processIPv6ZoneIdentifiers, type IPv6ZoneResult } from '$lib/utils/ipv6-zone-id.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let inputText = $state('fe80::1\nfe80::1%eth0\nfe80::1234:5678:90ab:cdef%wlan0\n::1\n2001:db8::1\nff02::1%eth0'); let result = $state<IPv6ZoneResult | null>(null); @@ -29,7 +37,7 @@ addressesWithZones: 0, addressesRequiringZones: 0, }, - errors: [error instanceof Error ? error.message : 'Unknown error'], + errors: [error instanceof Error ? error.message : $t('tools.ipv6_zone_id.errors.unknownError')], }; } finally { isLoading = false; @@ -44,8 +52,18 @@ let filename = ''; if (format === 'csv') { - const headers = - 'Input,Has Zone ID,Address,Zone ID,Address Type,Requires Zone ID,With Zone,Without Zone,Valid,Error'; + const headers = [ + $t('tools.ipv6_zone_id.export.headers.input'), + $t('tools.ipv6_zone_id.export.headers.hasZoneId'), + $t('tools.ipv6_zone_id.export.headers.address'), + $t('tools.ipv6_zone_id.export.headers.zoneId'), + $t('tools.ipv6_zone_id.export.headers.addressType'), + $t('tools.ipv6_zone_id.export.headers.requiresZoneId'), + $t('tools.ipv6_zone_id.export.headers.withZone'), + $t('tools.ipv6_zone_id.export.headers.withoutZone'), + $t('tools.ipv6_zone_id.export.headers.valid'), + $t('tools.ipv6_zone_id.export.headers.error'), + ].join(','); const rows = result.processings.map( (proc) => `"${proc.input}","${proc.hasZoneId}","${proc.address}","${proc.zoneId}","${proc.addressType}","${proc.requiresZoneId}","${proc.processing.withZone}","${proc.processing.withoutZone}","${proc.isValid}","${proc.error || ''}"`, @@ -88,19 +106,19 @@ function getAddressTypeDescription(type: string): string { switch (type) { case 'link-local': - return 'Link-local address (fe80::/10)'; + return $t('tools.ipv6_zone_id.addressTypes.linkLocal'); case 'unique-local': - return 'Unique local address (fc00::/7)'; + return $t('tools.ipv6_zone_id.addressTypes.uniqueLocal'); case 'multicast': - return 'Multicast address (ff00::/8)'; + return $t('tools.ipv6_zone_id.addressTypes.multicast'); case 'global': - return 'Global unicast address'; + return $t('tools.ipv6_zone_id.addressTypes.global'); case 'loopback': - return 'Loopback address (::1)'; + return $t('tools.ipv6_zone_id.addressTypes.loopback'); case 'unspecified': - return 'Unspecified address (::)'; + return $t('tools.ipv6_zone_id.addressTypes.unspecified'); default: - return 'Unknown address type'; + return $t('tools.ipv6_zone_id.addressTypes.unknown'); } } @@ -115,37 +133,38 @@ <div class="card"> <header class="card-header"> - <h2>IPv6 Zone ID Handler</h2> - <p>Process IPv6 addresses with zone identifiers for link-local and multicast addresses</p> + <h2>{$t('tools.ipv6_zone_id.title')}</h2> + <p>{$t('tools.ipv6_zone_id.description')}</p> </header> <div class="input-section"> <div class="input-group"> - <label for="inputs">IPv6 Addresses</label> - <textarea - id="inputs" - bind:value={inputText} - placeholder="fe80::1 fe80::1%eth0 fe80::1234:5678:90ab:cdef%wlan0 ::1 2001:db8::1" - rows="6" + <label for="inputs">{$t('tools.ipv6_zone_id.input.label')}</label> + <textarea id="inputs" bind:value={inputText} placeholder={$t('tools.ipv6_zone_id.input.placeholder')} rows="6" ></textarea> <div class="input-help"> - Enter IPv6 addresses with or without zone identifiers (%). Zone IDs are interface names like eth0, wlan0, or - numeric IDs. + {$t('tools.ipv6_zone_id.input.help')} </div> </div> <div class="zone-info"> - <h3>Zone Identifier Information</h3> + <h3>{$t('tools.ipv6_zone_id.info.title')}</h3> <div class="info-section"> - <h4>When Zone IDs are Required:</h4> + <h4>{$t('tools.ipv6_zone_id.info.whenRequired.title')}</h4> <ul> - <li><strong>Link-local addresses</strong> (fe80::/10) - Almost always require zone IDs</li> - <li><strong>Multicast addresses</strong> (ff00::/8) - May require zone IDs depending on scope</li> + <li> + <strong>{$t('tools.ipv6_zone_id.info.whenRequired.linkLocal.type')}</strong> + {$t('tools.ipv6_zone_id.info.whenRequired.linkLocal.description')} + </li> + <li> + <strong>{$t('tools.ipv6_zone_id.info.whenRequired.multicast.type')}</strong> + {$t('tools.ipv6_zone_id.info.whenRequired.multicast.description')} + </li> </ul> </div> <div class="info-section"> - <h4>Common Zone Identifiers:</h4> + <h4>{$t('tools.ipv6_zone_id.info.commonIdentifiers.title')}</h4> <div class="zone-examples"> <code>eth0</code> <code>wlan0</code> @@ -161,7 +180,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Processing addresses... + {$t('tools.ipv6_zone_id.processing')} </div> {/if} @@ -169,7 +188,7 @@ <div class="results"> {#if result.errors.length > 0} <div class="errors"> - <h3><Icon name="alert-triangle" /> Errors</h3> + <h3><Icon name="alert-triangle" /> {$t('tools.ipv6_zone_id.results.errors.title')}</h3> {#each result.errors as error (error)} <div class="error-item">{error}</div> {/each} @@ -178,42 +197,42 @@ {#if result.processings.length > 0} <div class="summary"> - <h3>Processing Summary</h3> + <h3>{$t('tools.ipv6_zone_id.results.summary.title')}</h3> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{result.summary.totalInputs}</span> - <span class="stat-label">Total Inputs</span> + <span class="stat-label">{$t('tools.ipv6_zone_id.results.summary.totalInputs')}</span> </div> <div class="stat valid"> <span class="stat-value">{result.summary.validInputs}</span> - <span class="stat-label">Valid</span> + <span class="stat-label">{$t('tools.ipv6_zone_id.results.summary.valid')}</span> </div> <div class="stat invalid"> <span class="stat-value">{result.summary.invalidInputs}</span> - <span class="stat-label">Invalid</span> + <span class="stat-label">{$t('tools.ipv6_zone_id.results.summary.invalid')}</span> </div> <div class="stat with-zone"> <span class="stat-value">{result.summary.addressesWithZones}</span> - <span class="stat-label">With Zones</span> + <span class="stat-label">{$t('tools.ipv6_zone_id.results.summary.withZones')}</span> </div> <div class="stat require-zone"> <span class="stat-value">{result.summary.addressesRequiringZones}</span> - <span class="stat-label">Require Zones</span> + <span class="stat-label">{$t('tools.ipv6_zone_id.results.summary.requireZones')}</span> </div> </div> </div> <div class="processings"> <div class="processings-header"> - <h3>Zone ID Processing</h3> + <h3>{$t('tools.ipv6_zone_id.results.processing.title')}</h3> <div class="export-buttons"> <button onclick={() => exportResults('csv')}> <Icon name="csv-file" /> - Export CSV + {$t('tools.ipv6_zone_id.actions.exportCSV')} </button> <button onclick={() => exportResults('json')}> <Icon name="json-file" /> - Export JSON + {$t('tools.ipv6_zone_id.actions.exportJSON')} </button> </div> </div> @@ -224,14 +243,14 @@ <div class="card-header row"> <div class="address-info"> <div class="original-input"> - <span class="input-label">Input:</span> + <span class="input-label">{$t('tools.ipv6_zone_id.results.processing.input')}:</span> <div class="input-with-copy"> <code>{processing.input}</code> <button type="button" class:copied={clipboard.isCopied(`input-${processing.input}`)} onclick={() => clipboard.copy(processing.input, `input-${processing.input}`)} - title="Copy input" + title={$t('tools.ipv6_zone_id.actions.copyInput')} > <Icon name={clipboard.isCopied(`input-${processing.input}`) ? 'check' : 'copy'} size="xs" /> </button> @@ -252,14 +271,14 @@ <div class="processing-details"> <div class="address-breakdown"> <div class="breakdown-item"> - <span class="breakdown-label">Address:</span> + <span class="breakdown-label">{$t('tools.ipv6_zone_id.results.processing.address')}:</span> <div class="input-with-copy"> <code>{processing.address}</code> <button type="button" class:copied={clipboard.isCopied(`address-${processing.address}`)} onclick={() => clipboard.copy(processing.address, `address-${processing.address}`)} - title="Copy address" + title={$t('tools.ipv6_zone_id.actions.copyAddress')} > <Icon name={clipboard.isCopied(`address-${processing.address}`) ? 'check' : 'copy'} @@ -271,14 +290,14 @@ {#if processing.hasZoneId} <div class="breakdown-item"> - <span class="breakdown-label">Zone ID:</span> + <span class="breakdown-label">{$t('tools.ipv6_zone_id.results.processing.zoneId')}:</span> <div class="input-with-copy"> <code>{processing.zoneId}</code> <button type="button" class:copied={clipboard.isCopied(`zone-${processing.zoneId}`)} onclick={() => clipboard.copy(processing.zoneId, `zone-${processing.zoneId}`)} - title="Copy zone ID" + title={$t('tools.ipv6_zone_id.actions.copyZoneId')} > <Icon name={clipboard.isCopied(`zone-${processing.zoneId}`) ? 'check' : 'copy'} @@ -289,26 +308,28 @@ {#if processing.processing.zoneIdValid} <span class="zone-status valid"> <Icon name="check" /> - Valid + {$t('tools.ipv6_zone_id.results.processing.zoneStatus.valid')} </span> {:else} <span class="zone-status invalid"> <Icon name="x" /> - Invalid + {$t('tools.ipv6_zone_id.results.processing.zoneStatus.invalid')} </span> {/if} </div> {:else} <div class="breakdown-item"> - <span class="breakdown-label">Zone ID:</span> - <span class="no-zone">None</span> + <span class="breakdown-label">{$t('tools.ipv6_zone_id.results.processing.zoneId')}:</span> + <span class="no-zone">{$t('tools.ipv6_zone_id.results.processing.noZone')}</span> </div> {/if} </div> <div class="address-classification"> <div class="classification-item"> - <span class="classification-label">Address Type:</span> + <span class="classification-label" + >{$t('tools.ipv6_zone_id.results.processing.addressType')}:</span + > <span class="address-type" style="color: {getAddressTypeColor(processing.addressType)}" @@ -320,20 +341,24 @@ </div> <div class="classification-item"> - <span class="classification-label">Requires Zone ID:</span> + <span class="classification-label" + >{$t('tools.ipv6_zone_id.results.processing.requiresZone')}:</span + > <span class="zone-requirement" class:required={processing.requiresZoneId} class:optional={!processing.requiresZoneId} > - {processing.requiresZoneId ? 'Yes' : 'No'} + {processing.requiresZoneId + ? $t('tools.ipv6_zone_id.common.yes') + : $t('tools.ipv6_zone_id.common.no')} </span> </div> </div> <div class="processing-results"> <div class="result-item"> - <span class="result-label">With Zone:</span> + <span class="result-label">{$t('tools.ipv6_zone_id.results.processing.withZone')}:</span> <div class="input-with-copy"> <code>{processing.processing.withZone}</code> <button @@ -344,7 +369,7 @@ processing.processing.withZone, `with-zone-${processing.processing.withZone}`, )} - title="Copy with zone" + title={$t('tools.ipv6_zone_id.actions.copyWithZone')} > <Icon name={clipboard.isCopied(`with-zone-${processing.processing.withZone}`) @@ -357,7 +382,7 @@ </div> <div class="result-item"> - <span class="result-label">Without Zone:</span> + <span class="result-label">{$t('tools.ipv6_zone_id.results.processing.withoutZone')}:</span> <div class="input-with-copy"> <code>{processing.processing.withoutZone}</code> <button @@ -368,7 +393,7 @@ processing.processing.withoutZone, `without-zone-${processing.processing.withoutZone}`, )} - title="Copy without zone" + title={$t('tools.ipv6_zone_id.actions.copyWithoutZone')} > <Icon name={clipboard.isCopied(`without-zone-${processing.processing.withoutZone}`) @@ -383,7 +408,7 @@ {#if processing.processing.suggestedZones.length > 0} <div class="suggested-zones"> - <h4>Suggested Zone Identifiers:</h4> + <h4>{$t('tools.ipv6_zone_id.results.processing.suggestedZones')}:</h4> <div class="zones-list"> {#each processing.processing.suggestedZones as zone (zone)} <button @@ -391,7 +416,7 @@ class="zone-button" class:copied={clipboard.isCopied(`suggested-${zone}`)} onclick={() => clipboard.copy(`${processing.address}%${zone}`, `suggested-${zone}`)} - title="Copy full address" + title={$t('tools.ipv6_zone_id.actions.copyFullAddress')} > <code>{zone}</code> <Icon name={clipboard.isCopied(`suggested-${zone}`) ? 'check' : 'copy'} size="xs" /> @@ -404,7 +429,7 @@ {#if processing.requiresZoneId && !processing.hasZoneId} <div class="zone-warning"> <Icon name="alert-triangle" /> - This address type typically requires a zone identifier for proper routing + {$t('tools.ipv6_zone_id.results.processing.zoneWarning')} </div> {/if} </div> diff --git a/src/lib/components/tools/LOCBuilder.svelte b/src/lib/components/tools/LOCBuilder.svelte index 903aecb8..9138db93 100644 --- a/src/lib/components/tools/LOCBuilder.svelte +++ b/src/lib/components/tools/LOCBuilder.svelte @@ -2,6 +2,13 @@ import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let domain = $state(''); let latitude = $state('37.7749'); @@ -17,13 +24,13 @@ const clipboard = useClipboard(); - const cityExamples = [ - { name: 'San Francisco', lat: 37.7749, lng: -122.4194, alt: 10 }, - { name: 'New York', lat: 40.7128, lng: -74.006, alt: 10 }, - { name: 'London', lat: 51.5074, lng: -0.1278, alt: 11 }, - { name: 'Tokyo', lat: 35.6762, lng: 139.6503, alt: 40 }, - { name: 'Sydney', lat: -33.8688, lng: 151.2093, alt: 58 }, - ]; + const cityExamples = $derived([ + { name: $t('tools.loc_builder.examples.sanFrancisco.label'), lat: 37.7749, lng: -122.4194, alt: 10 }, + { name: $t('tools.loc_builder.examples.newYork.label'), lat: 40.7128, lng: -74.006, alt: 10 }, + { name: $t('tools.loc_builder.examples.london.label'), lat: 51.5074, lng: -0.1278, alt: 11 }, + { name: $t('tools.loc_builder.examples.tokyo.label'), lat: 35.6762, lng: 139.6503, alt: 40 }, + { name: $t('tools.loc_builder.examples.sydney.label'), lat: -33.8688, lng: 151.2093, alt: 58 }, + ]); function degreesToLOC(degrees: number, isLongitude = false) { const hemisphere = isLongitude ? (degrees >= 0 ? 'E' : 'W') : degrees >= 0 ? 'N' : 'S'; @@ -143,8 +150,8 @@ <div class="container"> <div class="card"> <div class="card-header"> - <h1>LOC Record Builder</h1> - <p>Convert latitude/longitude coordinates ↔ DNS LOC records for geographic positioning</p> + <h1>{$t('tools.loc_builder.title')}</h1> + <p>{$t('tools.loc_builder.description')}</p> </div> <div class="content"> @@ -152,11 +159,11 @@ <div class="mode-toggle"> <button class="mode-btn" class:active={!parseMode} onclick={() => (parseMode = false)}> <Icon name="map-pin" size="sm" /> - Coordinates β†’ LOC + {$t('tools.loc_builder.modes.coordsToLoc')} </button> <button class="mode-btn" class:active={parseMode} onclick={() => (parseMode = true)}> <Icon name="file" size="sm" /> - LOC β†’ Coordinates + {$t('tools.loc_builder.modes.locToCoords')} </button> </div> @@ -165,7 +172,7 @@ <details bind:open={showExamples}> <summary class="examples-summary"> <Icon name="lightbulb" size="sm" /> - City Examples + {$t('tools.loc_builder.examples.title')} <Icon name="chevron-down" size="sm" /> </summary> <div class="examples-grid"> @@ -182,24 +189,33 @@ <!-- Input Form --> <div class="input-section card"> <div class="input-group"> - <label for="domain" use:tooltip={'Domain name for the LOC record'}>Domain Name *</label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('tools.loc_builder.form.domain.tooltip')}>Domain Name *</label> + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools.loc_builder.form.domain.placeholder')} + /> </div> {#if parseMode} <div class="input-group"> - <label for="locString" use:tooltip={'Paste existing LOC record to parse'}>LOC Record String</label> + <label for="locString" use:tooltip={$t('tools.loc_builder.form.locString.tooltip')} + >{$t('tools.loc_builder.form.locString.label')}</label + > <textarea id="locString" bind:value={locString} - placeholder="example.com. IN LOC 37 46 29.000 N 122 25 10.000 W 10.00m 1m 10000m 10m" + placeholder={$t('tools.loc_builder.form.locString.placeholder')} rows="3" ></textarea> </div> {:else} <div class="coord-grid"> <div class="input-group"> - <label for="latitude" use:tooltip={'Latitude in decimal degrees (-90 to 90)'}>Latitude *</label> + <label for="latitude" use:tooltip={$t('tools.loc_builder.form.latitude.tooltip')} + >{$t('tools.loc_builder.form.latitude.label')}</label + > <input id="latitude" type="number" @@ -207,12 +223,14 @@ step="0.000001" min="-90" max="90" - placeholder="37.7749" + placeholder={$t('tools.loc_builder.form.latitude.placeholder')} /> </div> <div class="input-group"> - <label for="longitude" use:tooltip={'Longitude in decimal degrees (-180 to 180)'}>Longitude *</label> + <label for="longitude" use:tooltip={$t('tools.loc_builder.form.longitude.tooltip')} + >{$t('tools.loc_builder.form.longitude.label')}</label + > <input id="longitude" type="number" @@ -220,43 +238,64 @@ step="0.000001" min="-180" max="180" - placeholder="-122.4194" + placeholder={$t('tools.loc_builder.form.longitude.placeholder')} /> </div> </div> <div class="input-group"> - <label for="altitude" use:tooltip={'Altitude in meters above sea level'}>Altitude (m) *</label> - <input id="altitude" type="number" bind:value={altitude} step="0.1" placeholder="10" /> + <label for="altitude" use:tooltip={$t('tools.loc_builder.form.altitude.tooltip')} + >{$t('tools.loc_builder.form.altitude.label')}</label + > + <input + id="altitude" + type="number" + bind:value={altitude} + step="0.1" + placeholder={$t('tools.loc_builder.form.altitude.placeholder')} + /> </div> <div class="precision-grid"> <div class="input-group"> - <label for="size" use:tooltip={'Size/diameter of the location in meters'}>Size (m)</label> - <input id="size" type="number" bind:value={size} step="0.1" min="0" placeholder="1" /> + <label for="size" use:tooltip={$t('tools.loc_builder.form.size.tooltip')} + >{$t('tools.loc_builder.form.size.label')}</label + > + <input + id="size" + type="number" + bind:value={size} + step="0.1" + min="0" + placeholder={$t('tools.loc_builder.form.size.placeholder')} + /> </div> <div class="input-group"> - <label for="horizontalPrecision" use:tooltip={'Horizontal precision in meters'}>H. Precision (m)</label> + <label for="horizontalPrecision" use:tooltip={$t('tools.loc_builder.form.horizontalPrecision.tooltip')} + >{$t('tools.loc_builder.form.horizontalPrecision.label')}</label + > <input id="horizontalPrecision" type="number" bind:value={horizontalPrecision} step="1" min="0" - placeholder="10000" + placeholder={$t('tools.loc_builder.form.horizontalPrecision.placeholder')} /> </div> <div class="input-group"> - <label for="verticalPrecision" use:tooltip={'Vertical precision in meters'}>V. Precision (m)</label> + <label for="verticalPrecision" use:tooltip={$t('tools.loc_builder.form.verticalPrecision.tooltip')} + >{$t('tools.loc_builder.form.verticalPrecision.label')}</label + > <input id="verticalPrecision" type="number" bind:value={verticalPrecision} step="0.1" min="0" - placeholder="10" + placeholder={$t('tools.loc_builder.form.verticalPrecision.placeholder')} /> </div> </div> @@ -267,13 +306,13 @@ <div class="output-section"> <div class="card"> <h3 class="section-title"> - {parseMode ? 'Parsed Coordinates' : 'Generated LOC Record'} + {parseMode ? $t('tools.loc_builder.output.parsedCoords') : $t('tools.loc_builder.output.generatedLoc')} </h3> <div class="code-block"> {#if isValid} <code>{locRecord}</code> {:else} - <p class="placeholder">Fill in the required fields to generate the LOC record</p> + <p class="placeholder">{$t('tools.loc_builder.output.placeholder')}</p> {/if} </div> </div> @@ -282,11 +321,11 @@ <div class="actions"> <button onclick={copyToClipboard} class="btn btn-primary" class:success={clipboard.isCopied('copy')}> <Icon name={clipboard.isCopied('copy') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('copy') ? 'Copied!' : 'Copy Record'} + {clipboard.isCopied('copy') ? $t('common.actions.copied') : $t('tools.loc_builder.actions.copyRecord')} </button> <button onclick={downloadRecord} class="btn btn-success" class:success={clipboard.isCopied('download')}> <Icon name={clipboard.isCopied('download') ? 'check' : 'download'} size="sm" /> - {clipboard.isCopied('download') ? 'Downloaded!' : 'Download'} + {clipboard.isCopied('download') ? $t('common.actions.downloaded') : $t('common.actions.download')} </button> </div> {/if} @@ -296,7 +335,7 @@ <!-- Information Section --> <div class="info-section"> <div class="card info-card"> - <h3>About LOC Records</h3> + <h3>{$t('tools.loc_builder.about.title')}</h3> <p> LOC (Location) records store geographic location information in DNS. They specify latitude, longitude, altitude, and precision values, allowing applications to discover the physical location associated with a diff --git a/src/lib/components/tools/LeaseTimeCalculator.svelte b/src/lib/components/tools/LeaseTimeCalculator.svelte index b6cdef89..9ef12063 100644 --- a/src/lib/components/tools/LeaseTimeCalculator.svelte +++ b/src/lib/components/tools/LeaseTimeCalculator.svelte @@ -14,6 +14,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let poolSize = $state<number | undefined>(100); let expectedClients = $state<number | undefined>(50); @@ -105,8 +106,8 @@ </script> <ToolContentContainer - title="DHCP Lease Time Calculator" - description="Calculate optimal DHCP lease times based on network size, client turnover, and utilization. Includes T1/T2 renewal times and configuration examples." + title={$t('tools/dhcp-lease-time-calculator.title')} + description={$t('tools/dhcp-lease-time-calculator.subtitle')} > <ExamplesCard {examples} @@ -118,40 +119,52 @@ <div class="card input-card"> <div class="card-header"> - <h3>Network Configuration</h3> - <p class="help-text">Enter your network characteristics to calculate optimal lease times</p> + <h3>{$t('tools/dhcp-lease-time-calculator.input.title')}</h3> + <p class="help-text">{$t('tools/dhcp-lease-time-calculator.input.helpText')}</p> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="pool-size"> <Icon name="layers" size="sm" /> - IP Pool Size + {$t('tools/dhcp-lease-time-calculator.input.poolSize.label')} </label> - <input id="pool-size" type="number" bind:value={poolSize} placeholder="100" min="1" /> - <small>Total available IP addresses in your DHCP pool</small> + <input + id="pool-size" + type="number" + bind:value={poolSize} + placeholder={$t('tools/dhcp-lease-time-calculator.input.poolSize.placeholder')} + min="1" + /> + <small>{$t('tools/dhcp-lease-time-calculator.input.poolSize.hint')}</small> </div> <div class="input-group"> <label for="expected-clients"> <Icon name="users" size="sm" /> - Expected Clients + {$t('tools/dhcp-lease-time-calculator.input.expectedClients.label')} </label> - <input id="expected-clients" type="number" bind:value={expectedClients} placeholder="50" min="0" /> - <small>Average number of concurrent clients</small> + <input + id="expected-clients" + type="number" + bind:value={expectedClients} + placeholder={$t('tools/dhcp-lease-time-calculator.input.expectedClients.placeholder')} + min="0" + /> + <small>{$t('tools/dhcp-lease-time-calculator.input.expectedClients.hint')}</small> </div> </div> <div class="input-group"> <label for="network-type"> <Icon name="network" size="sm" /> - Network Type + {$t('tools/dhcp-lease-time-calculator.input.networkType.label')} </label> <select id="network-type" bind:value={networkType}> {#each Object.entries(NETWORK_TYPE_DEFAULTS) as [key, value] (key)} <option value={key}>{value.name}</option> {/each} - <option value="custom">Custom (use churn rate)</option> + <option value="custom">{$t('tools/dhcp-lease-time-calculator.input.networkType.custom')}</option> </select> {#if networkType !== 'custom'} <small>{NETWORK_TYPE_DEFAULTS[networkType].description}</small> @@ -161,25 +174,43 @@ <div class="input-group"> <label for="churn-rate"> <Icon name="refresh" size="sm" /> - Client Churn Rate + {$t('tools/dhcp-lease-time-calculator.input.churnRate.label')} </label> <select id="churn-rate" bind:value={churnRate}> - <option value="low">Low - {formatTime(CHURN_RATE_HOURS.low * 3600)}</option> - <option value="medium">Medium - {formatTime(CHURN_RATE_HOURS.medium * 3600)}</option> - <option value="high">High - {formatTime(CHURN_RATE_HOURS.high * 3600)}</option> - <option value="custom">Custom</option> + <option value="low" + >{$t('tools/dhcp-lease-time-calculator.input.churnRate.low', { + time: formatTime(CHURN_RATE_HOURS.low * 3600), + })}</option + > + <option value="medium" + >{$t('tools/dhcp-lease-time-calculator.input.churnRate.medium', { + time: formatTime(CHURN_RATE_HOURS.medium * 3600), + })}</option + > + <option value="high" + >{$t('tools/dhcp-lease-time-calculator.input.churnRate.high', { + time: formatTime(CHURN_RATE_HOURS.high * 3600), + })}</option + > + <option value="custom">{$t('tools/dhcp-lease-time-calculator.input.churnRate.custom')}</option> </select> - <small>How long devices typically stay connected</small> + <small>{$t('tools/dhcp-lease-time-calculator.input.churnRate.hint')}</small> </div> {#if churnRate === 'custom'} <div class="input-group"> <label for="custom-churn"> <Icon name="clock" size="sm" /> - Custom Churn Time (hours) + {$t('tools/dhcp-lease-time-calculator.input.customChurn.label')} </label> - <input id="custom-churn" type="number" bind:value={customChurnHours} placeholder="24" min="1" /> - <small>Average hours a device stays connected</small> + <input + id="custom-churn" + type="number" + bind:value={customChurnHours} + placeholder={$t('tools/dhcp-lease-time-calculator.input.customChurn.placeholder')} + min="1" + /> + <small>{$t('tools/dhcp-lease-time-calculator.input.customChurn.hint')}</small> </div> {/if} </div> @@ -187,7 +218,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/dhcp-lease-time-calculator.errors.title')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -199,22 +230,31 @@ {#if result && validationErrors.length === 0} <div class="card results"> - <h3>Calculated Lease Times</h3> + <h3>{$t('tools/dhcp-lease-time-calculator.results.title')}</h3> <div class="summary-card"> - <div><strong>Pool Utilization:</strong> {result.utilizationPercent}%</div> - <div><strong>Recommended Lease:</strong> {result.recommendedLeaseFormatted}</div> + <div> + <strong>{$t('tools/dhcp-lease-time-calculator.results.summary.poolUtilization')}</strong> + {result.utilizationPercent}% + </div> + <div> + <strong>{$t('tools/dhcp-lease-time-calculator.results.summary.recommendedLease')}</strong> + {result.recommendedLeaseFormatted} + </div> </div> {#if result.exhaustionTime} <div class="warning-card"> <Icon name="alert-triangle" size="sm" /> - <span><strong>Address Exhaustion:</strong> {result.exhaustionTime}</span> + <span + ><strong>{$t('tools/dhcp-lease-time-calculator.results.exhaustionWarning')}</strong> + {result.exhaustionTime}</span + > </div> {/if} <div class="lease-times"> - {#each [{ label: 'Default Lease Time', value: result.recommendedLeaseFormatted, seconds: result.recommendedLeaseSeconds, key: 'lease' }, { label: 'T1 (Renewal)', value: result.t1RenewalFormatted, seconds: result.t1RenewalSeconds, key: 't1' }, { label: 'T2 (Rebinding)', value: result.t2RebindingFormatted, seconds: result.t2RebindingSeconds, key: 't2' }] as time (time.key)} + {#each [{ label: $t('tools/dhcp-lease-time-calculator.results.leaseTimes.defaultLease'), value: result.recommendedLeaseFormatted, seconds: result.recommendedLeaseSeconds, key: 'lease' }, { label: $t('tools/dhcp-lease-time-calculator.results.leaseTimes.t1Renewal'), value: result.t1RenewalFormatted, seconds: result.t1RenewalSeconds, key: 't1' }, { label: $t('tools/dhcp-lease-time-calculator.results.leaseTimes.t2Rebinding'), value: result.t2RebindingFormatted, seconds: result.t2RebindingSeconds, key: 't2' }] as time (time.key)} <div class="time-item"> <div class="time-header"> <span class="time-label">{time.label}</span> @@ -227,14 +267,16 @@ </button> </div> <div class="time-value">{time.value}</div> - <div class="time-seconds">{time.seconds} seconds</div> + <div class="time-seconds"> + {$t('tools/dhcp-lease-time-calculator.results.leaseTimes.seconds', { seconds: time.seconds })} + </div> </div> {/each} </div> {#if result.recommendations.length > 0} <div class="recommendations"> - <h4>Recommendations</h4> + <h4>{$t('tools/dhcp-lease-time-calculator.results.recommendations.title')}</h4> {#each result.recommendations as recommendation, i (i)} <div class="recommendation-item"> {recommendation} @@ -244,7 +286,7 @@ {/if} </div> - {#each [{ title: 'ISC DHCPd Configuration', content: result.configExamples.iscDhcpd, key: 'isc' }, { title: 'Kea DHCPv4 Configuration', content: result.configExamples.keaDhcp4, key: 'kea' }] as config (config.key)} + {#each [{ title: $t('tools/dhcp-lease-time-calculator.config.iscDhcpd'), content: result.configExamples.iscDhcpd, key: 'isc' }, { title: $t('tools/dhcp-lease-time-calculator.config.keaDhcp4'), content: result.configExamples.keaDhcp4, key: 'kea' }] as config (config.key)} <div class="card results"> <div class="card-header-with-action"> <h3>{config.title}</h3> @@ -255,7 +297,9 @@ onclick={() => clipboard.copy(config.content, config.key)} > <Icon name={clipboard.isCopied(config.key) ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied(config.key) ? 'Copied' : 'Copy'} + {clipboard.isCopied(config.key) + ? $t('tools/dhcp-lease-time-calculator.buttons.copied') + : $t('tools/dhcp-lease-time-calculator.buttons.copy')} </button> </div> <pre class="output-value code-block">{config.content}</pre> diff --git a/src/lib/components/tools/LeaseTimeOption51.svelte b/src/lib/components/tools/LeaseTimeOption51.svelte index 11f1a858..8c7a1ff2 100644 --- a/src/lib/components/tools/LeaseTimeOption51.svelte +++ b/src/lib/components/tools/LeaseTimeOption51.svelte @@ -12,6 +12,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables/useClipboard.svelte'; + import { t } from '$lib/stores/language'; const clipboard = useClipboard(); @@ -29,22 +30,38 @@ let decodeResult = $state<LeaseTimeResult | null>(null); let decodeError = $state<string>(''); - const navOptions = [ - { value: 'build', label: 'Build Option' }, - { value: 'decode', label: 'Decode Option' }, - ]; + const navOptions = $derived([ + { value: 'build' as const, label: $t('tools/lease-time-option51.nav.build') }, + { value: 'decode' as const, label: $t('tools/lease-time-option51.nav.decode') }, + ]); const examples = LEASE_TIME_PRESETS.map((preset) => ({ ...preset, description: `${preset.description} β€’ ${preset.infinite ? 'Infinite' : formatTime(preset.seconds)}`, })); - const decodeExamples = [ - { label: '1 Hour', hexValue: '00000e10', description: '3,600 seconds (0x00000e10)' }, - { label: '24 Hours', hexValue: '00015180', description: '86,400 seconds (0x00015180)' }, - { label: '7 Days', hexValue: '00093a80', description: '604,800 seconds (0x00093a80)' }, - { label: 'Infinite', hexValue: 'ffffffff', description: 'Infinite lease (0xffffffff)' }, - ]; + const decodeExamples = $derived([ + { + label: $t('tools/lease-time-option51.examples.decode.oneHour.label'), + hexValue: '00000e10', + description: $t('tools/lease-time-option51.examples.decode.oneHour.description'), + }, + { + label: $t('tools/lease-time-option51.examples.decode.twentyFourHours.label'), + hexValue: '00015180', + description: $t('tools/lease-time-option51.examples.decode.twentyFourHours.description'), + }, + { + label: $t('tools/lease-time-option51.examples.decode.sevenDays.label'), + hexValue: '00093a80', + description: $t('tools/lease-time-option51.examples.decode.sevenDays.description'), + }, + { + label: $t('tools/lease-time-option51.examples.decode.infinite.label'), + hexValue: 'ffffffff', + description: $t('tools/lease-time-option51.examples.decode.infinite.description'), + }, + ]); function loadPreset(preset: (typeof LEASE_TIME_PRESETS)[0]) { activeTab = 'build'; @@ -112,8 +129,8 @@ </script> <ToolContentContainer - title="DHCP Option 51 - IP Address Lease Time" - description="Option 51 specifies the lease time in seconds for the IP address assignment. T1 (renewal at 50%) and T2 (rebinding at 87.5%) timers are automatically calculated." + title={$t('tools/lease-time-option51.title')} + description={$t('tools/lease-time-option51.subtitle')} {navOptions} bind:selectedNav={activeTab} > @@ -126,38 +143,54 @@ /> <div class="card input-card"> - <h3>Lease Time Configuration</h3> + <h3>{$t('tools/lease-time-option51.build.title')}</h3> <div class="form-group"> <label class="checkbox-label"> <input type="checkbox" bind:checked={infinite} /> - Infinite Lease (0xFFFFFFFF) + {$t('tools/lease-time-option51.build.infinite.label')} </label> - <span class="hint">Permanent IP address assignment (may not be supported by all servers)</span> + <span class="hint">{$t('tools/lease-time-option51.build.infinite.hint')}</span> </div> {#if !infinite} <div class="form-group"> - <label for="lease-seconds">Lease Time (seconds)</label> + <label for="lease-seconds">{$t('tools/lease-time-option51.build.leaseSeconds.label')}</label> <input id="lease-seconds" type="number" bind:value={leaseSeconds} min="0" max="4294967294" class="input" /> {#if leaseSeconds > 0} - <span class="hint">= {formatTime(leaseSeconds)}</span> + <span class="hint" + >{$t('tools/lease-time-option51.build.leaseSeconds.hint', { time: formatTime(leaseSeconds) })}</span + > {/if} </div> <div class="quick-values"> - <span class="label">Quick Values:</span> - <button class="btn-quick" onclick={() => (leaseSeconds = 3600)}>1h</button> - <button class="btn-quick" onclick={() => (leaseSeconds = 14400)}>4h</button> - <button class="btn-quick" onclick={() => (leaseSeconds = 86400)}>24h</button> - <button class="btn-quick" onclick={() => (leaseSeconds = 259200)}>3d</button> - <button class="btn-quick" onclick={() => (leaseSeconds = 604800)}>7d</button> + <span class="label">{$t('tools/lease-time-option51.build.quickValues.label')}</span> + <button class="btn-quick" onclick={() => (leaseSeconds = 3600)} + >{$t('tools/lease-time-option51.build.quickValues.oneHour')}</button + > + <button class="btn-quick" onclick={() => (leaseSeconds = 14400)} + >{$t('tools/lease-time-option51.build.quickValues.fourHours')}</button + > + <button class="btn-quick" onclick={() => (leaseSeconds = 86400)} + >{$t('tools/lease-time-option51.build.quickValues.twentyFourHours')}</button + > + <button class="btn-quick" onclick={() => (leaseSeconds = 259200)} + >{$t('tools/lease-time-option51.build.quickValues.threeDays')}</button + > + <button class="btn-quick" onclick={() => (leaseSeconds = 604800)} + >{$t('tools/lease-time-option51.build.quickValues.sevenDays')}</button + > </div> {/if} {#if buildErrors.length > 0} <div class="error-card" class:warning={buildErrors.every((e) => e.startsWith('Warning:'))}> - <strong>{buildErrors.some((e) => e.startsWith('Warning:')) ? 'Warnings:' : 'Validation Errors:'}</strong> + <strong + >{buildErrors.some((e) => e.startsWith('Warning:')) + ? $t('tools/lease-time-option51.build.errors.warnings') + : $t('tools/lease-time-option51.build.errors.validationErrors')}</strong + > <ul> {#each buildErrors as error, i (i)} <li>{error}</li> @@ -169,16 +202,16 @@ {#if buildResult} <div class="card result-card"> - <h3>Option 51 - Lease Time</h3> + <h3>{$t('tools/lease-time-option51.results.buildTitle')}</h3> <div class="result-grid"> <div class="result-item"> - <span class="label">Lease Time:</span> + <span class="label">{$t('tools/lease-time-option51.results.leaseTime')}</span> <span class="value highlight">{buildResult.humanReadable}</span> </div> <div class="result-item"> - <span class="label">Hex Encoded:</span> + <span class="label">{$t('tools/lease-time-option51.results.hexEncoded')}</span> <code class="code-value">{buildResult.hexEncoded}</code> <button class="btn-copy" @@ -186,12 +219,14 @@ onclick={() => clipboard.copy(buildResult!.hexEncoded, 'build-hex')} aria-label="Copy hex" > - {clipboard.isCopied('build-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('build-hex') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <div class="result-item"> - <span class="label">Wire Format:</span> + <span class="label">{$t('tools/lease-time-option51.results.wireFormat')}</span> <code class="code-value">{buildResult.wireFormat}</code> <button class="btn-copy" @@ -199,40 +234,54 @@ onclick={() => clipboard.copy(buildResult!.wireFormat, 'build-wire')} aria-label="Copy wire format" > - {clipboard.isCopied('build-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('build-wire') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <div class="result-item"> - <span class="label">Total Length:</span> - <span class="value">{buildResult.totalLength} bytes</span> + <span class="label">{$t('tools/lease-time-option51.results.totalLength')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.lengthBytes', { length: buildResult.totalLength })}</span + > </div> {#if !buildResult.isInfinite} <div class="result-item"> - <span class="label">T1 Renewal:</span> - <span class="value">{buildResult.t1RenewalFormatted} (50% of lease)</span> + <span class="label">{$t('tools/lease-time-option51.results.t1Renewal')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.t1RenewalValue', { + time: buildResult.t1RenewalFormatted ?? '', + })}</span + > </div> <div class="result-item"> - <span class="label">T2 Rebinding:</span> - <span class="value">{buildResult.t2RebindingFormatted} (87.5% of lease)</span> + <span class="label">{$t('tools/lease-time-option51.results.t2Rebinding')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.t2RebindingValue', { + time: buildResult.t2RebindingFormatted ?? '', + })}</span + > </div> {/if} </div> <div class="config-section"> - <h4>Configuration Examples</h4> + <h4>{$t('tools/lease-time-option51.results.configExamples')}</h4> <div class="output-group"> <div class="output-header"> - <h5>ISC DHCPd</h5> + <h5>{$t('tools/lease-time-option51.results.iscDhcpd')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('build-isc')} onclick={() => clipboard.copy(buildResult!.configExamples.iscDhcpd, 'build-isc')} > - {clipboard.isCopied('build-isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('build-isc') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.iscDhcpd}</code></pre> @@ -240,13 +289,15 @@ <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv4</h5> + <h5>{$t('tools/lease-time-option51.results.keaDhcp4')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('build-kea')} onclick={() => clipboard.copy(buildResult!.configExamples.keaDhcp4, 'build-kea')} > - {clipboard.isCopied('build-kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('build-kea') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.keaDhcp4}</code></pre> @@ -254,13 +305,15 @@ <div class="output-group"> <div class="output-header"> - <h5>dnsmasq</h5> + <h5>{$t('tools/lease-time-option51.results.dnsmasq')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('build-dnsmasq')} onclick={() => clipboard.copy(buildResult!.configExamples.dnsmasq, 'build-dnsmasq')} > - {clipboard.isCopied('build-dnsmasq') ? 'Copied' : 'Copy'} + {clipboard.isCopied('build-dnsmasq') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{buildResult.configExamples.dnsmasq}</code></pre> @@ -277,23 +330,23 @@ /> <div class="card input-card"> - <h3>Decode Option 51</h3> + <h3>{$t('tools/lease-time-option51.decode.title')}</h3> <div class="form-group"> - <label for="hex-input">Hex String</label> + <label for="hex-input">{$t('tools/lease-time-option51.decode.hexInput.label')}</label> <input id="hex-input" type="text" bind:value={hexInput} - placeholder="e.g., 00015180 or 00 01 51 80" + placeholder={$t('tools/lease-time-option51.decode.hexInput.placeholder')} class="input" /> - <span class="hint">Enter 8 hex characters (4 bytes, spaces optional)</span> + <span class="hint">{$t('tools/lease-time-option51.decode.hexInput.hint')}</span> </div> {#if decodeError} <div class="error-card"> - <strong>Decode Error:</strong> + <strong>{$t('tools/lease-time-option51.decode.error.title')}</strong> <p>{decodeError}</p> </div> {/if} @@ -301,50 +354,64 @@ {#if decodeResult} <div class="card result-card"> - <h3>Decoded Option 51</h3> + <h3>{$t('tools/lease-time-option51.results.decodeTitle')}</h3> <div class="result-grid"> <div class="result-item"> - <span class="label">Lease Time:</span> + <span class="label">{$t('tools/lease-time-option51.results.leaseTime')}</span> <span class="value highlight">{decodeResult.humanReadable}</span> </div> <div class="result-item"> - <span class="label">Seconds:</span> - <span class="value">{decodeResult.leaseSeconds.toLocaleString()}</span> + <span class="label">{$t('tools/lease-time-option51.results.seconds')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.secondsValue', { + seconds: decodeResult.leaseSeconds.toLocaleString(), + })}</span + > </div> {#if decodeResult.isInfinite} <div class="result-item infinite-badge"> - <span class="badge">Infinite Lease</span> + <span class="badge">{$t('tools/lease-time-option51.results.infiniteLease')}</span> </div> {/if} {#if !decodeResult.isInfinite} <div class="result-item"> - <span class="label">T1 Renewal:</span> - <span class="value">{decodeResult.t1RenewalFormatted} (50% of lease)</span> + <span class="label">{$t('tools/lease-time-option51.results.t1Renewal')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.t1RenewalValue', { + time: decodeResult.t1RenewalFormatted ?? '', + })}</span + > </div> <div class="result-item"> - <span class="label">T2 Rebinding:</span> - <span class="value">{decodeResult.t2RebindingFormatted} (87.5% of lease)</span> + <span class="label">{$t('tools/lease-time-option51.results.t2Rebinding')}</span> + <span class="value" + >{$t('tools/lease-time-option51.results.t2RebindingValue', { + time: decodeResult.t2RebindingFormatted ?? '', + })}</span + > </div> {/if} </div> <div class="config-section"> - <h4>Configuration Examples</h4> + <h4>{$t('tools/lease-time-option51.results.configExamples')}</h4> <div class="output-group"> <div class="output-header"> - <h5>ISC DHCPd</h5> + <h5>{$t('tools/lease-time-option51.results.iscDhcpd')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-isc')} onclick={() => clipboard.copy(decodeResult!.configExamples.iscDhcpd, 'decode-isc')} > - {clipboard.isCopied('decode-isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-isc') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.iscDhcpd}</code></pre> @@ -352,13 +419,15 @@ <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv4</h5> + <h5>{$t('tools/lease-time-option51.results.keaDhcp4')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-kea')} onclick={() => clipboard.copy(decodeResult!.configExamples.keaDhcp4, 'decode-kea')} > - {clipboard.isCopied('decode-kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-kea') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.keaDhcp4}</code></pre> @@ -366,13 +435,15 @@ <div class="output-group"> <div class="output-header"> - <h5>dnsmasq</h5> + <h5>{$t('tools/lease-time-option51.results.dnsmasq')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('decode-dnsmasq')} onclick={() => clipboard.copy(decodeResult!.configExamples.dnsmasq, 'decode-dnsmasq')} > - {clipboard.isCopied('decode-dnsmasq') ? 'Copied' : 'Copy'} + {clipboard.isCopied('decode-dnsmasq') + ? $t('tools/lease-time-option51.buttons.copied') + : $t('tools/lease-time-option51.buttons.copy')} </button> </div> <pre class="code-block"><code>{decodeResult.configExamples.dnsmasq}</code></pre> diff --git a/src/lib/components/tools/NAPTRBuilder.svelte b/src/lib/components/tools/NAPTRBuilder.svelte index 0e705fb9..433765d2 100644 --- a/src/lib/components/tools/NAPTRBuilder.svelte +++ b/src/lib/components/tools/NAPTRBuilder.svelte @@ -2,6 +2,7 @@ import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; let domain = $state(''); let order = $state('100'); @@ -14,32 +15,76 @@ const clipboard = useClipboard(); let showExamples = $state(false); - const flagOptions = [ - { value: 'U', label: 'U - Terminal rule (URI)', description: 'The Rule is terminal and the result is a URI' }, + const flagOptions = $derived([ + { + value: 'U', + label: $t('tools/naptr-builder.flags.uFlag.label'), + description: $t('tools/naptr-builder.flags.uFlag.description'), + }, { value: 'S', - label: 'S - Terminal rule (SRV)', - description: 'The Rule is terminal and the result is for SRV lookup', + label: $t('tools/naptr-builder.flags.sFlag.label'), + description: $t('tools/naptr-builder.flags.sFlag.description'), }, { value: 'A', - label: 'A - Terminal rule (Address)', - description: 'The Rule is terminal and the result is an address record', + label: $t('tools/naptr-builder.flags.aFlag.label'), + description: $t('tools/naptr-builder.flags.aFlag.description'), + }, + { + value: 'P', + label: $t('tools/naptr-builder.flags.pFlag.label'), + description: $t('tools/naptr-builder.flags.pFlag.description'), }, - { value: 'P', label: 'P - Protocol specific', description: 'Protocol-specific flags' }, - { value: '', label: 'Empty - Non-terminal', description: 'The Rule is not terminal (continue processing)' }, - ]; - - const serviceExamples = [ - { value: 'E2U+sip', label: 'SIP Service', description: 'Session Initiation Protocol' }, - { value: 'E2U+email', label: 'Email Service', description: 'Electronic mail service' }, - { value: 'E2U+web+http', label: 'HTTP Web Service', description: 'Web service over HTTP' }, - { value: 'E2U+web+https', label: 'HTTPS Web Service', description: 'Secure web service over HTTPS' }, - { value: 'E2U+tel', label: 'Telephone Service', description: 'Telephone number mapping' }, - { value: 'E2U+fax', label: 'Fax Service', description: 'Facsimile service' }, - { value: 'E2U+h323', label: 'H.323 Service', description: 'H.323 multimedia protocol' }, - { value: 'E2U+im', label: 'Instant Messaging', description: 'Instant messaging service' }, - ]; + { + value: '', + label: $t('tools/naptr-builder.flags.emptyFlag.label'), + description: $t('tools/naptr-builder.flags.emptyFlag.description'), + }, + ]); + + const serviceExamples = $derived([ + { + value: 'E2U+sip', + label: $t('tools/naptr-builder.services.sip.label'), + description: $t('tools/naptr-builder.services.sip.description'), + }, + { + value: 'E2U+email', + label: $t('tools/naptr-builder.services.email.label'), + description: $t('tools/naptr-builder.services.email.description'), + }, + { + value: 'E2U+web+http', + label: $t('tools/naptr-builder.services.webHttp.label'), + description: $t('tools/naptr-builder.services.webHttp.description'), + }, + { + value: 'E2U+web+https', + label: $t('tools/naptr-builder.services.webHttps.label'), + description: $t('tools/naptr-builder.services.webHttps.description'), + }, + { + value: 'E2U+tel', + label: $t('tools/naptr-builder.services.tel.label'), + description: $t('tools/naptr-builder.services.tel.description'), + }, + { + value: 'E2U+fax', + label: $t('tools/naptr-builder.services.fax.label'), + description: $t('tools/naptr-builder.services.fax.description'), + }, + { + value: 'E2U+h323', + label: $t('tools/naptr-builder.services.h323.label'), + description: $t('tools/naptr-builder.services.h323.description'), + }, + { + value: 'E2U+im', + label: $t('tools/naptr-builder.services.im.label'), + description: $t('tools/naptr-builder.services.im.description'), + }, + ]); let naptrRecord = $derived.by(() => { if (!domain.trim()) return ''; @@ -64,23 +109,23 @@ const warns = []; if (flags === 'U' && !regexp.includes('!')) { - warns.push('URI flag requires a valid substitution expression with delimiters'); + warns.push($t('tools/naptr-builder.warnings.uriFlag')); } if (flags === 'S' && replacement === '.') { - warns.push('SRV flag typically requires a replacement domain, not "."'); + warns.push($t('tools/naptr-builder.warnings.srvFlag')); } if (flags === '' && replacement === '.') { - warns.push('Non-terminal rules should have a replacement domain for continued processing'); + warns.push($t('tools/naptr-builder.warnings.nonTerminal')); } if (regexp && !regexp.match(/^!.*!.*!$/)) { - warns.push('Regular expressions should follow the format: !pattern!replacement!'); + warns.push($t('tools/naptr-builder.warnings.regexpFormat')); } if (parseInt(order) === parseInt(preference)) { - warns.push('Order and Preference should typically be different values'); + warns.push($t('tools/naptr-builder.warnings.orderPreference')); } return warns; @@ -148,8 +193,8 @@ <div class="container"> <div class="card"> <div class="card-header"> - <h1>NAPTR Record Builder</h1> - <p>Construct NAPTR (Naming Authority Pointer) records for dynamic delegation and service mapping</p> + <h1>{$t('tools/naptr-builder.title')}</h1> + <p>{$t('tools/naptr-builder.description')}</p> </div> <div class="content"> @@ -158,14 +203,22 @@ <details bind:open={showExamples}> <summary class="examples-summary"> <Icon name="lightbulb" size="sm" /> - Quick Examples + {$t('tools/naptr-builder.examples.title')} <span class="chevron"><Icon name="chevron-down" size="sm" /></span> </summary> <div class="examples-grid"> - <button class="example-btn" onclick={() => loadExample('sip')}> SIP Service </button> - <button class="example-btn" onclick={() => loadExample('email')}> Email Service </button> - <button class="example-btn" onclick={() => loadExample('web')}> Web Service </button> - <button class="example-btn" onclick={() => loadExample('srv')}> SRV Delegation </button> + <button class="example-btn" onclick={() => loadExample('sip')} + >{$t('tools/naptr-builder.examples.sipService')}</button + > + <button class="example-btn" onclick={() => loadExample('email')} + >{$t('tools/naptr-builder.examples.emailService')}</button + > + <button class="example-btn" onclick={() => loadExample('web')} + >{$t('tools/naptr-builder.examples.webService')}</button + > + <button class="example-btn" onclick={() => loadExample('srv')} + >{$t('tools/naptr-builder.examples.srvDelegation')}</button + > </div> </details> </div> @@ -174,32 +227,39 @@ <!-- Input Form --> <div class="input-section"> <div class="input-group"> - <label for="domain" use:tooltip={'The domain name for this NAPTR record'}>Domain Name *</label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> - <p class="description">The domain name for this NAPTR record</p> + <label for="domain" use:tooltip={$t('tools/naptr-builder.input.domainTooltip')} + >{$t('tools/naptr-builder.input.domainLabel')}</label + > + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools/naptr-builder.input.domainPlaceholder')} + /> + <p class="description">{$t('tools/naptr-builder.input.domainDescription')}</p> </div> <div class="order-grid"> <div class="input-group"> - <label for="order" use:tooltip={'Processing order - lower values are processed first (0-65535)'} - >Order *</label + <label for="order" use:tooltip={$t('tools/naptr-builder.input.orderTooltip')} + >{$t('tools/naptr-builder.input.orderLabel')}</label > <input id="order" type="number" bind:value={order} min="0" max="65535" /> - <p class="description">Processing order (0-65535)</p> + <p class="description">{$t('tools/naptr-builder.input.orderDescription')}</p> </div> <div class="input-group"> - <label for="preference" use:tooltip={'Preference within the same order value (0-65535)'} - >Preference *</label + <label for="preference" use:tooltip={$t('tools/naptr-builder.input.preferenceTooltip')} + >{$t('tools/naptr-builder.input.preferenceLabel')}</label > <input id="preference" type="number" bind:value={preference} min="0" max="65535" /> - <p class="description">Preference within order (0-65535)</p> + <p class="description">{$t('tools/naptr-builder.input.preferenceDescription')}</p> </div> </div> <div class="input-group"> - <label for="flags" use:tooltip={'Control processing behavior - affects how the result is interpreted'} - >Flags</label + <label for="flags" use:tooltip={$t('tools/naptr-builder.input.flagsTooltip')} + >{$t('tools/naptr-builder.input.flagsLabel')}</label > <select id="flags" bind:value={flags}> {#each flagOptions as option (option.value)} @@ -207,17 +267,23 @@ {/each} </select> <p class="description"> - {flagOptions.find((opt) => opt.value === flags)?.description || 'Select flag type'} + {flagOptions.find((opt) => opt.value === flags)?.description || + $t('tools/naptr-builder.flags.selectPlaceholder')} </p> </div> <div class="input-group"> - <label for="service" use:tooltip={'Service identifier or protocol (e.g., E2U+sip for SIP services)'} - >Service</label + <label for="service" use:tooltip={$t('tools/naptr-builder.input.serviceTooltip')} + >{$t('tools/naptr-builder.input.serviceLabel')}</label > - <input id="service" type="text" bind:value={service} placeholder="E2U+sip" /> + <input + id="service" + type="text" + bind:value={service} + placeholder={$t('tools/naptr-builder.input.servicePlaceholder')} + /> <details class="service-examples"> - <summary>Show service examples</summary> + <summary>{$t('tools/naptr-builder.services.showExamples')}</summary> <div class="service-list"> {#each serviceExamples as example (example.value)} <button class="service-item" onclick={() => (service = example.value)}> @@ -229,31 +295,42 @@ </div> <div class="input-group"> - <label - for="regexp" - use:tooltip={'Regular expression for pattern matching and substitution (format: !pattern!replacement!)'} - >Regular Expression</label + <label for="regexp" use:tooltip={$t('tools/naptr-builder.input.regexpTooltip')} + >{$t('tools/naptr-builder.input.regexpLabel')}</label > - <input id="regexp" type="text" bind:value={regexp} placeholder="!^.*$!sip:info@example.com!" class="mono" /> - <p class="description">Substitution expression (format: !pattern!replacement!)</p> + <input + id="regexp" + type="text" + bind:value={regexp} + placeholder={$t('tools/naptr-builder.input.regexpPlaceholder')} + class="mono" + /> + <p class="description">{$t('tools/naptr-builder.input.regexpDescription')}</p> </div> <div class="input-group"> - <label for="replacement" use:tooltip={"Next domain to query, or '.' for terminal rules"}>Replacement</label> - <input id="replacement" type="text" bind:value={replacement} placeholder="." /> - <p class="description">Domain name for next lookup, or "." for terminal rules</p> + <label for="replacement" use:tooltip={$t('tools/naptr-builder.input.replacementTooltip')} + >{$t('tools/naptr-builder.input.replacementLabel')}</label + > + <input + id="replacement" + type="text" + bind:value={replacement} + placeholder={$t('tools/naptr-builder.input.replacementPlaceholder')} + /> + <p class="description">{$t('tools/naptr-builder.input.replacementDescription')}</p> </div> </div> <!-- Output --> <div class="output-section"> <div class="card"> - <h3 class="section-title">Generated NAPTR Record</h3> + <h3 class="section-title">{$t('tools/naptr-builder.output.title')}</h3> <div class="code-block"> {#if isValid} <code>{naptrRecord}</code> {:else} - <p class="placeholder">Fill in the required fields to generate the NAPTR record</p> + <p class="placeholder">{$t('tools/naptr-builder.output.placeholder')}</p> {/if} </div> </div> @@ -262,7 +339,7 @@ <div class="message warning"> <Icon name="alert-triangle" size="sm" /> <div> - <h4>Configuration Warnings</h4> + <h4>{$t('tools/naptr-builder.warnings.title')}</h4> <ul> {#each warnings as warning, index (index)} <li>{warning}</li> @@ -276,11 +353,15 @@ <div class="actions"> <button onclick={copyToClipboard} class="btn btn-primary" class:success={clipboard.isCopied('copy')}> <Icon name={clipboard.isCopied('copy') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('copy') ? 'Copied!' : 'Copy Record'} + {clipboard.isCopied('copy') + ? $t('tools/naptr-builder.output.copied') + : $t('tools/naptr-builder.output.copyButton')} </button> <button onclick={downloadRecord} class="btn btn-success" class:success={clipboard.isCopied('download')}> <Icon name={clipboard.isCopied('download') ? 'check' : 'download'} size="sm" /> - {clipboard.isCopied('download') ? 'Downloaded!' : 'Download'} + {clipboard.isCopied('download') + ? $t('tools/naptr-builder.output.downloaded') + : $t('tools/naptr-builder.output.downloadButton')} </button> </div> {/if} @@ -290,41 +371,37 @@ <!-- Information Section --> <div class="info-section"> <div class="card info-card"> - <h3 class="section-title">About NAPTR Records</h3> - <p> - NAPTR (Naming Authority Pointer) records provide a way to map domain names to URIs or other domain names - through regular expression-based rewriting. They're commonly used in telecommunications for ENUM (E.164 - Number Mapping) and SIP services, allowing dynamic delegation and service discovery. - </p> + <h3 class="section-title">{$t('tools/naptr-builder.info.aboutTitle')}</h3> + <p>{$t('tools/naptr-builder.info.aboutDescription')}</p> </div> <div class="info-grid"> <div class="card info-card"> - <h4>Field Descriptions</h4> + <h4>{$t('tools/naptr-builder.info.fieldsTitle')}</h4> <dl class="field-list"> <dt>Order:</dt> - <dd>Processing order (lower values first)</dd> + <dd>{$t('tools/naptr-builder.info.fields.order')}</dd> <dt>Preference:</dt> - <dd>Preference within same order</dd> + <dd>{$t('tools/naptr-builder.info.fields.preference')}</dd> <dt>Flags:</dt> - <dd>Control processing behavior</dd> + <dd>{$t('tools/naptr-builder.info.fields.flags')}</dd> <dt>Service:</dt> - <dd>Service identifier or protocol</dd> + <dd>{$t('tools/naptr-builder.info.fields.service')}</dd> <dt>RegExp:</dt> - <dd>Pattern matching and substitution</dd> + <dd>{$t('tools/naptr-builder.info.fields.regexp')}</dd> <dt>Replacement:</dt> - <dd>Next domain to query</dd> + <dd>{$t('tools/naptr-builder.info.fields.replacement')}</dd> </dl> </div> <div class="card info-card"> - <h4>Common Use Cases</h4> + <h4>{$t('tools/naptr-builder.info.useCasesTitle')}</h4> <ul class="use-case-list"> - <li>ENUM telephone number mapping</li> - <li>SIP service discovery</li> - <li>Dynamic delegation</li> - <li>Protocol mapping</li> - <li>Service location</li> + <li>{$t('tools/naptr-builder.info.useCases.enum')}</li> + <li>{$t('tools/naptr-builder.info.useCases.sip')}</li> + <li>{$t('tools/naptr-builder.info.useCases.delegation')}</li> + <li>{$t('tools/naptr-builder.info.useCases.protocol')}</li> + <li>{$t('tools/naptr-builder.info.useCases.location')}</li> </ul> </div> </div> diff --git a/src/lib/components/tools/NetworkVisualizer.svelte b/src/lib/components/tools/NetworkVisualizer.svelte index bac7417e..36c6a3a0 100644 --- a/src/lib/components/tools/NetworkVisualizer.svelte +++ b/src/lib/components/tools/NetworkVisualizer.svelte @@ -2,6 +2,14 @@ import type { SubnetInfo } from '$lib/types/ip.js'; import Tooltip from '$lib/components/global/Tooltip.svelte'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools.network-visualizer'); + }); interface Props { subnetInfo: SubnetInfo; @@ -69,20 +77,24 @@ <div class="network-visualizer {className}"> <section class="visualizer-section"> - <h3>Network Visualization</h3> + <h3>{$t('tools.network-visualizer.sections.networkVisualization')}</h3> <!-- Network Range Bar --> <div class="range-section"> <div class="range-header"> - <span class="range-label">Address Range</span> + <span class="range-label">{$t('tools.network-visualizer.ranges.addressRange')}</span> <span class="range-count"> - {formatNumber(subnetInfo.hostCount)} total addresses + {$t('tools.network-visualizer.ranges.totalAddresses', { count: formatNumber(subnetInfo.hostCount) })} </span> </div> <div class="range-bar"> <!-- Network Address --> - <div class="range-segment network" style="width: {100 / subnetInfo.hostCount}%" title="Network Address"> + <div + class="range-segment network" + style="width: {100 / subnetInfo.hostCount}%" + title={$t('tools.network-visualizer.addressTypes.networkAddress')} + > {#if subnetInfo.hostCount <= 32} <span class="segment-label">N</span> {/if} @@ -92,12 +104,16 @@ <div class="range-segment usable" style="left: {100 / subnetInfo.hostCount}%; width: {usablePercentage * (1 - 2 / subnetInfo.hostCount)}%" - title="Usable Host Addresses" + title={$t('tools.network-visualizer.tooltips.usableHostAddresses')} ></div> <!-- Broadcast Address --> {#if subnetInfo.hostCount > 1} - <div class="range-segment broadcast" style="width: {100 / subnetInfo.hostCount}%" title="Broadcast Address"> + <div + class="range-segment broadcast" + style="width: {100 / subnetInfo.hostCount}%" + title={$t('tools.network-visualizer.addressTypes.broadcastAddress')} + > {#if subnetInfo.hostCount <= 32} <span class="segment-label">B</span> {/if} @@ -109,16 +125,18 @@ <div class="range-legend"> <div class="legend-item"> <div class="legend-color network"></div> - <span>Network</span> + <span>{$t('tools.network-visualizer.legend.network')}</span> </div> <div class="legend-item"> <div class="legend-color usable"></div> - <span>Usable Hosts ({formatNumber(subnetInfo.usableHosts)})</span> + <span + >{$t('tools.network-visualizer.legend.usableHosts', { count: formatNumber(subnetInfo.usableHosts) })}</span + > </div> {#if subnetInfo.hostCount > 1} <div class="legend-item"> <div class="legend-color broadcast"></div> - <span>Broadcast</span> + <span>{$t('tools.network-visualizer.legend.broadcast')}</span> </div> {/if} </div> @@ -127,7 +145,7 @@ <!-- Binary Subnet Mask Visualization --> <section class="binary-section"> - <h4>Subnet Mask Binary Breakdown</h4> + <h4>{$t('tools.network-visualizer.sections.binaryBreakdown')}</h4> <div class="binary-display"> {#each subnetInfo.subnet.octets as octet, i (i)} @@ -139,7 +157,13 @@ <div class="bits-group"> {#each octet.toString(2).padStart(8, '0').split('') as bit, bitIndex (bitIndex)} <Tooltip - text="{bit === '1' ? 'Network bit (1)' : 'Host bit (0)'} - Position {i * 8 + bitIndex + 1}" + text={$t('tools.network-visualizer.binary.bitTooltip', { + type: + bit === '1' + ? $t('tools.network-visualizer.binary.networkBit') + : $t('tools.network-visualizer.binary.hostBit'), + position: i * 8 + bitIndex + 1, + })} position="top" > <span class="bit-box {bit === '1' ? 'network-bit' : 'host-bit'}"> @@ -149,7 +173,7 @@ {/each} </div> <span class="octet-label"> - (Octet {i + 1}) + {$t('tools.network-visualizer.binary.octetLabel', { number: i + 1 })} </span> </div> {/each} @@ -159,11 +183,11 @@ <div class="bit-stats"> <div class="bit-stat"> <div class="legend-color network"></div> - <span>Network bits: {subnetInfo.cidr}</span> + <span>{$t('tools.network-visualizer.binary.networkBits', { count: subnetInfo.cidr })}</span> </div> <div class="bit-stat"> <div class="legend-color host"></div> - <span>Host bits: {32 - subnetInfo.cidr}</span> + <span>{$t('tools.network-visualizer.binary.hostBits', { count: 32 - subnetInfo.cidr })}</span> </div> </div> </div> @@ -172,7 +196,7 @@ <!-- Address Grid (for smaller subnets) --> {#if subnetInfo.hostCount <= 64} <section class="grid-section"> - <h4>Address Grid</h4> + <h4>{$t('tools.network-visualizer.sections.addressGrid')}</h4> <div class="address-grid-wrap"> <div class="address-grid"> @@ -196,14 +220,14 @@ <!-- Efficiency Metrics --> <section class="efficiency-section"> - <h4>Network Efficiency</h4> + <h4>{$t('tools.network-visualizer.sections.networkEfficiency')}</h4> <div class="efficiency-grid"> <div class="efficiency-metric"> <div class="metric-value {getUtilizationColor(usablePercentage)}"> {usablePercentage.toFixed(1)}% </div> - <div class="metric-label">Address Utilization</div> + <div class="metric-label">{$t('tools.network-visualizer.efficiency.addressUtilization')}</div> </div> <div class="efficiency-metric"> diff --git a/src/lib/components/tools/NextAvailable.svelte b/src/lib/components/tools/NextAvailable.svelte index b617fab2..1735058f 100644 --- a/src/lib/components/tools/NextAvailable.svelte +++ b/src/lib/components/tools/NextAvailable.svelte @@ -10,6 +10,14 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let pools = $state(`192.168.0.0/16 10.0.0.0/8`); @@ -28,9 +36,9 @@ let userModified = $state(false); let showVisualization = $state(true); - const examples = [ + const examples = $derived([ { - label: 'Office Subnets', + label: $t('tools.next_available.examples.officeSubnets.label'), pools: '192.168.0.0/16', allocations: `192.168.1.0/24 192.168.10.0/24 @@ -40,7 +48,7 @@ policy: 'first-fit' as AllocationPolicy, }, { - label: 'Host-based Search', + label: $t('tools.next_available.examples.hostBasedSearch.label'), pools: '10.0.0.0/8', allocations: `10.0.0.0/16 10.1.0.0/16`, @@ -49,7 +57,7 @@ policy: 'best-fit' as AllocationPolicy, }, { - label: 'Multiple Pools', + label: $t('tools.next_available.examples.multiplePools.label'), pools: `172.16.0.0/12 192.168.0.0/16`, allocations: `172.16.1.0/24 @@ -59,7 +67,7 @@ policy: 'first-fit' as AllocationPolicy, }, { - label: 'IPv6 Example', + label: $t('tools.next_available.examples.ipv6Example.label'), pools: '2001:db8::/32', allocations: `2001:db8:1::/48 2001:db8:10::/48`, @@ -67,7 +75,7 @@ prefix: 48, policy: 'first-fit' as AllocationPolicy, }, - ]; + ]); /* Set example */ function setExample(example: (typeof examples)[0]) { @@ -104,7 +112,16 @@ 2, ); } else { - const headers = ['Rank', 'CIDR', 'Network', 'Broadcast', 'Size', 'Usable Hosts', 'Parent Pool', 'Gap Size']; + const headers = [ + $t('tools.next_available.export.headers.rank'), + $t('tools.next_available.export.headers.cidr'), + $t('tools.next_available.export.headers.network'), + $t('tools.next_available.export.headers.broadcast'), + $t('tools.next_available.export.headers.size'), + $t('tools.next_available.export.headers.usableHosts'), + $t('tools.next_available.export.headers.parentPool'), + $t('tools.next_available.export.headers.gapSize'), + ]; const rows = result.candidates.map((candidate, i) => [ (i + 1).toString(), `"${candidate.cidr}"`, @@ -171,14 +188,17 @@ type: 'pool' | 'allocation' | 'free' | 'candidate', ): string { const labels = { - pool: 'Pool', - allocation: 'Allocation', - free: 'Free Space', - candidate: 'Candidate', + pool: $t('tools.next_available.visualization.pool'), + allocation: $t('tools.next_available.visualization.allocation'), + free: $t('tools.next_available.visualization.freeSpace'), + candidate: $t('tools.next_available.visualization.candidate'), }; - const size = range.start && range.end ? formatNumber(Number(range.end - range.start + 1n)) : 'Unknown'; - return `${labels[type]}\n${range.cidr}\nSize: ${size} addresses`; + const size = + range.start && range.end + ? formatNumber(Number(range.end - range.start + 1n)) + : $t('tools.next_available.visualization.unknown'); + return `${labels[type]}\n${range.cidr}\n${$t('tools.next_available.visualization.sizeLabel')}: ${size} ${$t('tools.next_available.visualization.addresses')}`; } // Track user modifications @@ -209,15 +229,15 @@ <div class="card"> <header class="card-header"> - <h2>Next Available Subnet Finder</h2> + <h2>{$t('tools.next_available.title')}</h2> <p> - Find available subnets from pool CIDRs minus existing allocations with first-fit or best-fit allocation policies. + {$t('tools.next_available.description')} </p> </header> <!-- Search Mode --> <div class="mode-section"> - <h3>Search Criteria</h3> + <h3>{$t('tools.next_available.searchCriteria.title')}</h3> <div class="tabs"> <button type="button" @@ -227,9 +247,9 @@ searchMode = 'prefix'; userModified = true; }} - use:tooltip={{ text: 'Search for subnets with specific prefix length (e.g., /24)', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.searchCriteria.byPrefix.tooltip'), position: 'top' }} > - By Prefix + {$t('tools.next_available.searchCriteria.byPrefix.label')} </button> <button type="button" @@ -239,9 +259,9 @@ searchMode = 'hosts'; userModified = true; }} - use:tooltip={{ text: 'Search for subnets that can accommodate N hosts', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.searchCriteria.byHosts.tooltip'), position: 'top' }} > - By Host Count + {$t('tools.next_available.searchCriteria.byHosts.label')} </button> </div> </div> @@ -250,14 +270,14 @@ <div class="input-section"> <div class="input-grid"> <div class="input-group"> - <label for="pools" use:tooltip={{ text: 'Available IP address pools (one per line)', position: 'top' }}> - Pool CIDRs + <label for="pools" use:tooltip={{ text: $t('tools.next_available.input.pools.tooltip'), position: 'top' }}> + {$t('tools.next_available.input.pools.label')} </label> <textarea id="pools" bind:value={pools} oninput={() => (userModified = true)} - placeholder="192.168.0.0/16 10.0.0.0/8" + placeholder={$t('tools.next_available.input.pools.placeholder')} class="input-textarea pools" rows="4" ></textarea> @@ -266,15 +286,15 @@ <div class="input-group"> <label for="allocations" - use:tooltip={{ text: 'Already allocated subnets/ranges (optional, one per line)', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.input.allocations.tooltip'), position: 'top' }} > - Existing Allocations + {$t('tools.next_available.input.allocations.label')} </label> <textarea id="allocations" bind:value={allocations} oninput={() => (userModified = true)} - placeholder="192.168.1.0/24 10.0.0.0/16" + placeholder={$t('tools.next_available.input.allocations.placeholder')} class="input-textarea allocations" rows="4" ></textarea> @@ -287,9 +307,9 @@ <div class="input-group"> <label for="desired-prefix" - use:tooltip={{ text: 'Desired subnet prefix (e.g., 24 for /24 subnets)', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.input.targetPrefix.tooltip'), position: 'top' }} > - Target Prefix Length + {$t('tools.next_available.input.targetPrefix.label')} </label> <input id="desired-prefix" @@ -305,9 +325,9 @@ <div class="input-group"> <label for="desired-hosts" - use:tooltip={{ text: 'Minimum number of hosts the subnet must accommodate', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.input.requiredHosts.tooltip'), position: 'top' }} > - Required Host Count + {$t('tools.next_available.input.requiredHosts.label')} </label> <input id="desired-hosts" @@ -321,24 +341,21 @@ {/if} <div class="input-group"> - <label - for="policy" - use:tooltip={{ text: 'First-fit: lowest address. Best-fit: smallest gap.', position: 'top' }} - > - Allocation Policy + <label for="policy" use:tooltip={{ text: $t('tools.next_available.input.policy.tooltip'), position: 'top' }}> + {$t('tools.next_available.input.policy.label')} </label> <select id="policy" bind:value={policy} onchange={() => (userModified = true)} class="input-field"> - <option value="first-fit">First Fit (Lowest)</option> - <option value="best-fit">Best Fit (Smallest Gap)</option> + <option value="first-fit">{$t('tools.next_available.input.policy.options.firstFit')}</option> + <option value="best-fit">{$t('tools.next_available.input.policy.options.bestFit')}</option> </select> </div> <div class="input-group"> <label for="max-candidates" - use:tooltip={{ text: 'Maximum number of subnet suggestions to return', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.input.maxCandidates.tooltip'), position: 'top' }} > - Max Candidates + {$t('tools.next_available.input.maxCandidates.label')} </label> <input id="max-candidates" @@ -356,10 +373,10 @@ <div class="options-section"> <label class="checkbox-label" - use:tooltip={{ text: 'For IPv4, treat network and broadcast addresses as unusable', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.input.options.usableHosts.tooltip'), position: 'top' }} > <input type="checkbox" bind:checked={ipv4UsableHosts} onchange={() => (userModified = true)} /> - <span class="checkbox-text"> IPv4 usable hosts (exclude network/broadcast) </span> + <span class="checkbox-text"> {$t('tools.next_available.input.options.usableHosts.label')} </span> </label> </div> @@ -368,7 +385,7 @@ type="button" class="btn btn-secondary btn-sm" onclick={clearInputs} - use:tooltip={{ text: 'Clear all inputs', position: 'top' }} + use:tooltip={{ text: $t('tools.next_available.actions.clearInputs'), position: 'top' }} > <Icon name="trash" size="sm" /> </button> @@ -376,7 +393,7 @@ <!-- Examples --> <div class="examples-section"> - <h4>Quick Examples</h4> + <h4>{$t('tools.next_available.examples.title')}</h4> <div class="examples-grid"> {#each examples as example (example.label)} <button @@ -398,7 +415,7 @@ <!-- Errors --> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Errors</h3> + <h3>{$t('tools.next_available.results.errors.title')}</h3> <ul> {#each result.errors as error, index (index)} <li>{error}</li> @@ -410,7 +427,7 @@ <!-- Warnings --> {#if result.warnings.length > 0} <div class="info-panel warning"> - <h3>Warnings</h3> + <h3>{$t('tools.next_available.results.warnings.title')}</h3> <ul> {#each result.warnings as warning, index (index)} <li>{warning}</li> @@ -423,7 +440,7 @@ <!-- Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Available Subnets</h3> + <h3>{$t('tools.next_available.results.title')}</h3> <div class="export-buttons"> <button type="button" diff --git a/src/lib/components/tools/NthIP.svelte b/src/lib/components/tools/NthIP.svelte index 1347760f..a2835df7 100644 --- a/src/lib/components/tools/NthIP.svelte +++ b/src/lib/components/tools/NthIP.svelte @@ -3,8 +3,16 @@ import { calculateNthIPs, type NthIPResult } from '$lib/utils/nth-ip.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import '../../../styles/diagnostics-pages.scss'; + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); + let inputText = $state('192.168.1.0/24 @ 10\n10.0.0.0-10.0.0.255 [50]\n172.16.0.0/16 100\n2001:db8::/64#1000'); let globalOffset = $state(0); let result = $state<NthIPResult | null>(null); @@ -13,40 +21,40 @@ let userModified = $state(false); const clipboard = useClipboard(); - const examples = [ + const examples = $derived([ { input: '192.168.1.0/24 @ 10', - description: 'Get 10th IP from a /24 subnet', + description: $t('tools.nth_ip.examples.tenthFromSubnet.description'), }, { input: '10.0.0.0-10.0.0.255 [128]\n172.16.0.0/16 1000', - description: 'Multiple range types with different indices', + description: $t('tools.nth_ip.examples.multipleRanges.description'), }, { input: '2001:db8::/64#100\nfe80::/10 @ 50', - description: 'IPv6 networks with various formats', + description: $t('tools.nth_ip.examples.ipv6Networks.description'), }, { input: '192.168.0.0/16 + 100\n10.0.0.0/8 [5000]', - description: 'Large networks with high indices', + description: $t('tools.nth_ip.examples.largeNetworks.description'), }, { input: '203.0.113.0/24 @ 1\n203.0.113.0/24 @ -1', - description: 'First and last IP using positive/negative indexing', + description: $t('tools.nth_ip.examples.firstLastIP.description'), }, { input: '192.168.1.1-192.168.1.100 [25]\n192.168.1.101-192.168.1.200 [75]', - description: 'Sequential IP ranges with specific indices', + description: $t('tools.nth_ip.examples.sequentialRanges.description'), }, { input: '2001:db8:85a3::/48#65536\nfc00::/7 @ 1000000', - description: 'Large IPv6 address spaces', + description: $t('tools.nth_ip.examples.largeIPv6.description'), }, { input: '127.0.0.0/8 @ 256\n::1/128 @ 0\n169.254.0.0/16 [32768]', - description: 'Special-use addresses: loopback and link-local', + description: $t('tools.nth_ip.examples.specialUse.description'), }, - ]; + ]); function calculateIPs() { if (!inputText.trim()) { @@ -62,7 +70,7 @@ result = { calculations: [], summary: { totalCalculations: 0, validCalculations: 0, invalidCalculations: 0, outOfBoundsCalculations: 0 }, - errors: ['No valid input lines found'], + errors: [$t('tools.nth_ip.errors.noValidInputs')], }; return; } @@ -72,7 +80,7 @@ result = { calculations: [], summary: { totalCalculations: 0, validCalculations: 0, invalidCalculations: 0, outOfBoundsCalculations: 0 }, - errors: [error instanceof Error ? error.message : 'Unknown error occurred while calculating nth IPs'], + errors: [error instanceof Error ? error.message : $t('tools.nth_ip.errors.unknownError')], }; } finally { isLoading = false; @@ -87,7 +95,18 @@ let filename = ''; if (format === 'csv') { - const headers = 'Input,Network,Index,Offset,Result IP,Version,Total Addresses,In Bounds,Valid,Error'; + const headers = [ + $t('tools.nth_ip.export.headers.input'), + $t('tools.nth_ip.export.headers.network'), + $t('tools.nth_ip.export.headers.index'), + $t('tools.nth_ip.export.headers.offset'), + $t('tools.nth_ip.export.headers.resultIP'), + $t('tools.nth_ip.export.headers.version'), + $t('tools.nth_ip.export.headers.totalAddresses'), + $t('tools.nth_ip.export.headers.inBounds'), + $t('tools.nth_ip.export.headers.valid'), + $t('tools.nth_ip.export.headers.error'), + ].join(','); const rows = result.calculations.map( (calc) => `"${calc.input}","${calc.network}","${calc.index}","${calc.offset}","${calc.resultIP}","IPv${calc.version}","${calc.totalAddresses}","${calc.isInBounds}","${calc.isValid}","${calc.error || ''}"`, @@ -133,8 +152,8 @@ <div class="card"> <header class="card-header"> - <h1>Nth IP Calculator</h1> - <p>Resolve the IP address at a specific index within networks and ranges with optional global offset.</p> + <h1>{$t('tools.nth_ip.title')}</h1> + <p>{$t('tools.nth_ip.description')}</p> </header> <!-- Examples --> @@ -142,7 +161,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools.nth_ip.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (`${example.input}-${i}`)} @@ -150,7 +169,7 @@ class="example-card" class:selected={selectedExampleIndex === i && !userModified} onclick={() => loadExample(example, i)} - use:tooltip={`Calculate: ${example.input.split('\n')[0]}`} + use:tooltip={$t('tools.nth_ip.examples.calculate', { input: example.input.split('\n')[0] })} > <h5>{example.input.split('\n')[0]}</h5> <p>{example.description}</p> @@ -163,43 +182,40 @@ <!-- Input Form --> <div class="card input-card"> <div class="card-header"> - <h3>Network Configuration</h3> + <h3>{$t('tools.nth_ip.input.title')}</h3> </div> <div class="card-content"> <div class="form-row"> <div class="form-group textarea-group"> - <label - for="inputs" - use:tooltip={'Enter network and index specifications, one per line. Supports formats: network @ index, network [index], network index, or network#index'} - > - Network and Index Specifications + <label for="inputs" use:tooltip={$t('tools.nth_ip.input.tooltip')}> + {$t('tools.nth_ip.input.label')} </label> <textarea id="inputs" bind:value={inputText} oninput={handleInputChange} - placeholder="192.168.1.0/24 @ 10 10.0.0.0-10.0.0.255 [50] 172.16.0.0/16 100 2001:db8::/64#1000" + placeholder={$t('tools.nth_ip.input.placeholder')} rows="6" ></textarea> <div class="input-help"> - Formats: network @ index, network [index], network index, or network#index. Optional offset: + number + {$t('tools.nth_ip.input.help')} </div> </div> <div class="options-section"> <div class="option-group"> - <label for="offset" use:tooltip={'Add this value to all index calculations (0-based indexing)'}> - Global Offset + <label for="offset" use:tooltip={$t('tools.nth_ip.input.globalOffset.tooltip')}> + {$t('tools.nth_ip.input.globalOffset.label')} </label> <input id="offset" type="number" bind:value={globalOffset} oninput={handleInputChange} - placeholder="0" + placeholder={$t('tools.nth_ip.input.globalOffset.placeholder')} min="0" /> - <div class="option-help">Add this value to all index calculations (0-based indexing)</div> + <div class="option-help">{$t('tools.nth_ip.input.globalOffset.help')}</div> </div> </div> </div> @@ -211,7 +227,7 @@ <div class="card-content"> <div class="loading"> <Icon name="loader" size="sm" animate="spin" /> - Calculating IPs... + {$t('tools.nth_ip.calculating')} </div> </div> </div> @@ -225,7 +241,7 @@ <div class="error-content"> <Icon name="alert-triangle" size="md" /> <div> - <strong>Calculation Errors</strong> + <strong>{$t('tools.nth_ip.results.errors.title')}</strong> {#each result.errors as error, index (index)} <p>{error}</p> {/each} @@ -238,7 +254,7 @@ {#if result.calculations.length > 0} <div class="card summary-card"> <div class="card-header row"> - <h3>Calculation Summary</h3> + <h3>{$t('tools.nth_ip.results.summary.title')}</h3> <button class="copy-btn" class:copied={clipboard.isCopied('summary')} @@ -246,33 +262,38 @@ result && result.summary && clipboard.copy( - `Total: ${result.summary.totalCalculations}\nValid: ${result.summary.validCalculations}\nInvalid: ${result.summary.invalidCalculations}\nOut of Bounds: ${result.summary.outOfBoundsCalculations}`, + $t('tools.nth_ip.results.summary.copyText', { + total: result.summary.totalCalculations, + valid: result.summary.validCalculations, + invalid: result.summary.invalidCalculations, + outOfBounds: result.summary.outOfBoundsCalculations, + }), 'summary', )} - use:tooltip={'Copy summary to clipboard'} + use:tooltip={$t('tools.nth_ip.actions.copySummary')} > <Icon name={clipboard.isCopied('summary') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('summary') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('summary') ? $t('tools.nth_ip.actions.copied') : $t('tools.nth_ip.actions.copy')} </button> </div> <div class="card-content"> <div class="summary-stats"> <div class="info-card"> - <div class="info-label">Total</div> + <div class="info-label">{$t('tools.nth_ip.results.summary.total')}</div> <div class="metric-value">{result.summary.totalCalculations}</div> </div> <div class="info-card"> - <div class="info-label">Valid</div> + <div class="info-label">{$t('tools.nth_ip.results.summary.valid')}</div> <div class="metric-value success">{result.summary.validCalculations}</div> </div> <div class="info-card"> - <div class="info-label">Invalid</div> + <div class="info-label">{$t('tools.nth_ip.results.summary.invalid')}</div> <div class="metric-value" class:error={result.summary.invalidCalculations > 0}> {result.summary.invalidCalculations} </div> </div> <div class="info-card"> - <div class="info-label">Out of Bounds</div> + <div class="info-label">{$t('tools.nth_ip.results.summary.outOfBounds')}</div> <div class="metric-value" class:warning={result.summary.outOfBoundsCalculations > 0}> {result.summary.outOfBoundsCalculations} </div> @@ -283,15 +304,15 @@ <div class="card calculations-card"> <div class="card-header row"> - <h3>IP Calculations</h3> + <h3>{$t('tools.nth_ip.results.calculations.title')}</h3> <div class="export-buttons"> - <button onclick={() => exportResults('csv')} use:tooltip={'Export results as CSV file'}> + <button onclick={() => exportResults('csv')} use:tooltip={$t('tools.nth_ip.actions.exportCSV')}> <Icon name="csv-file" size="xs" /> - Export CSV + {$t('tools.nth_ip.actions.exportCSVLabel')} </button> - <button onclick={() => exportResults('json')} use:tooltip={'Export results as JSON file'}> + <button onclick={() => exportResults('json')} use:tooltip={$t('tools.nth_ip.actions.exportJSON')}> <Icon name="json-file" size="xs" /> - Export JSON + {$t('tools.nth_ip.actions.exportJSONLabel')} </button> </div> </div> @@ -312,32 +333,38 @@ class="copy-btn" class:copied={clipboard.isCopied(`input-${index}`)} onclick={() => clipboard.copy(calculation.input, `input-${index}`)} - use:tooltip={'Copy input specification'} + use:tooltip={$t('tools.nth_ip.actions.copyInput')} > <Icon name={clipboard.isCopied(`input-${index}`) ? 'check' : 'copy'} size="xs" /> </button> </div> <div class="input-meta"> - <span class="network-type" use:tooltip={`Network type: ${calculation.inputType}`} - >{calculation.inputType.toUpperCase()}</span + <span + class="network-type" + use:tooltip={$t('tools.nth_ip.results.calculations.networkTypeTooltip', { + type: calculation.inputType, + })}>{calculation.inputType.toUpperCase()}</span > - <span class="ip-version" use:tooltip={`IP version ${calculation.version}`} - >IPv{calculation.version}</span + <span + class="ip-version" + use:tooltip={$t('tools.nth_ip.results.calculations.ipVersionTooltip', { + version: calculation.version, + })}>IPv{calculation.version}</span > </div> </div> <div class="status"> {#if calculation.isValid && calculation.isInBounds} - <span use:tooltip={'Valid calculation within bounds'}> + <span use:tooltip={$t('tools.nth_ip.results.calculations.status.validInBounds')}> <Icon name="check-circle" size="md" /> </span> {:else if calculation.isValid && !calculation.isInBounds} - <span use:tooltip={'Valid calculation but index out of bounds'}> + <span use:tooltip={$t('tools.nth_ip.results.calculations.status.validOutOfBounds')}> <Icon name="alert-circle" size="md" /> </span> {:else} - <span use:tooltip={'Invalid calculation'}> + <span use:tooltip={$t('tools.nth_ip.results.calculations.status.invalid')}> <Icon name="x-circle" size="md" /> </span> {/if} @@ -348,8 +375,10 @@ <div class="calculation-details"> <div class="result-section"> <div class="result-ip"> - <span class="result-label" use:tooltip={'The calculated IP address at the specified index'} - >Result IP:</span + <span + class="result-label" + use:tooltip={$t('tools.nth_ip.results.calculations.resultIPTooltip')} + >{$t('tools.nth_ip.results.calculations.resultIP')}:</span > <div class="value-copy"> <span class="result-value">{calculation.resultIP}</span> @@ -357,7 +386,7 @@ class="copy-btn" class:copied={clipboard.isCopied(`result-${index}`)} onclick={() => clipboard.copy(calculation.resultIP, `result-${index}`)} - use:tooltip={'Copy result IP address'} + use:tooltip={$t('tools.nth_ip.actions.copyResultIP')} > <Icon name={clipboard.isCopied(`result-${index}`) ? 'check' : 'copy'} size="xs" /> </button> @@ -367,25 +396,30 @@ {#if !calculation.isInBounds} <div class="bounds-warning"> <Icon name="alert-triangle" size="sm" /> - <span>Index out of bounds</span> + <span>{$t('tools.nth_ip.results.calculations.indexOutOfBounds')}</span> </div> {/if} </div> <div class="calculation-info"> <div class="details-header"> - <h4>Calculation Details</h4> + <h4>{$t('tools.nth_ip.results.calculations.details.title')}</h4> </div> <div class="info-grid"> <div class="info-card"> - <div class="info-label" use:tooltip={'Network or range being processed'}>Network</div> + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.details.networkTooltip')} + > + {$t('tools.nth_ip.results.calculations.details.network')} + </div> <div class="value-copy"> <span class="ip-value">{calculation.network}</span> <button class="copy-btn" class:copied={clipboard.isCopied(`network-${index}`)} onclick={() => clipboard.copy(calculation.network, `network-${index}`)} - use:tooltip={'Copy network address'} + use:tooltip={$t('tools.nth_ip.actions.copyNetwork')} > <Icon name={clipboard.isCopied(`network-${index}`) ? 'check' : 'copy'} size="xs" /> </button> @@ -393,20 +427,31 @@ </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Total number of addresses in this network'}> - Total Addresses + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.details.totalAddressesTooltip')} + > + {$t('tools.nth_ip.results.calculations.details.totalAddresses')} </div> <div class="metric-value">{calculation.totalAddresses}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'The index position requested'}>Index</div> + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.details.indexTooltip')} + > + {$t('tools.nth_ip.results.calculations.details.index')} + </div> <div class="metric-value info">{calculation.index}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Global offset applied to the calculation'}> - Offset + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.details.offsetTooltip')} + > + {$t('tools.nth_ip.results.calculations.details.offset')} </div> <div class="metric-value">{calculation.offset}</div> </div> @@ -416,12 +461,17 @@ {#if calculation.details} <div> <div class="details-header"> - <h4>Network Details</h4> + <h4>{$t('tools.nth_ip.results.calculations.networkDetails.title')}</h4> </div> <div class="network-details"> <div class="details-grid"> <div class="info-card"> - <div class="info-label" use:tooltip={'First IP address in the network'}>Start</div> + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.networkDetails.startTooltip')} + > + {$t('tools.nth_ip.results.calculations.networkDetails.start')} + </div> <div class="value-copy"> <span class="ip-value">{calculation.details.networkStart}</span> <button @@ -430,7 +480,7 @@ onclick={() => calculation.details && clipboard.copy(calculation.details.networkStart, `start-${index}`)} - use:tooltip={'Copy network start address'} + use:tooltip={$t('tools.nth_ip.actions.copyNetworkStart')} > <Icon name={clipboard.isCopied(`start-${index}`) ? 'check' : 'copy'} size="xs" /> </button> @@ -438,7 +488,12 @@ </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Last IP address in the network'}>End</div> + <div + class="info-label" + use:tooltip={$t('tools.nth_ip.results.calculations.networkDetails.endTooltip')} + > + {$t('tools.nth_ip.results.calculations.networkDetails.end')} + </div> <div class="value-copy"> <span class="ip-value">{calculation.details.networkEnd}</span> <button @@ -447,7 +502,7 @@ onclick={() => calculation.details && clipboard.copy(calculation.details.networkEnd, `end-${index}`)} - use:tooltip={'Copy network end address'} + use:tooltip={$t('tools.nth_ip.actions.copyNetworkEnd')} > <Icon name={clipboard.isCopied(`end-${index}`) ? 'check' : 'copy'} size="xs" /> </button> diff --git a/src/lib/components/tools/PTRGenerator.svelte b/src/lib/components/tools/PTRGenerator.svelte index b178cbc9..30796709 100644 --- a/src/lib/components/tools/PTRGenerator.svelte +++ b/src/lib/components/tools/PTRGenerator.svelte @@ -2,6 +2,14 @@ import { tooltip } from '$lib/actions/tooltip.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let inputValue = $state('192.168.1.100'); let inputType = $state<'single' | 'cidr'>('single'); @@ -31,44 +39,44 @@ let _userModified = $state(false); let showZoneFiles = $state(true); - const examples = [ + const examples = $derived([ { - label: 'Single IPv4', + label: $t('tools.ptr_generator.examples.singleIPv4.label'), input: '192.168.1.100', type: 'single' as const, - description: 'Generate PTR for single IPv4 address', + description: $t('tools.ptr_generator.examples.singleIPv4.description'), }, { - label: 'Single IPv6', + label: $t('tools.ptr_generator.examples.singleIPv6.label'), input: '2001:db8::1', type: 'single' as const, - description: 'Generate PTR for single IPv6 address', + description: $t('tools.ptr_generator.examples.singleIPv6.description'), }, { - label: 'IPv4 /24 Subnet', + label: $t('tools.ptr_generator.examples.ipv4Subnet24.label'), input: '192.168.1.0/24', type: 'cidr' as const, - description: 'Generate PTRs for entire /24 subnet', + description: $t('tools.ptr_generator.examples.ipv4Subnet24.description'), }, { - label: 'IPv4 /28 Small Block', + label: $t('tools.ptr_generator.examples.ipv4SmallBlock.label'), input: '10.0.0.16/28', type: 'cidr' as const, - description: 'Generate PTRs for /28 block (16 addresses)', + description: $t('tools.ptr_generator.examples.ipv4SmallBlock.description'), }, { - label: 'IPv6 /64 Network', + label: $t('tools.ptr_generator.examples.ipv6Network.label'), input: '2001:db8::/64', type: 'cidr' as const, - description: 'Generate IPv6 PTR zone structure', + description: $t('tools.ptr_generator.examples.ipv6Network.description'), }, { - label: 'IPv6 /48 Prefix', + label: $t('tools.ptr_generator.examples.largeBlock.label'), input: '2001:db8:1000::/48', type: 'cidr' as const, - description: 'Generate IPv6 /48 PTR zone', + description: $t('tools.ptr_generator.examples.largeBlock.description'), }, - ]; + ]); function loadExample(example: (typeof examples)[0]) { inputValue = example.input; @@ -154,7 +162,7 @@ const prefix = parseInt(prefixStr); if (!isValidIPv4(network) || prefix < 0 || prefix > 32) { - throw new Error('Invalid IPv4 CIDR notation'); + throw new Error($t('tools.ptr_generator.errors.invalidIPv4CIDR')); } const networkParts = network.split('.').map((p) => parseInt(p)); @@ -162,7 +170,7 @@ // Limit to reasonable sizes if (hostBits > 16) { - throw new Error('CIDR block too large (more than 65536 addresses). Please use a smaller block.'); + throw new Error($t('tools.ptr_generator.errors.cidrTooLarge')); } const totalHosts = Math.pow(2, hostBits); @@ -193,13 +201,13 @@ const prefix = parseInt(prefixStr); if (!isValidIPv6(network) || prefix < 0 || prefix > 128) { - throw new Error('Invalid IPv6 CIDR notation'); + throw new Error($t('tools.ptr_generator.errors.invalidIPv6CIDR')); } // For IPv6, we'll generate a representative set rather than all addresses // since IPv6 networks can be astronomically large if (prefix > 64) { - throw new Error('IPv6 CIDR blocks smaller than /64 are not supported for enumeration'); + throw new Error($t('tools.ptr_generator.errors.ipv6CIDRTooSmall')); } // For demonstration, return just the network address and a few examples @@ -282,12 +290,12 @@ $TTL 86400 const { ptrName, zone } = generateIPv6PTR(trimmed); entries.push({ ip: trimmed, ptrName, type: 'IPv6', zone }); } else { - throw new Error('Invalid IP address format'); + throw new Error($t('tools.ptr_generator.errors.invalidIPFormat')); } } else { // CIDR notation if (!trimmed.includes('/')) { - throw new Error('CIDR notation requires a prefix length (e.g., 192.168.1.0/24)'); + throw new Error($t('tools.ptr_generator.errors.cidrRequiresPrefix')); } const [network] = trimmed.split('/'); @@ -305,7 +313,7 @@ $TTL 86400 entries.push({ ip, ptrName, type: 'IPv6', zone }); }); } else { - throw new Error('Invalid network address in CIDR notation'); + throw new Error($t('tools.ptr_generator.errors.invalidNetworkAddress')); } } @@ -339,7 +347,7 @@ $TTL 86400 } catch (error) { results = { success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + error: error instanceof Error ? error.message : $t('tools.ptr_generator.errors.unknownError'), entries: [], zoneFiles: [], summary: { totalEntries: 0, ipv4Entries: 0, ipv6Entries: 0, uniqueZones: 0 }, @@ -365,8 +373,8 @@ $TTL 86400 <div class="card"> <header class="card-header"> - <h1>PTR Record Generator</h1> - <p>Generate PTR record names for IPv4 and IPv6 addresses and CIDR blocks with zone file stubs</p> + <h1>{$t('tools.ptr_generator.title')}</h1> + <p>{$t('tools.ptr_generator.description')}</p> </header> <!-- Educational Overview Card --> @@ -375,20 +383,22 @@ $TTL 86400 <div class="overview-item"> <Icon name="rotate" size="sm" /> <div> - <strong>Reverse DNS:</strong> PTR records provide reverse DNS lookups, mapping IP addresses back to domain names. + <strong>{$t('tools.ptr_generator.overview.reverseDNS.title')}:</strong> + {$t('tools.ptr_generator.overview.reverseDNS.content')} </div> </div> <div class="overview-item"> <Icon name="server" size="sm" /> <div> - <strong>Zone Structure:</strong> IPv4 uses <code>in-addr.arpa</code> and IPv6 uses <code>ip6.arpa</code> for reverse - DNS zones. + <strong>{$t('tools.ptr_generator.overview.zoneStructure.title')}:</strong> + {$t('tools.ptr_generator.overview.zoneStructure.content')} </div> </div> <div class="overview-item"> <Icon name="file" size="sm" /> <div> - <strong>Zone Files:</strong> Generates ready-to-use DNS zone file stubs with proper SOA and NS records. + <strong>{$t('tools.ptr_generator.overview.zoneFiles.title')}:</strong> + {$t('tools.ptr_generator.overview.zoneFiles.content')} </div> </div> </div> @@ -399,7 +409,7 @@ $TTL 86400 <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools.ptr_generator.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -410,7 +420,9 @@ $TTL 86400 <div class="example-header"> <div class="example-label">{example.label}</div> <div class="example-type {example.type}"> - {example.type === 'single' ? 'Single IP' : 'CIDR Block'} + {example.type === 'single' + ? $t('tools.ptr_generator.examples.types.singleIP') + : $t('tools.ptr_generator.examples.types.cidrBlock')} </div> </div> <code class="example-input">{example.input}</code> @@ -425,20 +437,20 @@ $TTL 86400 <div class="card input-card"> <!-- Input Type Selection --> <div class="type-section"> - <h3 class="type-label">Input Type</h3> + <h3 class="type-label">{$t('tools.ptr_generator.input.type.label')}</h3> <div class="type-options"> <label class="type-option"> <input type="radio" bind:group={inputType} value="single" onchange={handleTypeChange} /> <div class="type-content"> <Icon name="target" size="sm" /> - <span>Single IP</span> + <span>{$t('tools.ptr_generator.input.type.singleIP')}</span> </div> </label> <label class="type-option"> <input type="radio" bind:group={inputType} value="cidr" onchange={handleTypeChange} /> <div class="type-content"> <Icon name="network" size="sm" /> - <span>CIDR Block</span> + <span>{$t('tools.ptr_generator.input.type.cidrBlock')}</span> </div> </label> </div> @@ -449,18 +461,22 @@ $TTL 86400 <label for="ip-input" use:tooltip={inputType === 'single' - ? 'Enter a single IPv4 or IPv6 address' - : 'Enter an IPv4 or IPv6 CIDR block (e.g., 192.168.1.0/24)'} + ? $t('tools.ptr_generator.input.address.tooltipSingle') + : $t('tools.ptr_generator.input.address.tooltipCIDR')} > <Icon name={inputType === 'single' ? 'target' : 'network'} size="sm" /> - {inputType === 'single' ? 'IP Address' : 'CIDR Block'} + {inputType === 'single' + ? $t('tools.ptr_generator.input.address.labelSingle') + : $t('tools.ptr_generator.input.address.labelCIDR')} </label> <input id="ip-input" type="text" bind:value={inputValue} oninput={handleInputChange} - placeholder={inputType === 'single' ? '192.168.1.100 or 2001:db8::1' : '192.168.1.0/24 or 2001:db8::/64'} + placeholder={inputType === 'single' + ? $t('tools.ptr_generator.input.address.placeholderSingle') + : $t('tools.ptr_generator.input.address.placeholderCIDR')} class="ip-input {results?.success === true ? 'valid' : results?.success === false ? 'invalid' : ''}" spellcheck="false" /> @@ -472,8 +488,8 @@ $TTL 86400 <input type="checkbox" bind:checked={showZoneFiles} /> <div class="checkbox-custom"></div> <div class="checkbox-content"> - <span class="checkbox-label">Generate zone file stubs</span> - <div class="checkbox-hint">Include DNS zone file templates with SOA and NS records</div> + <span class="checkbox-label">{$t('tools.ptr_generator.input.options.generateZoneFiles')}</span> + <div class="checkbox-hint">{$t('tools.ptr_generator.input.options.zoneFilesHint')}</div> </div> </label> </div> @@ -484,27 +500,27 @@ $TTL 86400 <div class="card results-card"> {#if results.success} <div class="results-header"> - <h3>PTR Records Generated</h3> + <h3>{$t('tools.ptr_generator.results.title')}</h3> <div class="summary-stats"> <div class="stat-item"> <span class="stat-value">{results.summary.totalEntries}</span> - <span class="stat-label">Total PTRs</span> + <span class="stat-label">{$t('tools.ptr_generator.results.summary.totalPTRs')}</span> </div> {#if results.summary.ipv4Entries > 0} <div class="stat-item"> <span class="stat-value">{results.summary.ipv4Entries}</span> - <span class="stat-label">IPv4</span> + <span class="stat-label">{$t('tools.ptr_generator.results.summary.ipv4')}</span> </div> {/if} {#if results.summary.ipv6Entries > 0} <div class="stat-item"> <span class="stat-value">{results.summary.ipv6Entries}</span> - <span class="stat-label">IPv6</span> + <span class="stat-label">{$t('tools.ptr_generator.results.summary.ipv6')}</span> </div> {/if} <div class="stat-item"> <span class="stat-value">{results.summary.uniqueZones}</span> - <span class="stat-label">Zones</span> + <span class="stat-label">{$t('tools.ptr_generator.results.summary.zones')}</span> </div> </div> </div> @@ -513,14 +529,14 @@ $TTL 86400 <div class="ptr-records"> <h4> <Icon name="list" size="sm" /> - PTR Records + {$t('tools.ptr_generator.results.records.title')} </h4> <div class="records-table"> <div class="table-header"> - <div class="col-ip">IP Address</div> - <div class="col-ptr">PTR Record Name</div> - <div class="col-type">Type</div> - <div class="col-zone">Zone</div> + <div class="col-ip">{$t('tools.ptr_generator.results.records.ipAddress')}</div> + <div class="col-ptr">{$t('tools.ptr_generator.results.records.ptrName')}</div> + <div class="col-type">{$t('tools.ptr_generator.results.records.type')}</div> + <div class="col-zone">{$t('tools.ptr_generator.results.records.zone')}</div> </div> {#each results.entries.slice(0, 50) as entry (`${entry.ip}-${entry.ptrName}`)} <div class="table-row"> @@ -546,7 +562,7 @@ $TTL 86400 {/each} {#if results.entries.length > 50} <div class="table-truncated"> - ... and {results.entries.length - 50} more records + {$t('tools.ptr_generator.results.records.moreRecords', { count: results.entries.length - 50 })} </div> {/if} </div> @@ -557,7 +573,7 @@ $TTL 86400 <div class="zone-files"> <h4> <Icon name="file" size="sm" /> - Zone File Stubs + {$t('tools.ptr_generator.results.zoneFiles.title')} </h4> {#each results.zoneFiles as zoneFile (zoneFile.zone)} <div class="zone-file"> @@ -571,7 +587,7 @@ $TTL 86400 onclick={() => clipboard.copy(zoneFile.content, `zone-${zoneFile.zone}`)} > <Icon name={clipboard.isCopied(`zone-${zoneFile.zone}`) ? 'check' : 'copy'} size="sm" /> - Copy Zone File + {$t('tools.ptr_generator.actions.copyZoneFile')} </button> </div> <pre class="zone-content"><code>{zoneFile.content}</code></pre> @@ -582,15 +598,15 @@ $TTL 86400 {:else} <div class="error-result"> <Icon name="alert-triangle" size="lg" /> - <h4>Generation Error</h4> + <h4>{$t('tools.ptr_generator.errors.title')}</h4> <p>{results.error}</p> <div class="error-help"> - <strong>Valid formats:</strong> + <strong>{$t('tools.ptr_generator.errors.validFormats')}:</strong> <ul> - <li>Single IPv4: 192.168.1.100</li> - <li>Single IPv6: 2001:db8::1</li> - <li>IPv4 CIDR: 192.168.1.0/24 (max /16)</li> - <li>IPv6 CIDR: 2001:db8::/64 (max /64)</li> + <li>{$t('tools.ptr_generator.errors.formats.singleIPv4')}</li> + <li>{$t('tools.ptr_generator.errors.formats.singleIPv6')}</li> + <li>{$t('tools.ptr_generator.errors.formats.ipv4CIDR')}</li> + <li>{$t('tools.ptr_generator.errors.formats.ipv6CIDR')}</li> </ul> </div> </div> @@ -602,34 +618,30 @@ $TTL 86400 <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>What are PTR Records?</h4> + <h4>{$t('tools.ptr_generator.education.whatArePTRRecords.title')}</h4> <p> - PTR (Pointer) records provide reverse DNS lookups, allowing you to resolve an IP address back to a domain - name. They're essential for mail servers, logging, and network diagnostics. + {$t('tools.ptr_generator.education.whatArePTRRecords.content')} </p> </div> <div class="education-item info-panel"> - <h4>Zone Structure</h4> + <h4>{$t('tools.ptr_generator.education.zoneStructure.title')}</h4> <p> - IPv4 reverse zones use <code>in-addr.arpa</code> with octets reversed (e.g., 1.168.192.in-addr.arpa for - 192.168.1.x). IPv6 uses <code>ip6.arpa</code> with individual hex digits reversed. + {$t('tools.ptr_generator.education.zoneStructure.content')} </p> </div> <div class="education-item info-panel"> - <h4>Zone Delegation</h4> + <h4>{$t('tools.ptr_generator.education.zoneDelegation.title')}</h4> <p> - PTR zones are typically delegated by your ISP or hosting provider. The zone files generated here provide - templates that can be customized for your specific DNS infrastructure. + {$t('tools.ptr_generator.education.zoneDelegation.content')} </p> </div> <div class="education-item info-panel"> - <h4>Best Practices</h4> + <h4>{$t('tools.ptr_generator.education.bestPractices.title')}</h4> <p> - Ensure PTR records match forward DNS (A/AAAA) records. Use descriptive hostnames that include the IP address - or subnet information for easier network management. + {$t('tools.ptr_generator.education.bestPractices.content')} </p> </div> </div> @@ -653,14 +665,6 @@ $TTL 86400 gap: var(--spacing-sm); color: var(--text-secondary); - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } - strong { color: var(--text-primary); } @@ -1256,14 +1260,6 @@ $TTL 86400 line-height: 1.6; margin: 0; } - - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } } @media (max-width: 768px) { diff --git a/src/lib/components/tools/PTRSweepPlanner.svelte b/src/lib/components/tools/PTRSweepPlanner.svelte index 5edca6d0..9d8e7da2 100644 --- a/src/lib/components/tools/PTRSweepPlanner.svelte +++ b/src/lib/components/tools/PTRSweepPlanner.svelte @@ -3,6 +3,13 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { analyzePTRCoverage, type PTRCoverageAnalysis } from '$lib/utils/reverse-dns.js'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let cidrInput = $state('192.168.1.0/24'); let existingPTRsInput = $state(`100.1.168.192.in-addr.arpa @@ -21,18 +28,18 @@ let selectedExample = $state<string | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Partial Coverage', + label: $t('tools.ptr_sweep_planner.examples.partialCoverage.label'), cidr: '192.168.1.0/28', ptrs: `100.1.168.192.in-addr.arpa 101.1.168.192.in-addr.arpa 105.1.168.192.in-addr.arpa`, pattern: '.*\\.example\\.com\\.$', - description: 'Network with some missing PTRs', + description: $t('tools.ptr_sweep_planner.examples.partialCoverage.description'), }, { - label: 'Mixed Naming', + label: $t('tools.ptr_sweep_planner.examples.mixedNaming.label'), cidr: '10.0.0.0/28', ptrs: `1.0.0.10.in-addr.arpa 2.0.0.10.in-addr.arpa @@ -40,24 +47,24 @@ 15.0.0.10.in-addr.arpa 20.0.0.10.in-addr.arpa`, pattern: 'host-.*\\.corp\\.com\\.$', - description: 'Check pattern compliance', + description: $t('tools.ptr_sweep_planner.examples.mixedNaming.description'), }, { - label: 'IPv6 Network', + label: $t('tools.ptr_sweep_planner.examples.ipv6Network.label'), cidr: '2001:db8:1000::/64', ptrs: `0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.8.b.d.0.1.0.0.2.ip6.arpa 1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.8.b.d.0.1.0.0.2.ip6.arpa`, pattern: '.*\\.ipv6\\.example\\.com\\.$', - description: 'IPv6 PTR coverage analysis', + description: $t('tools.ptr_sweep_planner.examples.ipv6Network.description'), }, - ]; + ]); - const patternHelp = [ - { pattern: '.*\\.example\\.com\\.$', description: 'Any hostname ending in .example.com.' }, - { pattern: 'host-.*\\.corp\\.com\\.$', description: 'Hostnames starting with "host-" in corp.com' }, - { pattern: '^[0-9-]+\\.net\\.example\\.com\\.$', description: 'IP-based hostnames in net.example.com' }, - { pattern: '(server|workstation)-.*', description: 'Names starting with "server-" or "workstation-"' }, - ]; + const patternHelp = $derived([ + { pattern: '.*\\.example\\.com\\.$', description: $t('tools.ptr_sweep_planner.patternHelp.anyExample') }, + { pattern: 'host-.*\\.corp\\.com\\.$', description: $t('tools.ptr_sweep_planner.patternHelp.hostCorp') }, + { pattern: '^[0-9-]+\\.net\\.example\\.com\\.$', description: $t('tools.ptr_sweep_planner.patternHelp.ipBased') }, + { pattern: '(server|workstation)-.*', description: $t('tools.ptr_sweep_planner.patternHelp.serverWorkstation') }, + ]); function loadExample(example: (typeof examples)[0]) { cidrInput = example.cidr; @@ -145,8 +152,8 @@ <div class="card"> <header class="card-header"> - <h1>PTR Sweep Planner</h1> - <p>Analyze PTR record coverage for network blocks and identify missing or extra records</p> + <h1>{$t('tools.ptr_sweep_planner.title')}</h1> + <p>{$t('tools.ptr_sweep_planner.description')}</p> </header> <!-- Educational Overview Card --> @@ -155,19 +162,22 @@ <div class="overview-item"> <Icon name="search" size="sm" /> <div> - <strong>Coverage Analysis:</strong> Compare expected PTR records for a CIDR block against actual existing records. + <strong>{$t('tools.ptr_sweep_planner.overview.coverageAnalysis.title')}:</strong> + {$t('tools.ptr_sweep_planner.overview.coverageAnalysis.description')} </div> </div> <div class="overview-item"> <Icon name="target" size="sm" /> <div> - <strong>Pattern Matching:</strong> Validate existing PTR records against regex naming patterns for compliance. + <strong>{$t('tools.ptr_sweep_planner.overview.patternMatching.title')}:</strong> + {$t('tools.ptr_sweep_planner.overview.patternMatching.description')} </div> </div> <div class="overview-item"> <Icon name="list-check" size="sm" /> <div> - <strong>Gap Analysis:</strong> Identify missing PTRs, extra PTRs, and generate remediation plans. + <strong>{$t('tools.ptr_sweep_planner.overview.gapAnalysis.title')}:</strong> + {$t('tools.ptr_sweep_planner.overview.gapAnalysis.description')} </div> </div> </div> @@ -178,7 +188,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools.ptr_sweep_planner.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example, idx (`${example.label}-${idx}`)} @@ -205,16 +215,16 @@ <div class="card input-card"> <!-- CIDR Input --> <div class="input-group"> - <label for="cidr-input" use:tooltip={'Enter the CIDR block to analyze PTR coverage for'}> + <label for="cidr-input" use:tooltip={$t('tools.ptr_sweep_planner.form.cidrBlock.tooltip')}> <Icon name="network" size="sm" /> - CIDR Block to Analyze + {$t('tools.ptr_sweep_planner.form.cidrBlock.label')} </label> <input id="cidr-input" type="text" bind:value={cidrInput} oninput={handleInputChange} - placeholder="192.168.1.0/24 or 2001:db8::/64" + placeholder={$t('tools.ptr_sweep_planner.form.cidrBlock.placeholder')} class="cidr-input {results?.success === true ? 'valid' : results?.success === false ? 'invalid' : ''}" spellcheck="false" /> @@ -222,15 +232,15 @@ <!-- Existing PTRs Input --> <div class="input-group"> - <label for="ptrs-input" use:tooltip={'Paste existing PTR record names, one per line'}> + <label for="ptrs-input" use:tooltip={$t('tools.ptr_sweep_planner.form.existingPtrs.tooltip')}> <Icon name="list" size="sm" /> - Existing PTR Records + {$t('tools.ptr_sweep_planner.form.existingPtrs.label')} </label> <textarea id="ptrs-input" bind:value={existingPTRsInput} oninput={handleInputChange} - placeholder="100.1.168.192.in-addr.arpa 101.1.168.192.in-addr.arpa ..." + placeholder={$t('tools.ptr_sweep_planner.form.existingPtrs.placeholder')} class="ptrs-input" rows="8" spellcheck="false" @@ -239,23 +249,23 @@ <!-- Naming Pattern --> <div class="input-group"> - <label for="pattern-input" use:tooltip={'Optional regex pattern to validate PTR target naming'}> + <label for="pattern-input" use:tooltip={$t('tools.ptr_sweep_planner.form.namingPattern.tooltip')}> <Icon name="search" size="sm" /> - Naming Pattern (Optional) + {$t('tools.ptr_sweep_planner.form.namingPattern.label')} </label> <input id="pattern-input" type="text" bind:value={namingPattern} oninput={handleInputChange} - placeholder=".*\.example\.com\.$" + placeholder={$t('tools.ptr_sweep_planner.form.namingPattern.placeholder')} class="pattern-input" spellcheck="false" /> <!-- Pattern Help --> <div class="pattern-help"> - <h4>Common Patterns:</h4> + <h4>{$t('tools.ptr_sweep_planner.patternHelp.title')}:</h4> <div class="pattern-examples"> {#each patternHelp as item, helpIdx (`${item.pattern}-${helpIdx}`)} <button @@ -279,7 +289,7 @@ <div class="card results-card"> {#if results.success} <div class="results-header"> - <h3>Coverage Analysis Results</h3> + <h3>{$t('tools.ptr_sweep_planner.results.title')}</h3> <div class="coverage-meter"> <div class="coverage-bar"> <div @@ -292,7 +302,7 @@ ></div> </div> <div class="coverage-text"> - {results.analysis.coverage.toFixed(1)}% Coverage + {$t('tools.ptr_sweep_planner.results.coverage', { percentage: results.analysis.coverage.toFixed(1) })} </div> </div> </div> @@ -301,24 +311,24 @@ <div class="summary-stats"> <div class="stat-item"> <span class="stat-value">{results.analysis.totalAddresses}</span> - <span class="stat-label">Expected PTRs</span> + <span class="stat-label">{$t('tools.ptr_sweep_planner.results.stats.expectedPtrs')}</span> </div> <div class="stat-item"> <span class="stat-value">{results.analysis.totalAddresses - results.analysis.missingPTRs.length}</span> - <span class="stat-label">Found PTRs</span> + <span class="stat-label">{$t('tools.ptr_sweep_planner.results.stats.foundPtrs')}</span> </div> <div class="stat-item"> <span class="stat-value">{results.analysis.missingPTRs.length}</span> - <span class="stat-label">Missing PTRs</span> + <span class="stat-label">{$t('tools.ptr_sweep_planner.results.stats.missingPtrs')}</span> </div> <div class="stat-item"> <span class="stat-value">{results.analysis.extraPTRs.length}</span> - <span class="stat-label">Extra PTRs</span> + <span class="stat-label">{$t('tools.ptr_sweep_planner.results.stats.extraPtrs')}</span> </div> {#if namingPattern.trim()} <div class="stat-item"> <span class="stat-value">{results.analysis.patternMatches}</span> - <span class="stat-label">Pattern Matches</span> + <span class="stat-label">{$t('tools.ptr_sweep_planner.results.stats.patternMatches')}</span> </div> {/if} </div> @@ -331,14 +341,16 @@ <div class="section-header"> <h4> <Icon name="alert-circle" size="sm" /> - Missing PTR Records ({results.analysis.missingPTRs.length}) + {$t('tools.ptr_sweep_planner.results.sections.missingPtrs.title', { + count: results.analysis.missingPTRs.length, + })} </h4> <button class="copy-button {clipboard.isCopied('missing-ptrs') ? 'copied' : ''}" onclick={() => results && clipboard.copy(results.analysis.missingPTRs.join('\n'), 'missing-ptrs')} > <Icon name={clipboard.isCopied('missing-ptrs') ? 'check' : 'copy'} size="sm" /> - Copy List + {$t('tools.ptr_sweep_planner.results.copyList')} </button> </div> @@ -350,7 +362,9 @@ {/each} {#if results.analysis.missingPTRs.length > 20} <div class="records-truncated"> - ... and {results.analysis.missingPTRs.length - 20} more missing records + {$t('tools.ptr_sweep_planner.results.sections.missingPtrs.truncated', { + count: results.analysis.missingPTRs.length - 20, + })} </div> {/if} </div> @@ -363,14 +377,16 @@ <div class="section-header"> <h4> <Icon name="plus-circle" size="sm" /> - Extra PTR Records ({results.analysis.extraPTRs.length}) + {$t('tools.ptr_sweep_planner.results.sections.extraPtrs.title', { + count: results.analysis.extraPTRs.length, + })} </h4> <button class="copy-button {clipboard.isCopied('extra-ptrs') ? 'copied' : ''}" onclick={() => results && clipboard.copy(results.analysis.extraPTRs.join('\n'), 'extra-ptrs')} > <Icon name={clipboard.isCopied('extra-ptrs') ? 'check' : 'copy'} size="sm" /> - Copy List + {$t('tools.ptr_sweep_planner.results.copyList')} </button> </div> @@ -382,7 +398,9 @@ {/each} {#if results.analysis.extraPTRs.length > 10} <div class="records-truncated"> - ... and {results.analysis.extraPTRs.length - 10} more extra records + {$t('tools.ptr_sweep_planner.results.sections.extraPtrs.truncated', { + count: results.analysis.extraPTRs.length - 10, + })} </div> {/if} </div> @@ -394,7 +412,7 @@ <div class="section-header"> <h4> <Icon name="clipboard-list" size="sm" /> - Recommended Actions + {$t('tools.ptr_sweep_planner.results.actions.title')} </h4> </div> @@ -403,7 +421,7 @@ <div class="action-item"> <div class="action-header"> <Icon name="plus" size="sm" /> - <span>Create Missing PTR Records</span> + <span>{$t('tools.ptr_sweep_planner.results.actions.createMissing.title')}</span> <button class="copy-button {clipboard.isCopied('create-commands') ? 'copied' : ''}" onclick={() => @@ -411,11 +429,13 @@ clipboard.copy(generateCreateCommands(results.analysis.missingPTRs), 'create-commands')} > <Icon name={clipboard.isCopied('create-commands') ? 'check' : 'copy'} size="sm" /> - Copy Zone Lines + {$t('tools.ptr_sweep_planner.results.actions.createMissing.copyZoneLines')} </button> </div> <div class="action-description"> - Add {results.analysis.missingPTRs.length} missing PTR records to your reverse zone files. + {$t('tools.ptr_sweep_planner.results.actions.createMissing.description', { + count: results.analysis.missingPTRs.length, + })} </div> </div> {/if} @@ -424,11 +444,12 @@ <div class="action-item"> <div class="action-header"> <Icon name="trash-2" size="sm" /> - <span>Review Extra Records</span> + <span>{$t('tools.ptr_sweep_planner.results.actions.reviewExtra.title')}</span> </div> <div class="action-description"> - Review {results.analysis.extraPTRs.length} extra PTR records that don't correspond to addresses in this - CIDR block. + {$t('tools.ptr_sweep_planner.results.actions.reviewExtra.description', { + count: results.analysis.extraPTRs.length, + })} </div> </div> {/if} @@ -437,12 +458,15 @@ <div class="action-item"> <div class="action-header"> <Icon name="edit" size="sm" /> - <span>Fix Naming Pattern Violations</span> + <span>{$t('tools.ptr_sweep_planner.results.actions.fixPattern.title')}</span> </div> <div class="action-description"> - {results.analysis.totalAddresses - - results.analysis.missingPTRs.length - - results.analysis.patternMatches} existing PTR records don't match the naming pattern. + {$t('tools.ptr_sweep_planner.results.actions.fixPattern.description', { + count: + results.analysis.totalAddresses - + results.analysis.missingPTRs.length - + results.analysis.patternMatches, + })} </div> </div> {/if} @@ -451,10 +475,12 @@ <div class="action-item success"> <div class="action-header"> <Icon name="check-circle" size="sm" /> - <span>Excellent Coverage!</span> + <span>{$t('tools.ptr_sweep_planner.results.actions.excellentCoverage.title')}</span> </div> <div class="action-description"> - Your reverse DNS coverage is excellent with {results.analysis.coverage.toFixed(1)}% completeness. + {$t('tools.ptr_sweep_planner.results.actions.excellentCoverage.description', { + percentage: results.analysis.coverage.toFixed(1), + })} </div> </div> {/if} @@ -464,14 +490,14 @@ {:else} <div class="error-result"> <Icon name="alert-triangle" size="lg" /> - <h4>Analysis Error</h4> + <h4>{$t('tools.ptr_sweep_planner.error.title')}</h4> <p>{results.error}</p> <div class="error-help"> - <strong>Check your input:</strong> + <strong>{$t('tools.ptr_sweep_planner.error.checkInput')}:</strong> <ul> - <li>CIDR notation: 192.168.1.0/24, 2001:db8::/64</li> - <li>PTR records: One per line, proper format</li> - <li>Pattern: Valid JavaScript regex syntax</li> + <li>{$t('tools.ptr_sweep_planner.error.help.cidr')}</li> + <li>{$t('tools.ptr_sweep_planner.error.help.ptrRecords')}</li> + <li>{$t('tools.ptr_sweep_planner.error.help.pattern')}</li> </ul> </div> </div> @@ -483,37 +509,30 @@ <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>PTR Coverage Planning</h4> + <h4>{$t('tools.ptr_sweep_planner.education.ptrCoverage.title')}</h4> <p> - PTR coverage analysis helps identify gaps in reverse DNS configuration. Complete coverage ensures all IPs in - your network blocks have proper reverse DNS entries for troubleshooting and compliance requirements. + {$t('tools.ptr_sweep_planner.education.ptrCoverage.description')} </p> </div> <div class="education-item info-panel"> - <h4>Naming Pattern Validation</h4> + <h4>{$t('tools.ptr_sweep_planner.education.namingPattern.title')}</h4> <p> - Use regex patterns to enforce consistent hostname naming conventions. Patterns like - <code>.*\.corp\.example\.com\.$</code> ensure all PTR records point to properly formatted hostnames within your - domain structure. + {$t('tools.ptr_sweep_planner.education.namingPattern.description')} </p> </div> <div class="education-item info-panel"> - <h4>Common PTR Issues</h4> + <h4>{$t('tools.ptr_sweep_planner.education.commonIssues.title')}</h4> <p> - Missing PTRs can cause mail delivery problems and failed reverse lookups. Extra PTRs may indicate outdated - records or configuration drift. Regular PTR sweeps help maintain DNS hygiene and network documentation - accuracy. + {$t('tools.ptr_sweep_planner.education.commonIssues.description')} </p> </div> <div class="education-item info-panel"> - <h4>Remediation Best Practices</h4> + <h4>{$t('tools.ptr_sweep_planner.education.remediation.title')}</h4> <p> - Create missing PTRs in batches, verify forward/reverse consistency (A/AAAA records), and establish monitoring - to detect future gaps. Use descriptive hostnames that include network or service information for easier - troubleshooting. + {$t('tools.ptr_sweep_planner.education.remediation.description')} </p> </div> </div> @@ -1039,14 +1058,6 @@ line-height: 1.6; margin: 0; } - - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } } @media (max-width: 768px) { diff --git a/src/lib/components/tools/PXEProfileBuilder.svelte b/src/lib/components/tools/PXEProfileBuilder.svelte index 71cc258e..5d6f732d 100644 --- a/src/lib/components/tools/PXEProfileBuilder.svelte +++ b/src/lib/components/tools/PXEProfileBuilder.svelte @@ -4,6 +4,7 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables'; + import { t } from '$lib/stores/language'; import { type PXEProfile, type PXEResult, @@ -44,14 +45,14 @@ description: `${preset.architecture === 'auto' ? 'Auto-detect UEFI/BIOS' : preset.architecture.toUpperCase()} - ${preset.tftpServer}`, })); - const architectureOptions = [ - { value: 'auto', label: 'Auto-detect' }, - { value: 'bios', label: 'BIOS only' }, - { value: 'uefi-x64', label: 'UEFI x64' }, - { value: 'uefi-x86', label: 'UEFI x86' }, - { value: 'uefi-arm64', label: 'UEFI ARM64' }, - { value: 'uefi-arm32', label: 'UEFI ARM32' }, - ]; + const architectureOptions = $derived([ + { value: 'auto', label: $t('tools/pxe-profile-builder.profile.architecture.options.auto') }, + { value: 'bios', label: $t('tools/pxe-profile-builder.profile.architecture.options.bios') }, + { value: 'uefi-x64', label: $t('tools/pxe-profile-builder.profile.architecture.options.uefiX64') }, + { value: 'uefi-x86', label: $t('tools/pxe-profile-builder.profile.architecture.options.uefiX86') }, + { value: 'uefi-arm64', label: $t('tools/pxe-profile-builder.profile.architecture.options.uefiArm64') }, + { value: 'uefi-arm32', label: $t('tools/pxe-profile-builder.profile.architecture.options.uefiArm32') }, + ]); function validateAndGenerate(currentProfile: PXEProfile): void { validationErrors = validatePXEProfile(currentProfile); @@ -115,8 +116,8 @@ </script> <ToolContentContainer - title="PXE Profile Generator" - description="Generate PXE boot profiles with automatic UEFI/BIOS detection using DHCP Options 93/94. Configure bootfiles for different architectures and generate dhcpd/Kea configuration snippets." + title={$t('tools/pxe-profile-builder.title')} + description={$t('tools/pxe-profile-builder.subtitle')} > <ExamplesCard examples={presetExamples} @@ -128,37 +129,42 @@ <div class="card input-card"> <div class="card-header"> - <h3>Profile Configuration</h3> + <h3>{$t('tools/pxe-profile-builder.profile.title')}</h3> </div> <div class="card-content"> <div class="input-group"> <label for="profile-name"> <Icon name="tag" size="sm" /> - Profile Name - <span class="required">*</span> + {$t('tools/pxe-profile-builder.profile.name.label')} + <span class="required">{$t('tools/pxe-profile-builder.common.required')}</span> </label> - <input id="profile-name" type="text" bind:value={profile.name} placeholder="e.g., Production PXE" /> + <input + id="profile-name" + type="text" + bind:value={profile.name} + placeholder={$t('tools/pxe-profile-builder.profile.name.placeholder')} + /> </div> <div class="input-group"> <label for="tftp-server"> <Icon name="server" size="sm" /> - TFTP Server (Option 66) - <span class="required">*</span> + {$t('tools/pxe-profile-builder.profile.tftpServer.label')} + <span class="required">{$t('tools/pxe-profile-builder.common.required')}</span> </label> <input id="tftp-server" type="text" bind:value={profile.tftpServer} - placeholder="e.g., pxe.example.com or 192.168.1.10" + placeholder={$t('tools/pxe-profile-builder.profile.tftpServer.placeholder')} /> - <span class="help-text">Hostname or IP address of the TFTP server</span> + <span class="help-text">{$t('tools/pxe-profile-builder.profile.tftpServer.hint')}</span> </div> <div class="input-group"> <label for="architecture"> <Icon name="cpu" size="sm" /> - Architecture Mode + {$t('tools/pxe-profile-builder.profile.architecture.label')} </label> <select id="architecture" bind:value={profile.architecture}> {#each architectureOptions as option (option.value)} @@ -166,7 +172,7 @@ {/each} </select> <span class="help-text"> - Auto-detect uses Option 93 to serve different bootfiles based on client firmware + {$t('tools/pxe-profile-builder.profile.architecture.hint')} </span> </div> </div> @@ -174,9 +180,9 @@ <div class="card input-card"> <div class="card-header"> - <h3>Bootfiles (Option 67)</h3> + <h3>{$t('tools/pxe-profile-builder.bootfiles.title')}</h3> <p class="help-text"> - Configure bootfile names for different client architectures. At least one bootfile is required. + {$t('tools/pxe-profile-builder.bootfiles.hint')} </p> </div> <div class="card-content"> @@ -184,18 +190,18 @@ <div class="input-group"> <label for="bios-bootfile"> <Icon name="hard-drive" size="sm" /> - BIOS Bootfile + {$t('tools/pxe-profile-builder.bootfiles.bios.label')} {#if profile.architecture === 'bios' || profile.architecture === 'auto'} - <span class="recommended">(recommended)</span> + <span class="recommended">{$t('tools/pxe-profile-builder.common.recommended')}</span> {/if} </label> <input id="bios-bootfile" type="text" bind:value={profile.biosBootfile} - placeholder="e.g., pxelinux.0 or undionly.kpxe" + placeholder={$t('tools/pxe-profile-builder.bootfiles.bios.placeholder')} /> - <span class="help-text">For legacy BIOS systems (Arch Type 0x0000)</span> + <span class="help-text">{$t('tools/pxe-profile-builder.bootfiles.bios.hint')}</span> </div> {/if} @@ -203,18 +209,18 @@ <div class="input-group"> <label for="uefi-x64-bootfile"> <Icon name="hard-drive" size="sm" /> - UEFI x64 Bootfile + {$t('tools/pxe-profile-builder.bootfiles.uefiX64.label')} {#if profile.architecture === 'uefi-x64' || profile.architecture === 'auto'} - <span class="recommended">(recommended)</span> + <span class="recommended">{$t('tools/pxe-profile-builder.common.recommended')}</span> {/if} </label> <input id="uefi-x64-bootfile" type="text" bind:value={profile.uefiX64Bootfile} - placeholder="e.g., bootx64.efi or ipxe-x64.efi" + placeholder={$t('tools/pxe-profile-builder.bootfiles.uefiX64.placeholder')} /> - <span class="help-text">For UEFI x86-64 systems (Arch Type 0x0007)</span> + <span class="help-text">{$t('tools/pxe-profile-builder.bootfiles.uefiX64.hint')}</span> </div> {/if} @@ -222,15 +228,15 @@ <div class="input-group"> <label for="uefi-x86-bootfile"> <Icon name="hard-drive" size="sm" /> - UEFI x86 Bootfile + {$t('tools/pxe-profile-builder.bootfiles.uefiX86.label')} </label> <input id="uefi-x86-bootfile" type="text" bind:value={profile.uefiX86Bootfile} - placeholder="e.g., bootia32.efi" + placeholder={$t('tools/pxe-profile-builder.bootfiles.uefiX86.placeholder')} /> - <span class="help-text">For UEFI IA32 systems (Arch Type 0x0006)</span> + <span class="help-text">{$t('tools/pxe-profile-builder.bootfiles.uefiX86.hint')}</span> </div> {/if} @@ -238,15 +244,15 @@ <div class="input-group"> <label for="uefi-arm64-bootfile"> <Icon name="hard-drive" size="sm" /> - UEFI ARM64 Bootfile + {$t('tools/pxe-profile-builder.bootfiles.uefiArm64.label')} </label> <input id="uefi-arm64-bootfile" type="text" bind:value={profile.uefiArm64Bootfile} - placeholder="e.g., bootaa64.efi" + placeholder={$t('tools/pxe-profile-builder.bootfiles.uefiArm64.placeholder')} /> - <span class="help-text">For UEFI ARM 64-bit systems (Arch Type 0x000b)</span> + <span class="help-text">{$t('tools/pxe-profile-builder.bootfiles.uefiArm64.hint')}</span> </div> {/if} @@ -254,15 +260,15 @@ <div class="input-group"> <label for="uefi-arm32-bootfile"> <Icon name="hard-drive" size="sm" /> - UEFI ARM32 Bootfile + {$t('tools/pxe-profile-builder.bootfiles.uefiArm32.label')} </label> <input id="uefi-arm32-bootfile" type="text" bind:value={profile.uefiArm32Bootfile} - placeholder="e.g., bootarm.efi" + placeholder={$t('tools/pxe-profile-builder.bootfiles.uefiArm32.placeholder')} /> - <span class="help-text">For UEFI ARM 32-bit systems (Arch Type 0x000a)</span> + <span class="help-text">{$t('tools/pxe-profile-builder.bootfiles.uefiArm32.hint')}</span> </div> {/if} </div> @@ -270,7 +276,7 @@ {#if validationErrors.length > 0} <div class="card errors-card"> - <h3>Validation Errors</h3> + <h3>{$t('tools/pxe-profile-builder.errors.validation')}</h3> {#each validationErrors as error, i (i)} <div class="error-message"> <Icon name="alert-triangle" size="sm" /> @@ -285,25 +291,35 @@ <div class="card input-card"> <div class="card-header"> - <h3>Network Settings (Optional)</h3> - <p class="help-text">Customize network values for configuration examples below</p> + <h3>{$t('tools/pxe-profile-builder.network.title')}</h3> + <p class="help-text">{$t('tools/pxe-profile-builder.network.hint')}</p> </div> <div class="card-content"> <div class="input-row"> <div class="input-group"> <label for="subnet"> <Icon name="network" size="sm" /> - Subnet + {$t('tools/pxe-profile-builder.network.subnet.label')} </label> - <input id="subnet" type="text" bind:value={profile.network!.subnet} placeholder="192.168.1.0" /> + <input + id="subnet" + type="text" + bind:value={profile.network!.subnet} + placeholder={$t('tools/pxe-profile-builder.network.subnet.placeholder')} + /> </div> <div class="input-group"> <label for="netmask"> <Icon name="network" size="sm" /> - Netmask + {$t('tools/pxe-profile-builder.network.netmask.label')} </label> - <input id="netmask" type="text" bind:value={profile.network!.netmask} placeholder="255.255.255.0" /> + <input + id="netmask" + type="text" + bind:value={profile.network!.netmask} + placeholder={$t('tools/pxe-profile-builder.network.netmask.placeholder')} + /> </div> </div> @@ -311,17 +327,27 @@ <div class="input-group"> <label for="range-start"> <Icon name="arrow-right" size="sm" /> - Range Start + {$t('tools/pxe-profile-builder.network.rangeStart.label')} </label> - <input id="range-start" type="text" bind:value={profile.network!.rangeStart} placeholder="192.168.1.100" /> + <input + id="range-start" + type="text" + bind:value={profile.network!.rangeStart} + placeholder={$t('tools/pxe-profile-builder.network.rangeStart.placeholder')} + /> </div> <div class="input-group"> <label for="range-end"> <Icon name="arrow-right" size="sm" /> - Range End + {$t('tools/pxe-profile-builder.network.rangeEnd.label')} </label> - <input id="range-end" type="text" bind:value={profile.network!.rangeEnd} placeholder="192.168.1.200" /> + <input + id="range-end" + type="text" + bind:value={profile.network!.rangeEnd} + placeholder={$t('tools/pxe-profile-builder.network.rangeEnd.placeholder')} + /> </div> </div> @@ -329,24 +355,34 @@ <div class="input-group"> <label for="gateway"> <Icon name="arrow-right" size="sm" /> - Gateway + {$t('tools/pxe-profile-builder.network.gateway.label')} </label> - <input id="gateway" type="text" bind:value={profile.network!.gateway} placeholder="192.168.1.1" /> + <input + id="gateway" + type="text" + bind:value={profile.network!.gateway} + placeholder={$t('tools/pxe-profile-builder.network.gateway.placeholder')} + /> </div> <div class="input-group"> <label for="dns"> <Icon name="globe" size="sm" /> - DNS Server + {$t('tools/pxe-profile-builder.network.dns.label')} </label> - <input id="dns" type="text" bind:value={profile.network!.dns} placeholder="8.8.8.8" /> + <input + id="dns" + type="text" + bind:value={profile.network!.dns} + placeholder={$t('tools/pxe-profile-builder.network.dns.placeholder')} + /> </div> </div> </div> {#if networkValidationErrors.length > 0} <div class="network-errors"> - <h4>Network Settings Errors</h4> + <h4>{$t('tools/pxe-profile-builder.errors.network')}</h4> {#each networkValidationErrors as error, i (i)} <div class="network-error-item"> <Icon name="alert-triangle" size="sm" /> @@ -360,36 +396,57 @@ {#if result && networkValidationErrors.length === 0} <div class="card results"> - <h3>Profile Summary</h3> + <h3>{$t('tools/pxe-profile-builder.results.summary.title')}</h3> <div class="summary-card"> - <div><strong>Profile Name:</strong> {result.profile.name}</div> - <div><strong>Architecture Mode:</strong> {result.profile.architecture}</div> - <div><strong>TFTP Server:</strong> {result.profile.tftpServer}</div> + <div><strong>{$t('tools/pxe-profile-builder.results.summary.profileName')}</strong> {result.profile.name}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.architecture')}</strong> + {result.profile.architecture} + </div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.tftpServer')}</strong> + {result.profile.tftpServer} + </div> {#if result.profile.biosBootfile} - <div><strong>BIOS Bootfile:</strong> {result.profile.biosBootfile}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.biosBootfile')}</strong> + {result.profile.biosBootfile} + </div> {/if} {#if result.profile.uefiX64Bootfile} - <div><strong>UEFI x64 Bootfile:</strong> {result.profile.uefiX64Bootfile}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.uefiX64Bootfile')}</strong> + {result.profile.uefiX64Bootfile} + </div> {/if} {#if result.profile.uefiX86Bootfile} - <div><strong>UEFI x86 Bootfile:</strong> {result.profile.uefiX86Bootfile}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.uefiX86Bootfile')}</strong> + {result.profile.uefiX86Bootfile} + </div> {/if} {#if result.profile.uefiArm64Bootfile} - <div><strong>UEFI ARM64 Bootfile:</strong> {result.profile.uefiArm64Bootfile}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.uefiArm64Bootfile')}</strong> + {result.profile.uefiArm64Bootfile} + </div> {/if} {#if result.profile.uefiArm32Bootfile} - <div><strong>UEFI ARM32 Bootfile:</strong> {result.profile.uefiArm32Bootfile}</div> + <div> + <strong>{$t('tools/pxe-profile-builder.results.summary.uefiArm32Bootfile')}</strong> + {result.profile.uefiArm32Bootfile} + </div> {/if} </div> </div> <div class="card results"> - <h3>DHCP Server Configuration</h3> + <h3>{$t('tools/pxe-profile-builder.results.config.title')}</h3> {#if result.examples.iscDhcpd} <div class="output-group"> <div class="output-header"> - <h4>ISC dhcpd Configuration</h4> + <h4>{$t('tools/pxe-profile-builder.results.config.iscDhcpd')}</h4> <button type="button" class="copy-btn" @@ -397,7 +454,9 @@ onclick={() => clipboard.copy(result!.examples.iscDhcpd!, 'isc')} > <Icon name={clipboard.isCopied('isc') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('isc') ? 'Copied' : 'Copy'} + {clipboard.isCopied('isc') + ? $t('tools/pxe-profile-builder.common.copied') + : $t('tools/pxe-profile-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.examples.iscDhcpd}</pre> @@ -407,7 +466,7 @@ {#if result.examples.keaDhcp4} <div class="output-group"> <div class="output-header"> - <h4>Kea DHCPv4 Configuration</h4> + <h4>{$t('tools/pxe-profile-builder.results.config.keaDhcp4')}</h4> <button type="button" class="copy-btn" @@ -415,7 +474,9 @@ onclick={() => clipboard.copy(result!.examples.keaDhcp4!, 'kea')} > <Icon name={clipboard.isCopied('kea') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('kea') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea') + ? $t('tools/pxe-profile-builder.common.copied') + : $t('tools/pxe-profile-builder.common.copy')} </button> </div> <pre class="output-value code-block">{result.examples.keaDhcp4}</pre> @@ -424,21 +485,19 @@ </div> <div class="card results"> - <h3>PXE Boot Architecture Detection</h3> + <h3>{$t('tools/pxe-profile-builder.results.archDetection.title')}</h3> <p> - DHCP Option 93 (Client System Architecture Type) allows the DHCP server to detect the client's firmware type and - serve the appropriate bootfile. Common architecture types: + {$t('tools/pxe-profile-builder.results.archDetection.intro')} </p> <ul> - <li><strong>0x0000</strong> - Intel x86PC (Legacy BIOS)</li> - <li><strong>0x0006</strong> - EFI IA32 (32-bit UEFI)</li> - <li><strong>0x0007</strong> - EFI BC (64-bit UEFI, most common)</li> - <li><strong>0x000a</strong> - EFI ARM 32-bit</li> - <li><strong>0x000b</strong> - EFI ARM 64-bit</li> + <li><strong>0x0000</strong> - {$t('tools/pxe-profile-builder.results.archDetection.types.bios')}</li> + <li><strong>0x0006</strong> - {$t('tools/pxe-profile-builder.results.archDetection.types.efiIa32')}</li> + <li><strong>0x0007</strong> - {$t('tools/pxe-profile-builder.results.archDetection.types.efiBC')}</li> + <li><strong>0x000a</strong> - {$t('tools/pxe-profile-builder.results.archDetection.types.efiArm32')}</li> + <li><strong>0x000b</strong> - {$t('tools/pxe-profile-builder.results.archDetection.types.efiArm64')}</li> </ul> <p> - When using auto-detect mode, the DHCP server will examine Option 93 in the client's DHCPDISCOVER message and - respond with the appropriate bootfile for that architecture. + {$t('tools/pxe-profile-builder.results.archDetection.autoDetectInfo')} </p> </div> {/if} diff --git a/src/lib/components/tools/PrefixDelegation.svelte b/src/lib/components/tools/PrefixDelegation.svelte index 61f6fe0f..11bd7219 100644 --- a/src/lib/components/tools/PrefixDelegation.svelte +++ b/src/lib/components/tools/PrefixDelegation.svelte @@ -12,9 +12,17 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import ExamplesCard from '$lib/components/common/ExamplesCard.svelte'; import { useClipboard } from '$lib/composables/useClipboard.svelte'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; const clipboard = useClipboard(); + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools.prefix-delegation'); + }); + // State let iaid = $state<number>(1); let t1 = $state<number | undefined>(302400); @@ -85,10 +93,7 @@ }); </script> -<ToolContentContainer - title="DHCPv6 Prefix Delegation (IA_PD)" - description="Build DHCPv6 IA_PD options for delegating IPv6 prefixes to requesting routers. Configure Identity Association for Prefix Delegation (Option 25) with IA Prefix options (Option 26) per RFC 8415." -> +<ToolContentContainer title={$t('tools/prefix-delegation.title')} description={$t('tools/prefix-delegation.subtitle')}> <ExamplesCard examples={PREFIX_DELEGATION_EXAMPLES} onSelect={(ex) => loadExample(ex)} @@ -97,26 +102,42 @@ /> <div class="card input-card"> - <h3>Prefix Delegation Configuration</h3> + <h3>{$t('tools/prefix-delegation.config.title')}</h3> <div class="form-row"> <div class="form-group"> - <label for="iaid">IAID (Identity Association ID)</label> + <label for="iaid">{$t('tools/prefix-delegation.config.iaid.label')}</label> <input id="iaid" type="number" bind:value={iaid} min="0" max="4294967295" class="input" /> - <span class="hint">Unique identifier for this IA_PD (0-4294967295)</span> + <span class="hint">{$t('tools/prefix-delegation.config.iaid.hint')}</span> </div> <div class="form-group"> - <label for="t1">T1 Renewal Time (seconds)</label> - <input id="t1" type="number" bind:value={t1} min="0" max="4294967295" placeholder="Optional" class="input" /> + <label for="t1">{$t('tools/prefix-delegation.config.t1.label')}</label> + <input + id="t1" + type="number" + bind:value={t1} + min="0" + max="4294967295" + placeholder={$t('tools/prefix-delegation.config.t1.placeholder')} + class="input" + /> {#if t1} <span class="hint">= {formatTime(t1)}</span> {/if} </div> <div class="form-group"> - <label for="t2">T2 Rebinding Time (seconds)</label> - <input id="t2" type="number" bind:value={t2} min="0" max="4294967295" placeholder="Optional" class="input" /> + <label for="t2">{$t('tools/prefix-delegation.config.t2.label')}</label> + <input + id="t2" + type="number" + bind:value={t2} + min="0" + max="4294967295" + placeholder={$t('tools/prefix-delegation.config.t2.placeholder')} + class="input" + /> {#if t2} <span class="hint">= {formatTime(t2)}</span> {/if} @@ -124,7 +145,7 @@ </div> <div class="form-group"> - <label for="prefix-0">Delegated Prefixes</label> + <label for="prefix-0">{$t('tools/prefix-delegation.config.prefixes.label')}</label> {#each prefixes as prefix, i (i)} <div class="prefix-row"> <div class="prefix-inputs"> @@ -132,7 +153,7 @@ id={i === 0 ? 'prefix-0' : undefined} type="text" bind:value={prefix.prefix} - placeholder="e.g., 2001:db8::/56" + placeholder={$t('tools/prefix-delegation.config.prefixes.prefixPlaceholder')} class="input" aria-label={i > 0 ? `Prefix ${i + 1}` : undefined} /> @@ -141,7 +162,7 @@ bind:value={prefix.preferredLifetime} min="0" max="4294967295" - placeholder="Preferred (s)" + placeholder={$t('tools/prefix-delegation.config.prefixes.preferredPlaceholder')} class="input input-sm" aria-label="Preferred lifetime" /> @@ -150,22 +171,26 @@ bind:value={prefix.validLifetime} min="0" max="4294967295" - placeholder="Valid (s)" + placeholder={$t('tools/prefix-delegation.config.prefixes.validPlaceholder')} class="input input-sm" aria-label="Valid lifetime" /> </div> {#if prefixes.length > 1} - <button class="btn btn-danger btn-sm" onclick={() => removePrefix(i)}>Remove</button> + <button class="btn btn-danger btn-sm" onclick={() => removePrefix(i)} + >{$t('tools/prefix-delegation.config.prefixes.removeButton')}</button + > {/if} </div> {/each} - <button class="btn btn-secondary btn-sm" onclick={addPrefix}>Add Prefix</button> + <button class="btn btn-secondary btn-sm" onclick={addPrefix} + >{$t('tools/prefix-delegation.config.prefixes.addButton')}</button + > </div> {#if errors.length > 0} <div class="error-card"> - <strong>Validation Errors:</strong> + <strong>{$t('tools/prefix-delegation.errors.title')}</strong> <ul> {#each errors as error, i (i)} <li>{error}</li> @@ -177,26 +202,26 @@ {#if result} <div class="card result-card"> - <h3>Option 25 - IA_PD</h3> + <h3>{$t('tools/prefix-delegation.results.title')}</h3> <div class="result-grid"> <div class="result-item"> - <span class="label">IAID:</span> + <span class="label">{$t('tools/prefix-delegation.results.iaid')}</span> <code class="code-value">{result.iaid} (0x{result.iaidHex})</code> </div> <div class="result-item"> - <span class="label">T1 Renewal:</span> + <span class="label">{$t('tools/prefix-delegation.results.t1Renewal')}</span> <span class="value">{result.t1Formatted} ({result.t1}s)</span> </div> <div class="result-item"> - <span class="label">T2 Rebinding:</span> + <span class="label">{$t('tools/prefix-delegation.results.t2Rebinding')}</span> <span class="value">{result.t2Formatted} ({result.t2}s)</span> </div> <div class="result-item"> - <span class="label">Full Hex:</span> + <span class="label">{$t('tools/prefix-delegation.results.fullHex')}</span> <code class="code-value">{result.fullHex}</code> <button class="btn-copy" @@ -204,12 +229,14 @@ onclick={() => clipboard.copy(result!.fullHex, 'full-hex')} aria-label="Copy hex" > - {clipboard.isCopied('full-hex') ? 'Copied' : 'Copy'} + {clipboard.isCopied('full-hex') + ? $t('tools/prefix-delegation.common.copied') + : $t('tools/prefix-delegation.common.copy')} </button> </div> <div class="result-item"> - <span class="label">Wire Format:</span> + <span class="label">{$t('tools/prefix-delegation.results.wireFormat')}</span> <code class="code-value">{result.fullWireFormat}</code> <button class="btn-copy" @@ -217,18 +244,20 @@ onclick={() => clipboard.copy(result!.fullWireFormat, 'full-wire')} aria-label="Copy wire format" > - {clipboard.isCopied('full-wire') ? 'Copied' : 'Copy'} + {clipboard.isCopied('full-wire') + ? $t('tools/prefix-delegation.common.copied') + : $t('tools/prefix-delegation.common.copy')} </button> </div> <div class="result-item"> - <span class="label">Total Length:</span> + <span class="label">{$t('tools/prefix-delegation.results.totalLength')}</span> <span class="value">{result.totalLength} bytes</span> </div> </div> <div class="prefixes-section"> - <h4>Delegated Prefixes (Option 26)</h4> + <h4>{$t('tools/prefix-delegation.results.prefixesSection.title')}</h4> {#each result.prefixes as prefix, i (i)} <div class="prefix-card"> <div class="prefix-header"> @@ -237,15 +266,19 @@ </div> <div class="prefix-details"> <div class="detail-item"> - <span class="detail-label">Preferred Lifetime:</span> + <span class="detail-label" + >{$t('tools/prefix-delegation.results.prefixesSection.preferredLifetime')}</span + > <span>{prefix.preferredLifetimeFormatted} ({prefix.preferredLifetime}s)</span> </div> <div class="detail-item"> - <span class="detail-label">Valid Lifetime:</span> + <span class="detail-label">{$t('tools/prefix-delegation.results.prefixesSection.validLifetime')}</span> <span>{prefix.validLifetimeFormatted} ({prefix.validLifetime}s)</span> </div> <div class="detail-item"> - <span class="detail-label">Wire Format:</span> + <span class="detail-label" + >{$t('tools/prefix-delegation.results.prefixesSection.prefixWireFormat')}</span + > <code class="code-small">{prefix.wireFormat}</code> <button class="btn-copy btn-copy-sm" @@ -253,7 +286,9 @@ onclick={() => clipboard.copy(prefix.wireFormat.replace(/\s/g, ''), `prefix-wire-${i}`)} aria-label="Copy prefix wire format" > - {clipboard.isCopied(`prefix-wire-${i}`) ? 'Copied' : 'Copy'} + {clipboard.isCopied(`prefix-wire-${i}`) + ? $t('tools/prefix-delegation.common.copied') + : $t('tools/prefix-delegation.common.copy')} </button> </div> </div> @@ -262,17 +297,19 @@ </div> <div class="config-section"> - <h4>Configuration Example</h4> + <h4>{$t('tools/prefix-delegation.results.configSection.title')}</h4> <div class="output-group"> <div class="output-header"> - <h5>Kea DHCPv6</h5> + <h5>{$t('tools/prefix-delegation.results.configSection.keaDhcp6')}</h5> <button class="btn-copy" class:copied={clipboard.isCopied('kea-config')} onclick={() => clipboard.copy(result!.examples.keaDhcp6!, 'kea-config')} > - {clipboard.isCopied('kea-config') ? 'Copied' : 'Copy'} + {clipboard.isCopied('kea-config') + ? $t('tools/prefix-delegation.common.copied') + : $t('tools/prefix-delegation.common.copy')} </button> </div> <pre class="code-block"><code>{result.examples.keaDhcp6}</code></pre> diff --git a/src/lib/components/tools/RPBuilder.svelte b/src/lib/components/tools/RPBuilder.svelte index 303f5b85..02281872 100644 --- a/src/lib/components/tools/RPBuilder.svelte +++ b/src/lib/components/tools/RPBuilder.svelte @@ -1,7 +1,15 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import { Copy, Download, Check, Mail } from 'lucide-svelte'; import { tooltip } from '$lib/actions/tooltip.js'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let domain = $state(''); let mailboxDname = $state('admin.example.com.'); @@ -10,32 +18,32 @@ const clipboard = useClipboard(); - const roleExamples = [ + const roleExamples = $derived([ { - name: 'System Administrator', + name: $t('tools.rp_builder.examples.roles.systemAdmin.name'), mbox: 'admin.example.com.', txt: 'admin-info.example.com.', - description: 'Primary system administrator contact', + description: $t('tools.rp_builder.examples.roles.systemAdmin.description'), }, { - name: 'Webmaster', + name: $t('tools.rp_builder.examples.roles.webmaster.name'), mbox: 'webmaster.example.com.', txt: 'webmaster-info.example.com.', - description: 'Website administrator contact', + description: $t('tools.rp_builder.examples.roles.webmaster.description'), }, { - name: 'Security Contact', + name: $t('tools.rp_builder.examples.roles.security.name'), mbox: 'security.example.com.', txt: 'security-info.example.com.', - description: 'Security incident response contact', + description: $t('tools.rp_builder.examples.roles.security.description'), }, { - name: 'DNS Administrator', + name: $t('tools.rp_builder.examples.roles.dnsAdmin.name'), mbox: 'dns-admin.example.com.', txt: 'dns-admin-info.example.com.', - description: 'DNS zone administrator', + description: $t('tools.rp_builder.examples.roles.dnsAdmin.description'), }, - ]; + ]); let rpRecord = $derived.by(() => { if (!domain.trim()) return ''; @@ -51,7 +59,8 @@ if (!txtDname.trim() || txtDname === '.') return ''; const txt = txtDname.trim().replace(/\.$/, ''); - return `${txt}. IN TXT "Administrative contact for ${domain.trim().replace(/\.$/, '') || 'this domain'}. Please use the mailbox specified in the RP record for contact."`; + const domainName = domain.trim().replace(/\.$/, '') || 'this domain'; + return `${txt}. IN TXT "Administrative contact for ${domainName}. Please use the mailbox specified in the RP record for contact."`; }); let isValid = $derived.by(() => { @@ -62,23 +71,23 @@ const warns = []; if (mailboxDname && !mailboxDname.includes('.')) { - warns.push('Mailbox domain name should be a fully qualified domain name'); + warns.push($t('tools.rp_builder.alerts.warnings.mailboxFqdn')); } if (txtDname && txtDname !== '.' && !txtDname.includes('.')) { - warns.push('TXT domain name should be a fully qualified domain name or "."'); + warns.push($t('tools.rp_builder.alerts.warnings.txtFqdn')); } if (mailboxDname && mailboxDname.endsWith('.')) { // This is correct } else if (mailboxDname && mailboxDname !== '.') { - warns.push('Domain names in RP records should end with a dot (.) for absolute names'); + warns.push($t('tools.rp_builder.alerts.warnings.mailboxDot')); } if (txtDname && txtDname.endsWith('.')) { // This is correct } else if (txtDname && txtDname !== '.') { - warns.push('TXT domain name should end with a dot (.) for absolute names'); + warns.push($t('tools.rp_builder.alerts.warnings.txtDot')); } return warns; @@ -88,15 +97,15 @@ const infos = []; if (mailboxDname === '.') { - infos.push('Using "." for mailbox means no mailbox is specified'); + infos.push($t('tools.rp_builder.alerts.info.noMailbox')); } if (txtDname === '.') { - infos.push('Using "." for TXT means no additional text information is provided'); + infos.push($t('tools.rp_builder.alerts.info.noTxt')); } if (txtDname && txtDname !== '.') { - infos.push(`Remember to create the TXT record at ${txtDname} with contact information`); + infos.push($t('tools.rp_builder.alerts.info.txtRecord', { txtDname })); } return infos; @@ -164,15 +173,15 @@ <div class="rp-builder"> <div class="card"> <div class="card-header"> - <h1>RP Record Builder</h1> - <p>Create RP (Responsible Person) records to specify administrative contacts for your domains</p> + <h1>{$t('tools.rp_builder.title')}</h1> + <p>{$t('tools.rp_builder.subtitle')}</p> </div> <div class="card-content"> <!-- Role Examples --> <details bind:open={showExamples} class="examples-section"> <summary> - <span>Common Role Examples</span> + <span>{$t('tools.rp_builder.examples.title')}</span> <svg class="chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor"> <polyline points="6,9 12,15 18,9"></polyline> </svg> @@ -191,90 +200,96 @@ <!-- Input Form --> <div class="input-section"> <div class="field-group"> - <label for="domain" use:tooltip={'The domain name for which this RP record will be created'}> - Domain Name * + <label for="domain" use:tooltip={$t('tools.rp_builder.form.domain.tooltip')}> + {$t('tools.rp_builder.form.domain.label')} * </label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> - <small>The domain name for this RP record</small> + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools.rp_builder.form.domain.placeholder')} + /> + <small>{$t('tools.rp_builder.form.domain.help')}</small> </div> <!-- Email to Domain Name Converter --> <div class="converter-section"> - <h4>Email to Domain Name Converter</h4> + <h4>{$t('tools.rp_builder.form.converter.title')}</h4> <div class="converter-input"> - <input type="email" bind:value={emailInput} placeholder="admin@example.com" /> - <button onclick={convertEmailToDname} class="btn-success" disabled={!emailInput.trim()}> Convert </button> + <input + type="email" + bind:value={emailInput} + placeholder={$t('tools.rp_builder.form.converter.placeholder')} + /> + <button onclick={convertEmailToDname} class="btn-success" disabled={!emailInput.trim()}> + {$t('tools.rp_builder.form.converter.button')} + </button> </div> - <small>Enter an email to automatically convert to domain name format</small> + <small>{$t('tools.rp_builder.form.converter.help')}</small> </div> <div class="field-group"> - <label - for="mailbox" - use:tooltip={"Domain name encoding the email address. Use '.' for no contact specified."} - > - Mailbox Domain Name * + <label for="mailbox" use:tooltip={$t('tools.rp_builder.form.mailbox.tooltip')}> + {$t('tools.rp_builder.form.mailbox.label')} * </label> <input id="mailbox" type="text" bind:value={mailboxDname} - placeholder="admin.example.com." + placeholder={$t('tools.rp_builder.form.mailbox.placeholder')} class="mono-input" /> - <small> Domain name encoding the email address (use "." for no contact) </small> + <small> {$t('tools.rp_builder.form.mailbox.help')} </small> {#if mailboxDname && mailboxDname !== '.'} <div class="email-preview"> <Mail size="12" /> - Email: {dnameToEmail(mailboxDname) || 'Invalid format'} + {$t('tools.rp_builder.form.mailbox.emailPreview')} + {dnameToEmail(mailboxDname) || $t('tools.rp_builder.output.invalidFormat')} </div> {/if} </div> <div class="field-group"> - <label - for="txt" - use:tooltip={"Domain name where TXT record with additional contact information can be found. Use '.' for no additional info."} - > - TXT Domain Name + <label for="txt" use:tooltip={$t('tools.rp_builder.form.txt.tooltip')}> + {$t('tools.rp_builder.form.txt.label')} </label> <input id="txt" type="text" bind:value={txtDname} - placeholder="admin-info.example.com." + placeholder={$t('tools.rp_builder.form.txt.placeholder')} class="mono-input" /> - <small> Domain name where TXT record with contact info can be found (use "." for none) </small> + <small> {$t('tools.rp_builder.form.txt.help')} </small> </div> </div> <!-- Output --> <div class="output-section"> <div class="output-group"> - <h3>Generated RP Record</h3> + <h3>{$t('tools.rp_builder.output.rpRecord')}</h3> <div class="code-output"> {#if isValid} <pre>{rpRecord}</pre> {:else} - <p class="placeholder-text">Fill in the required fields to generate the RP record</p> + <p class="placeholder-text">{$t('tools.rp_builder.output.placeholder')}</p> {/if} </div> </div> {#if txtRecord} <div class="output-group"> - <h3>Suggested TXT Record</h3> + <h3>{$t('tools.rp_builder.output.txtRecord')}</h3> <div class="code-output txt-output"> <pre>{txtRecord}</pre> - <small>This TXT record should be created at the specified domain</small> + <small>{$t('tools.rp_builder.output.txtHelp')}</small> </div> </div> {/if} {#if info.length > 0} <div class="alert alert-info"> - <h4>Information</h4> + <h4>{$t('tools.rp_builder.alerts.info.title')}</h4> <ul> {#each info as infoItem, index (index)} <li>{infoItem}</li> @@ -285,7 +300,7 @@ {#if warnings.length > 0} <div class="alert alert-warning"> - <h4>Configuration Warnings</h4> + <h4>{$t('tools.rp_builder.alerts.warnings.title')}</h4> <ul> {#each warnings as warning, index (index)} <li>{warning}</li> @@ -304,10 +319,10 @@ > {#if clipboard.isCopied('copy')} <Check size="16" /> - Copied! + {$t('tools.rp_builder.buttons.copied')} {:else} <Copy size="16" /> - Copy Records + {$t('tools.rp_builder.buttons.copy')} {/if} </button> <button @@ -318,10 +333,10 @@ > {#if clipboard.isCopied('download')} <Check size="16" /> - Downloaded! + {$t('tools.rp_builder.buttons.downloaded')} {:else} <Download size="16" /> - Download + {$t('tools.rp_builder.buttons.download')} {/if} </button> </div> @@ -332,50 +347,48 @@ <!-- Information Section --> <div class="info-section"> <div class="card info-card"> - <h4>About RP Records</h4> + <h4>{$t('tools.rp_builder.info.about.title')}</h4> <p> - RP (Responsible Person) records identify the responsible person for a domain or host. They specify both a - mailbox (encoded as a domain name) and optionally point to a TXT record with additional contact information. - This allows automated discovery of administrative contacts. + {$t('tools.rp_builder.info.about.description')} </p> </div> <div class="info-grid"> <div class="card"> - <h4>Email Encoding</h4> + <h4>{$t('tools.rp_builder.info.encoding.title')}</h4> <div class="encoding-examples"> - <p>Email addresses are encoded as domain names:</p> + <p>{$t('tools.rp_builder.info.encoding.description')}</p> <div class="code-example"> - <div><strong>Email:</strong> admin@example.com</div> - <div><strong>Encoded:</strong> admin.example.com.</div> + <div><strong>Email:</strong> {$t('tools.rp_builder.info.encoding.examples.simple.email')}</div> + <div><strong>Encoded:</strong> {$t('tools.rp_builder.info.encoding.examples.simple.encoded')}</div> </div> <div class="code-example"> - <div><strong>Email:</strong> user.name@example.com</div> - <div><strong>Encoded:</strong> user\.name.example.com.</div> + <div><strong>Email:</strong> {$t('tools.rp_builder.info.encoding.examples.complex.email')}</div> + <div><strong>Encoded:</strong> {$t('tools.rp_builder.info.encoding.examples.complex.encoded')}</div> </div> - <small>Dots in the local part are escaped with backslashes</small> + <small>{$t('tools.rp_builder.info.encoding.note')}</small> </div> </div> <div class="card"> - <h4>Common Use Cases</h4> + <h4>{$t('tools.rp_builder.info.useCases.title')}</h4> <ul class="use-cases"> - <li>Zone administrator contact</li> - <li>Server administrator contact</li> - <li>Security incident response</li> - <li>Automated contact discovery</li> - <li>Compliance requirements</li> + <li>{$t('tools.rp_builder.info.useCases.items.zone')}</li> + <li>{$t('tools.rp_builder.info.useCases.items.server')}</li> + <li>{$t('tools.rp_builder.info.useCases.items.security')}</li> + <li>{$t('tools.rp_builder.info.useCases.items.automated')}</li> + <li>{$t('tools.rp_builder.info.useCases.items.compliance')}</li> </ul> </div> </div> <div class="card best-practices-card"> - <h4>Best Practices</h4> + <h4>{$t('tools.rp_builder.info.bestPractices.title')}</h4> <ul class="best-practices"> - <li>Always use fully qualified domain names ending with a dot</li> - <li>Create corresponding TXT records with detailed contact information</li> - <li>Keep contact information up to date and monitored</li> - <li>Consider creating role-based contacts rather than personal ones</li> + <li>{$t('tools.rp_builder.info.bestPractices.items.fqdn')}</li> + <li>{$t('tools.rp_builder.info.bestPractices.items.txtRecords')}</li> + <li>{$t('tools.rp_builder.info.bestPractices.items.upToDate')}</li> + <li>{$t('tools.rp_builder.info.bestPractices.items.rolesBased')}</li> </ul> </div> </div> diff --git a/src/lib/components/tools/RRSIGPlanner.svelte b/src/lib/components/tools/RRSIGPlanner.svelte index 177872cf..6a00c6ee 100644 --- a/src/lib/components/tools/RRSIGPlanner.svelte +++ b/src/lib/components/tools/RRSIGPlanner.svelte @@ -1,6 +1,13 @@ <script lang="ts"> import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools/rrsig-planner'); + }); import { suggestRRSIGWindows, formatRRSIGDates, @@ -37,26 +44,30 @@ function copyCurrentWindow() { if (!currentWindowFormatted) return; - const text = `RRSIG Timing Window: -Inception: ${currentWindowFormatted.inceptionFormatted} (${currentWindowFormatted.inceptionTimestamp}) -Expiration: ${currentWindowFormatted.expirationFormatted} (${currentWindowFormatted.expirationTimestamp}) -Renewal Time: ${currentWindowFormatted.renewalFormatted}`; + const text = $t('copyTemplates.single', { + inception: currentWindowFormatted.inceptionFormatted, + inceptionTimestamp: currentWindowFormatted.inceptionTimestamp, + expiration: currentWindowFormatted.expirationFormatted, + expirationTimestamp: currentWindowFormatted.expirationTimestamp, + renewal: currentWindowFormatted.renewalFormatted, + }); clipboard.copy(text, 'current'); } function copyBothWindows() { if (!currentWindowFormatted || !nextWindowFormatted) return; - const text = `RRSIG Planning Schedule: - -Current Window: -Inception: ${currentWindowFormatted.inceptionFormatted} (${currentWindowFormatted.inceptionTimestamp}) -Expiration: ${currentWindowFormatted.expirationFormatted} (${currentWindowFormatted.expirationTimestamp}) -Renewal Time: ${currentWindowFormatted.renewalFormatted} - -Next Window: -Inception: ${nextWindowFormatted.inceptionFormatted} (${nextWindowFormatted.inceptionTimestamp}) -Expiration: ${nextWindowFormatted.expirationFormatted} (${nextWindowFormatted.expirationTimestamp}) -Renewal Time: ${nextWindowFormatted.renewalFormatted}`; + const text = $t('copyTemplates.schedule', { + currentInception: currentWindowFormatted.inceptionFormatted, + currentInceptionTimestamp: currentWindowFormatted.inceptionTimestamp, + currentExpiration: currentWindowFormatted.expirationFormatted, + currentExpirationTimestamp: currentWindowFormatted.expirationTimestamp, + currentRenewal: currentWindowFormatted.renewalFormatted, + nextInception: nextWindowFormatted.inceptionFormatted, + nextInceptionTimestamp: nextWindowFormatted.inceptionTimestamp, + nextExpiration: nextWindowFormatted.expirationFormatted, + nextExpirationTimestamp: nextWindowFormatted.expirationTimestamp, + nextRenewal: nextWindowFormatted.renewalFormatted, + }); clipboard.copy(text, 'both'); } @@ -76,10 +87,9 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="card"> <header class="card-header"> - <h1>RRSIG Planner</h1> + <h1>{$t('title')}</h1> <p> - Suggest RRSIG validity windows (inception/expiration) based on TTLs and desired overlap, with renewal lead-time - guidance for automated DNSSEC signature management. + {$t('description')} </p> </header> @@ -89,7 +99,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="form-group"> <label for="ttl"> <Icon name="clock" size="sm" /> - TTL (seconds) + {$t('form.ttl.label')} </label> <input id="ttl" @@ -100,14 +110,14 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; class="number-input {!isValidTTL ? 'invalid' : ''}" /> {#if !isValidTTL} - <p class="field-error">TTL must be between 1 and 86400 seconds</p> + <p class="field-error">{$t('form.ttl.error')}</p> {/if} </div> <div class="form-group"> <label for="overlap"> <Icon name="overlap" size="sm" /> - Desired Overlap (hours) + {$t('form.overlap.label')} </label> <input id="overlap" @@ -118,14 +128,14 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; class="number-input {!isValidOverlap ? 'invalid' : ''}" /> {#if !isValidOverlap} - <p class="field-error">Overlap must be between 1 and 168 hours</p> + <p class="field-error">{$t('form.overlap.error')}</p> {/if} </div> <div class="form-group"> <label for="lead-time"> <Icon name="timer" size="sm" /> - Renewal Lead Time (hours) + {$t('form.leadTime.label')} </label> <input id="lead-time" @@ -136,14 +146,14 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; class="number-input {!isValidLeadTime ? 'invalid' : ''}" /> {#if !isValidLeadTime} - <p class="field-error">Lead time must be between 1 and 168 hours</p> + <p class="field-error">{$t('form.leadTime.error')}</p> {/if} </div> <div class="form-group"> <label for="clock-skew"> <Icon name="clock" size="sm" /> - Clock Skew (hours) + {$t('form.clockSkew.label')} </label> <input id="clock-skew" @@ -155,14 +165,14 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; class="number-input {!isValidClockSkew ? 'invalid' : ''}" /> {#if !isValidClockSkew} - <p class="field-error">Clock skew must be between 0 and 24 hours</p> + <p class="field-error">{$t('form.clockSkew.error')}</p> {/if} </div> <div class="form-group"> <label for="validity-days"> <Icon name="calendar" size="sm" /> - Signature Validity (days) + {$t('form.validityDays.label')} </label> <input id="validity-days" @@ -173,7 +183,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; class="number-input {!isValidityDays ? 'invalid' : ''}" /> {#if !isValidityDays} - <p class="field-error">Validity must be between 1 and 365 days</p> + <p class="field-error">{$t('form.validityDays.error')}</p> {/if} </div> </div> @@ -185,7 +195,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="warning-content"> <Icon name="alert-triangle" size="sm" /> <div> - <strong>Timing Warnings:</strong> + <strong>{$t('warnings.title')}</strong> <ul class="warning-list"> {#each currentValidation.warnings as warning, index (index)} <li>{warning}</li> @@ -202,10 +212,10 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <!-- Current Window --> <div class="card window-card"> <div class="window-header"> - <h3>Current Signature Window</h3> + <h3>{$t('windows.current.title')}</h3> <button class="copy-button {clipboard.isCopied('current') ? 'copied' : ''}" onclick={copyCurrentWindow}> <Icon name={clipboard.isCopied('current') ? 'check' : 'copy'} size="sm" /> - Copy + {$t('windows.current.copy')} </button> </div> @@ -214,7 +224,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item inception"> <div class="timing-header"> <Icon name="play" size="sm" /> - <span class="timing-label">Inception (Start Time)</span> + <span class="timing-label">{$t('windows.timing.inception')}</span> </div> <div class="timing-value mono">{currentWindowFormatted.inceptionFormatted}</div> <div class="timing-readable">{currentWindowFormatted.inceptionTimestamp}</div> @@ -223,7 +233,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item expiration"> <div class="timing-header"> <Icon name="stop" size="sm" /> - <span class="timing-label">Expiration (End Time)</span> + <span class="timing-label">{$t('windows.timing.expiration')}</span> </div> <div class="timing-value mono">{currentWindowFormatted.expirationFormatted}</div> <div class="timing-readable">{currentWindowFormatted.expirationTimestamp}</div> @@ -232,23 +242,23 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item renewal"> <div class="timing-header"> <Icon name="refresh" size="sm" /> - <span class="timing-label">Renewal Time</span> + <span class="timing-label">{$t('windows.timing.renewal')}</span> </div> <div class="timing-value mono">{currentWindowFormatted.renewalFormatted}</div> - <div class="timing-note">Generate next signatures before this time</div> + <div class="timing-note">{$t('windows.timing.renewalNote')}</div> </div> <div class="metrics-grid"> <div class="metric-item"> - <span class="metric-label">Validity Period</span> + <span class="metric-label">{$t('windows.metrics.validityPeriod')}</span> <span class="metric-value">{formatDuration(currentWindow.validity)}</span> </div> <div class="metric-item"> - <span class="metric-label">Lead Time</span> + <span class="metric-label">{$t('windows.metrics.leadTime')}</span> <span class="metric-value">{formatDuration(currentWindow.leadTime)}</span> </div> <div class="metric-item"> - <span class="metric-label">Overlap Period</span> + <span class="metric-label">{$t('windows.metrics.overlapPeriod')}</span> <span class="metric-value">{formatDuration(desiredOverlap)}</span> </div> </div> @@ -259,7 +269,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <!-- Next Window --> <div class="card window-card"> <div class="window-header"> - <h3>Next Signature Window</h3> + <h3>{$t('windows.next.title')}</h3> </div> {#if nextWindowFormatted} @@ -267,7 +277,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item inception"> <div class="timing-header"> <Icon name="play" size="sm" /> - <span class="timing-label">Next Inception</span> + <span class="timing-label">{$t('windows.timing.nextInception')}</span> </div> <div class="timing-value mono">{nextWindowFormatted.inceptionFormatted}</div> <div class="timing-readable">{nextWindowFormatted.inceptionTimestamp}</div> @@ -276,7 +286,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item expiration"> <div class="timing-header"> <Icon name="stop" size="sm" /> - <span class="timing-label">Next Expiration</span> + <span class="timing-label">{$t('windows.timing.nextExpiration')}</span> </div> <div class="timing-value mono">{nextWindowFormatted.expirationFormatted}</div> <div class="timing-readable">{nextWindowFormatted.expirationTimestamp}</div> @@ -285,7 +295,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="timing-item renewal"> <div class="timing-header"> <Icon name="refresh" size="sm" /> - <span class="timing-label">Following Renewal</span> + <span class="timing-label">{$t('windows.timing.followingRenewal')}</span> </div> <div class="timing-value mono">{nextWindowFormatted.renewalFormatted}</div> </div> @@ -293,7 +303,7 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="copy-schedule-section"> <button class="copy-button {clipboard.isCopied('both') ? 'copied' : ''}" onclick={copyBothWindows}> <Icon name={clipboard.isCopied('both') ? 'check' : 'copy'} size="sm" /> - Copy Full Schedule + {$t('windows.next.copySchedule')} </button> </div> </div> @@ -305,25 +315,25 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <!-- Implementation Guidelines --> <div class="card guidelines-card"> <div class="card-section-header"> - <h3>Implementation Guidelines</h3> + <h3>{$t('guidelines.title')}</h3> </div> <div class="guidelines-content"> <div class="guideline-section"> - <h4>Automation Schedule:</h4> + <h4>{$t('guidelines.automation.title')}</h4> <ul class="guideline-list"> - <li>Monitor renewal times continuously</li> - <li>Generate new signatures {formatDuration(renewalLeadTime)} before expiration</li> - <li>Maintain {formatDuration(desiredOverlap)} overlap period</li> - <li>Account for {clockSkew}h clock skew tolerance</li> + <li>{$t('guidelines.automation.monitor')}</li> + <li>{$t('guidelines.automation.generate', { leadTime: formatDuration(renewalLeadTime) })}</li> + <li>{$t('guidelines.automation.maintain', { overlap: formatDuration(desiredOverlap) })}</li> + <li>{$t('guidelines.automation.account', { clockSkew: clockSkew })}</li> </ul> </div> <div class="guideline-section"> - <h4>Best Practices:</h4> + <h4>{$t('guidelines.bestPractices.title')}</h4> <ul class="guideline-list"> - <li>Test signature generation before deployment</li> - <li>Monitor DNSSEC validation after updates</li> - <li>Keep backup signatures for rollback</li> - <li>Log all signature generation events</li> + <li>{$t('guidelines.bestPractices.test')}</li> + <li>{$t('guidelines.bestPractices.monitor')}</li> + <li>{$t('guidelines.bestPractices.backup')}</li> + <li>{$t('guidelines.bestPractices.log')}</li> </ul> </div> </div> @@ -333,34 +343,30 @@ Renewal Time: ${nextWindowFormatted.renewalFormatted}`; <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>RRSIG Timing</h4> + <h4>{$t('education.timing.title')}</h4> <p> - RRSIG records have inception and expiration timestamps that define when the signature is valid. Proper timing - ensures continuous DNSSEC validation during key transitions. + {$t('education.timing.content')} </p> </div> <div class="education-item info-panel"> - <h4>Overlap Strategy</h4> + <h4>{$t('education.overlap.title')}</h4> <p> - Overlapping signature validity periods prevent validation failures during rollover. New signatures should be - generated before old ones expire. + {$t('education.overlap.content')} </p> </div> <div class="education-item info-panel"> - <h4>Clock Skew Tolerance</h4> + <h4>{$t('education.clockSkew.title')}</h4> <p> - Account for time differences between authoritative servers and validators. Start signatures slightly in the - past to accommodate clock skew. + {$t('education.clockSkew.content')} </p> </div> <div class="education-item info-panel"> - <h4>Automation Benefits</h4> + <h4>{$t('education.automation.title')}</h4> <p> - Automated RRSIG generation reduces manual errors and ensures consistent timing. Plan renewal schedules based - on TTL values and operational requirements. + {$t('education.automation.content')} </p> </div> </div> diff --git a/src/lib/components/tools/RandomIP.svelte b/src/lib/components/tools/RandomIP.svelte index 93b7de25..e049988f 100644 --- a/src/lib/components/tools/RandomIP.svelte +++ b/src/lib/components/tools/RandomIP.svelte @@ -4,6 +4,7 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import '../../../styles/diagnostics-pages.scss'; + import { t } from '$lib/stores/language'; let inputText = $state('192.168.1.0/24 x 10\n10.0.0.0-10.0.0.255 5\n172.16.0.0/16 * 3\n2001:db8::/64[15]'); let defaultCount = $state(5); @@ -15,32 +16,32 @@ let _userModified = $state(false); const clipboard = useClipboard(); - const examples = [ + const examples = $derived([ { - input: '192.168.1.0/24 x 5', - description: 'Generate 5 random IPs from a /24 subnet', + input: $t('tools/random-ip.examples.items.basicCidr.input'), + description: $t('tools/random-ip.examples.items.basicCidr.description'), }, { - input: '10.0.0.0-10.0.0.255 * 3\n172.16.0.0/16 [8]', - description: 'Multiple formats: range and CIDR with different counts', + input: $t('tools/random-ip.examples.items.multipleFormats.input'), + description: $t('tools/random-ip.examples.items.multipleFormats.description'), }, { - input: '2001:db8::/64 # 10\nfe80::/10 x 5', - description: 'IPv6 networks with various syntax formats', + input: $t('tools/random-ip.examples.items.ipv6.input'), + description: $t('tools/random-ip.examples.items.ipv6.description'), }, { - input: '192.168.0.0/16 100\n203.0.113.0/24 * 20', - description: 'Large generation counts from different networks', + input: $t('tools/random-ip.examples.items.largeCounts.input'), + description: $t('tools/random-ip.examples.items.largeCounts.description'), }, { - input: '127.0.0.0/8\n::1/128 x 1\n169.254.0.0/16 [10]', - description: 'Special-use addresses: loopback and link-local', + input: $t('tools/random-ip.examples.items.specialUse.input'), + description: $t('tools/random-ip.examples.items.specialUse.description'), }, { - input: '198.51.100.0/24 * 15\n198.18.0.0/15 [25]', - description: 'Test networks for documentation and benchmarking', + input: $t('tools/random-ip.examples.items.testNetworks.input'), + description: $t('tools/random-ip.examples.items.testNetworks.description'), }, - ]; + ]); function generateIPs() { if (!inputText.trim()) { @@ -144,15 +145,15 @@ <div class="tool-container"> <div class="tool-header"> - <h1>Random IP Generator</h1> - <p>Generate random IP addresses from networks and ranges with uniqueness control and seeded randomness</p> + <h1>{$t('tools/random-ip.title')}</h1> + <p>{$t('tools/random-ip.subtitle')}</p> </div> <div class="card examples-card"> <details class="examples-details"> - <summary class="examples-summary" use:tooltip={'Click to see example inputs'}> + <summary class="examples-summary" use:tooltip={$t('tools/random-ip.examples.titleTooltip')}> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/random-ip.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, index (`${example.input}-${index}`)} @@ -171,31 +172,31 @@ </div> <div class="card"> - <h3>Network Configuration</h3> + <h3>{$t('tools/random-ip.networkConfig.title')}</h3> <div class="form-row"> <div class="textarea-group"> <div class="form-group"> - <label for="inputs" use:tooltip={'Enter networks with generation counts using various formats'} - >Networks and Counts</label + <label for="inputs" use:tooltip={$t('tools/random-ip.networkConfig.networksTooltip')} + >{$t('tools/random-ip.networkConfig.networksLabel')}</label > <textarea id="inputs" bind:value={inputText} oninput={handleInputChange} - placeholder="192.168.1.0/24 x 10 10.0.0.0-10.0.0.255 5 172.16.0.0/16 * 3 2001:db8::/64[15]" + placeholder={$t('tools/random-ip.networkConfig.networksPlaceholder')} rows="6" - use:tooltip={'Specify networks and generation counts'} + use:tooltip={$t('tools/random-ip.networkConfig.networksSpecifyTooltip')} ></textarea> <div class="input-help"> - Formats: network x count, network * count, network count, network#count, network[count] + {$t('tools/random-ip.networkConfig.helpText')} </div> </div> </div> <div class="options-group"> <div class="option-card"> - <label for="default-count" use:tooltip={'Number of IPs to generate when count is not specified'} - >Default Count</label + <label for="default-count" use:tooltip={$t('tools/random-ip.networkConfig.defaultCount.tooltip')} + >{$t('tools/random-ip.networkConfig.defaultCount.label')}</label > <input id="default-count" @@ -203,30 +204,36 @@ bind:value={defaultCount} min="1" max="1000" - placeholder="5" - use:tooltip={'Default generation count'} + placeholder={$t('tools/random-ip.networkConfig.defaultCount.placeholder')} + use:tooltip={$t('tools/random-ip.networkConfig.defaultCount.valueTooltip')} /> </div> <div class="checkbox-group"> - <label class="checkbox-label" use:tooltip={'Ensure all generated IPs are unique within each network'}> + <label class="checkbox-label" use:tooltip={$t('tools/random-ip.networkConfig.unique.tooltip')}> <input type="checkbox" bind:checked={unique} /> <span class="checkmark"></span> - Unique IPs Only + {$t('tools/random-ip.networkConfig.unique.label')} </label> </div> <div class="option-card"> - <label for="seed" use:tooltip={'Use the same seed for reproducible random results'}>Random Seed</label> + <label for="seed" use:tooltip={$t('tools/random-ip.networkConfig.seed.tooltip')} + >{$t('tools/random-ip.networkConfig.seed.label')}</label + > <div class="seed-input"> <input id="seed" type="text" bind:value={seed} - placeholder="Optional seed for reproducible results" - use:tooltip={'Enter seed for reproducible randomness'} + placeholder={$t('tools/random-ip.networkConfig.seed.placeholder')} + use:tooltip={$t('tools/random-ip.networkConfig.seed.valueTooltip')} /> - <button onclick={generateNewSeed} type="button" use:tooltip={'Generate new random seed'}> + <button + onclick={generateNewSeed} + type="button" + use:tooltip={$t('tools/random-ip.networkConfig.seed.generateTooltip')} + > <Icon name="refresh" size="sm" /> </button> </div> @@ -238,7 +245,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Generating random IPs... + {$t('tools/random-ip.loading')} </div> {/if} @@ -247,7 +254,7 @@ {#if result.errors.length > 0} <div class="card error-card"> <div class="card-header row"> - <h3><Icon name="alert-triangle" size="sm" /> Errors</h3> + <h3><Icon name="alert-triangle" size="sm" /> {$t('tools/random-ip.errors.title')}</h3> </div> <div class="card-content"> {#each result.errors as error, index (index)} @@ -263,7 +270,7 @@ {#if result.generations.length > 0} <div class="card summary-card"> <div class="card-header row"> - <h3>Generation Summary</h3> + <h3>{$t('tools/random-ip.summary.title')}</h3> <button class="copy-btn" class:copied={clipboard.isCopied('summary')} @@ -274,32 +281,42 @@ `Total Networks: ${result.summary.totalNetworks}\nValid: ${result.summary.validNetworks}\nInvalid: ${result.summary.invalidNetworks}\nTotal IPs: ${result.summary.totalIPsGenerated}\nUnique IPs: ${result.summary.uniqueIPsGenerated}`, 'summary', )} - use:tooltip={'Copy summary to clipboard'} + use:tooltip={$t('tools/random-ip.summary.copyTooltip')} > <Icon name={clipboard.isCopied('summary') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('summary') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('summary') ? $t('tools/random-ip.common.copied') : $t('tools/random-ip.common.copy')} </button> </div> <div class="card-content"> <div class="summary-stats"> <div class="info-card"> - <div class="info-label" use:tooltip={'Total number of networks processed'}>Total Networks</div> + <div class="info-label" use:tooltip={$t('tools/random-ip.summary.totalNetworks.tooltip')}> + {$t('tools/random-ip.summary.totalNetworks.label')} + </div> <div class="metric-value">{result.summary.totalNetworks}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Networks that were processed successfully'}>Valid</div> + <div class="info-label" use:tooltip={$t('tools/random-ip.summary.valid.tooltip')}> + {$t('tools/random-ip.summary.valid.label')} + </div> <div class="metric-value success">{result.summary.validNetworks}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Networks that had processing errors'}>Invalid</div> + <div class="info-label" use:tooltip={$t('tools/random-ip.summary.invalid.tooltip')}> + {$t('tools/random-ip.summary.invalid.label')} + </div> <div class="metric-value error">{result.summary.invalidNetworks}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Total IP addresses generated across all networks'}>Total IPs</div> + <div class="info-label" use:tooltip={$t('tools/random-ip.summary.totalIps.tooltip')}> + {$t('tools/random-ip.summary.totalIps.label')} + </div> <div class="metric-value info">{result.summary.totalIPsGenerated}</div> </div> <div class="info-card"> - <div class="info-label" use:tooltip={'Number of unique IP addresses generated'}>Unique IPs</div> + <div class="info-label" use:tooltip={$t('tools/random-ip.summary.uniqueIps.tooltip')}> + {$t('tools/random-ip.summary.uniqueIps.label')} + </div> <div class="metric-value">{result.summary.uniqueIPsGenerated}</div> </div> </div> @@ -308,28 +325,36 @@ <div class="card all-ips-card"> <div class="card-header row"> - <h3>All Generated IPs ({result.allGeneratedIPs.length})</h3> + <h3> + {$t('tools/random-ip.allIps.title')} + {$t('tools/random-ip.allIps.count', { count: result.allGeneratedIPs.length })} + </h3> <div class="export-buttons"> <button class="copy-btn" class:copied={clipboard.isCopied('all-ips')} onclick={copyAllIPs} - use:tooltip={'Copy all generated IPs to clipboard'} + use:tooltip={$t('tools/random-ip.allIps.copyAllTooltip')} > <Icon name={clipboard.isCopied('all-ips') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('all-ips') ? 'Copied!' : 'Copy All'} + {clipboard.isCopied('all-ips') + ? $t('tools/random-ip.common.copied') + : $t('tools/random-ip.common.copyAll')} </button> - <button onclick={() => exportResults('txt')} use:tooltip={'Export as plain text file'}> + <button onclick={() => exportResults('txt')} use:tooltip={$t('tools/random-ip.allIps.exportTxtTooltip')}> <Icon name="download" size="xs" /> - TXT + {$t('tools/random-ip.common.txt')} </button> - <button onclick={() => exportResults('csv')} use:tooltip={'Export as CSV file'}> + <button onclick={() => exportResults('csv')} use:tooltip={$t('tools/random-ip.allIps.exportCsvTooltip')}> <Icon name="csv-file" size="xs" /> - CSV + {$t('tools/random-ip.common.csv')} </button> - <button onclick={() => exportResults('json')} use:tooltip={'Export as JSON file'}> + <button + onclick={() => exportResults('json')} + use:tooltip={$t('tools/random-ip.allIps.exportJsonTooltip')} + > <Icon name="json-file" size="xs" /> - JSON + {$t('tools/random-ip.common.json')} </button> </div> </div> @@ -342,7 +367,7 @@ class="ip-tag" class:copied={clipboard.isCopied(`ip-${index}`)} onclick={() => clipboard.copy(ip, `ip-${index}`)} - use:tooltip={'Click to copy IP address'} + use:tooltip={$t('tools/random-ip.allIps.ipCopyTooltip')} > {ip} <Icon name={clipboard.isCopied(`ip-${index}`) ? 'check' : 'copy'} size="xs" /> @@ -354,7 +379,7 @@ </div> <div class="generations"> - <h3>Network Generations</h3> + <h3>{$t('tools/random-ip.generations.title')}</h3> <div class="generations-list"> {#each result.generations as generation, index (index)} @@ -382,28 +407,32 @@ <div class="generation-info"> <div class="info-grid"> <div class="info-item"> - <span class="info-label">Requested:</span> + <span class="info-label">{$t('tools/random-ip.generations.requested')}</span> <span class="info-value">{generation.requestedCount}</span> </div> <div class="info-item"> - <span class="info-label">Generated:</span> + <span class="info-label">{$t('tools/random-ip.generations.generated')}</span> <span class="info-value">{generation.generatedIPs.length}</span> </div> <div class="info-item"> - <span class="info-label">Unique:</span> - <span class="info-value">{generation.uniqueIPs ? 'Yes' : 'No'}</span> + <span class="info-label">{$t('tools/random-ip.generations.unique')}</span> + <span class="info-value" + >{generation.uniqueIPs + ? $t('tools/random-ip.generations.uniqueYes') + : $t('tools/random-ip.generations.uniqueNo')}</span + > </div> {#if generation.seed} <div class="info-item"> - <span class="info-label">Seed:</span> + <span class="info-label">{$t('tools/random-ip.generations.seed')}</span> <button type="button" class="code-button info-code" onclick={() => clipboard.copy(generation.seed!, 'seed')} - title="Click to copy" + title={$t('tools/random-ip.generations.seedCopyTooltip')} > {generation.seed} </button> @@ -414,34 +443,34 @@ {#if generation.networkDetails} <div class="network-details"> - <h4>Network Range</h4> + <h4>{$t('tools/random-ip.generations.networkRange.title')}</h4> <div class="range-info"> <div class="range-item"> - <span class="range-label">Start:</span> + <span class="range-label">{$t('tools/random-ip.generations.networkRange.start')}</span> <button type="button" class="code-button range-code" onclick={() => clipboard.copy(generation.networkDetails!.start, 'start')} - title="Click to copy" + title={$t('tools/random-ip.generations.networkRange.startCopyTooltip')} > {generation.networkDetails.start} </button> </div> <div class="range-item"> - <span class="range-label">End:</span> + <span class="range-label">{$t('tools/random-ip.generations.networkRange.end')}</span> <button type="button" class="code-button range-code" onclick={() => clipboard.copy(generation.networkDetails!.end, 'end')} - title="Click to copy" + title={$t('tools/random-ip.generations.networkRange.endCopyTooltip')} > {generation.networkDetails.end} </button> </div> <div class="range-item"> - <span class="range-label">Total:</span> + <span class="range-label">{$t('tools/random-ip.generations.networkRange.total')}</span> <span class="range-value">{generation.networkDetails.totalAddresses}</span> </div> </div> @@ -451,7 +480,12 @@ {#if generation.generatedIPs.length > 0} <div class="generated-ips"> <div class="details-header"> - <h4>Generated IPs ({generation.generatedIPs.length})</h4> + <h4> + {$t('tools/random-ip.generations.generatedIps.title')} + {$t('tools/random-ip.generations.generatedIps.count', { + count: generation.generatedIPs.length, + })} + </h4> </div> <div class="ips-list"> {#each generation.generatedIPs as ip, ipIndex (`${ip}-${ipIndex}`)} @@ -460,7 +494,7 @@ class="ip-tag" class:copied={clipboard.isCopied(`gen-${index}-ip-${ipIndex}`)} onclick={() => clipboard.copy(ip, `gen-${index}-ip-${ipIndex}`)} - use:tooltip={'Click to copy IP address'} + use:tooltip={$t('tools/random-ip.allIps.ipCopyTooltip')} > {ip} <Icon diff --git a/src/lib/components/tools/ReversePTRGenerator.svelte b/src/lib/components/tools/ReversePTRGenerator.svelte index fd1a1393..d3fb18d7 100644 --- a/src/lib/components/tools/ReversePTRGenerator.svelte +++ b/src/lib/components/tools/ReversePTRGenerator.svelte @@ -3,6 +3,13 @@ import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; import { generatePTRName, generateCIDRPTRs, type PTRRecord } from '$lib/utils/reverse-dns.js'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools/ptr-generator'); + }); let inputValue = $state('192.168.1.100'); let inputType = $state<'single' | 'cidr'>('single'); @@ -23,34 +30,34 @@ const examples = [ { - label: 'Single IPv4', + label: $t('examples.singleIPv4.label'), input: '192.168.1.100', type: 'single' as const, - description: 'Generate PTR for single IPv4 address', + description: $t('examples.singleIPv4.description'), }, { - label: 'Single IPv6', + label: $t('examples.singleIPv6.label'), input: '2001:db8::1', type: 'single' as const, - description: 'Generate PTR for single IPv6 address', + description: $t('examples.singleIPv6.description'), }, { - label: 'IPv4 /28 Block', + label: $t('examples.ipv4SmallBlock.label'), input: '192.168.1.16/28', type: 'cidr' as const, - description: 'Generate PTRs for /28 subnet (16 addresses)', + description: $t('examples.ipv4SmallBlock.description'), }, { - label: 'IPv4 /24 Network', + label: $t('examples.ipv4Subnet24.label'), input: '10.0.0.0/24', type: 'cidr' as const, - description: 'Generate PTRs for entire /24 network', + description: $t('examples.ipv4Subnet24.description'), }, { - label: 'IPv6 /64 Network', + label: $t('examples.ipv6Network.label'), input: '2001:db8:1000::/64', type: 'cidr' as const, - description: 'Generate IPv6 PTR examples for /64', + description: $t('examples.ipv6Network.description'), }, ]; @@ -78,7 +85,7 @@ if (record) { entries.push(record); } else { - throw new Error('Invalid IP address format'); + throw new Error($t('errors.invalidInput')); } } else { // CIDR notation @@ -101,7 +108,7 @@ } catch (error) { results = { success: false, - error: error instanceof Error ? error.message : 'Unknown error occurred', + error: error instanceof Error ? error.message : $t('errors.processingError'), entries: [], summary: { totalEntries: 0, ipv4Entries: 0, ipv6Entries: 0, uniqueZones: 0 }, }; @@ -126,8 +133,8 @@ <div class="card"> <header class="card-header"> - <h1>Reverse PTR Generator</h1> - <p>Convert IP addresses and CIDR blocks to PTR record names and zone file examples</p> + <h1>{$t('title')}</h1> + <p>{$t('description')}</p> </header> <!-- Educational Overview Card --> @@ -136,8 +143,8 @@ <div class="overview-item"> <Icon name="rotate" size="sm" /> <div> - <strong>Reverse DNS:</strong> PTR records map IP addresses back to hostnames using <code>in-addr.arpa</code> - (IPv4) and <code>ip6.arpa</code> (IPv6) zones. + <strong>{$t('overview.reverseDNS.title')}:</strong> + {$t('overview.reverseDNS.content')} </div> </div> <div class="overview-item"> @@ -149,7 +156,8 @@ <div class="overview-item"> <Icon name="file" size="sm" /> <div> - <strong>Zone Lines:</strong> Ready-to-use DNS zone file entries with proper PTR record format. + <strong>{$t('overview.zoneFiles.title')}:</strong> + {$t('overview.zoneFiles.content')} </div> </div> </div> @@ -160,7 +168,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -171,7 +179,7 @@ <div class="example-header"> <div class="example-label">{example.label}</div> <div class="example-type {example.type}"> - {example.type === 'single' ? 'Single IP' : 'CIDR Block'} + {example.type === 'single' ? $t('examples.types.singleIP') : $t('examples.types.cidrBlock')} </div> </div> <code class="example-input">{example.input}</code> @@ -186,20 +194,20 @@ <div class="card input-card"> <!-- Input Type Selection --> <div class="type-section"> - <h3 class="type-label">Input Type</h3> + <h3 class="type-label">{$t('input.type.label')}</h3> <div class="type-options"> <label class="type-option"> <input type="radio" bind:group={inputType} value="single" onchange={handleTypeChange} /> <div class="type-content"> <Icon name="target" size="sm" /> - <span>Single IP</span> + <span>{$t('input.type.singleIP')}</span> </div> </label> <label class="type-option"> <input type="radio" bind:group={inputType} value="cidr" onchange={handleTypeChange} /> <div class="type-content"> <Icon name="network" size="sm" /> - <span>CIDR Block</span> + <span>{$t('input.type.cidrBlock')}</span> </div> </label> </div> @@ -214,7 +222,7 @@ : 'Enter an IPv4 or IPv6 CIDR block (e.g., 192.168.1.0/24)'} > <Icon name={inputType === 'single' ? 'target' : 'network'} size="sm" /> - {inputType === 'single' ? 'IP Address' : 'CIDR Block'} + {inputType === 'single' ? $t('input.address.labelSingle') : $t('input.type.cidrBlock')} </label> <input id="ip-input" @@ -233,16 +241,16 @@ <div class="card results-card"> {#if results.success} <div class="results-header"> - <h3>PTR Records Generated</h3> + <h3>{$t('results.title')}</h3> <div class="summary-stats"> <div class="stat-item"> <span class="stat-value">{results.summary.totalEntries}</span> - <span class="stat-label">Total PTRs</span> + <span class="stat-label">{$t('results.summary.totalPTRs')}</span> </div> {#if results.summary.ipv4Entries > 0} <div class="stat-item"> <span class="stat-value">{results.summary.ipv4Entries}</span> - <span class="stat-label">IPv4</span> + <span class="stat-label">{$t('results.summary.ipv4')}</span> </div> {/if} {#if results.summary.ipv6Entries > 0} @@ -253,7 +261,7 @@ {/if} <div class="stat-item"> <span class="stat-value">{results.summary.uniqueZones}</span> - <span class="stat-label">Zones</span> + <span class="stat-label">{$t('results.summary.zones')}</span> </div> </div> </div> @@ -262,14 +270,14 @@ <div class="ptr-records"> <h4> <Icon name="list" size="sm" /> - PTR Records & Zone Lines + {$t('results.entries.title')} </h4> <div class="records-table"> <div class="table-header"> - <div class="col-ip">IP Address</div> - <div class="col-ptr">PTR Record Name</div> + <div class="col-ip">{$t('results.entries.ipAddress')}</div> + <div class="col-ptr">{$t('results.entries.ptrRecord')}</div> <div class="col-zone-line">Zone File Line</div> - <div class="col-type">Type</div> + <div class="col-type">{$t('results.entries.type')}</div> </div> {#each results.entries.slice(0, 100) as entry (`${entry.ip}-${entry.ptrName}`)} <div class="table-row"> @@ -391,14 +399,6 @@ gap: var(--spacing-sm); color: var(--text-secondary); - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } - strong { color: var(--text-primary); } diff --git a/src/lib/components/tools/ReverseZoneGenerator.svelte b/src/lib/components/tools/ReverseZoneGenerator.svelte index 8e7a9c1f..dceb1bc8 100644 --- a/src/lib/components/tools/ReverseZoneGenerator.svelte +++ b/src/lib/components/tools/ReverseZoneGenerator.svelte @@ -1,4 +1,7 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + import { locale, loadTranslations, t } from '$lib/stores/language.js'; import { tooltip } from '$lib/actions/tooltip.js'; import Icon from '$lib/components/global/Icon.svelte'; import { useClipboard } from '$lib/composables'; @@ -10,6 +13,10 @@ type ZoneFileOptions, } from '$lib/utils/reverse-dns'; + onMount(async () => { + await loadTranslations(get(locale), 'tools/reverse-zone-generator'); + }); + let cidrInput = $state('192.168.1.0/24'); let hostnameTemplate = $state('host-{ip-dashes}.example.com.'); let nameServers = $state('ns1.example.com.\nns2.example.com.'); @@ -35,38 +42,38 @@ let selectedExample = $state<string | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'IPv4 /24 Network', + label: $t('examples.items.0.label'), cidr: '192.168.1.0/24', template: 'host-{ip-dashes}.example.com.', - description: 'Generate zone for full /24 subnet', + description: $t('examples.items.0.description'), }, { - label: 'IPv4 /28 Block', + label: $t('examples.items.1.label'), cidr: '10.0.0.16/28', template: 'server{ip}.lan.example.com.', - description: 'Small block with custom naming', + description: $t('examples.items.1.description'), }, { - label: 'IPv6 /64 Network', + label: $t('examples.items.2.label'), cidr: '2001:db8:1000::/64', template: 'host-{ip-dashes}.ipv6.example.com.', - description: 'IPv6 reverse zone generation', + description: $t('examples.items.2.description'), }, { - label: 'Corporate Network', + label: $t('examples.items.3.label'), cidr: '172.16.100.0/24', template: 'workstation-{ip-dashes}.corp.example.com.', - description: 'Corporate naming convention', + description: $t('examples.items.3.description'), }, - ]; + ]); - const templateHelp = [ - { placeholder: '{ip}', description: 'Original IP address (192.168.1.100)' }, - { placeholder: '{ip-dashes}', description: 'IP with dashes (192-168-1-100)' }, - { placeholder: '{domain}', description: 'Base domain from settings' }, - ]; + const templateHelp = $derived([ + { placeholder: '{ip}', description: $t('templateHelp.placeholders.0.description') }, + { placeholder: '{ip-dashes}', description: $t('templateHelp.placeholders.1.description') }, + { placeholder: '{domain}', description: $t('templateHelp.placeholders.2.description') }, + ]); function loadExample(example: (typeof examples)[0]) { cidrInput = example.cidr; @@ -155,8 +162,8 @@ <div class="card"> <header class="card-header"> - <h1>Reverse Zone Generator</h1> - <p>Generate complete reverse DNS zone files from CIDR blocks with customizable hostname templates</p> + <h1>{$t('title')}</h1> + <p>{$t('description')}</p> </header> <!-- Educational Overview Card --> @@ -165,21 +172,22 @@ <div class="overview-item"> <Icon name="file" size="sm" /> <div> - <strong>Full Zone Files:</strong> Complete DNS zone files with SOA, NS, and PTR records ready for deployment. + <strong>{$t('overview.fullZoneFiles.title')}</strong> + {$t('overview.fullZoneFiles.description')} </div> </div> <div class="overview-item"> <Icon name="template" size="sm" /> <div> - <strong>Hostname Templates:</strong> Customize hostname patterns using placeholders like - <code>{'{'}ip}</code> - and <code>{'{'}ip-dashes}</code>. + <strong>{$t('overview.hostnameTemplates.title')}</strong> + {$t('overview.hostnameTemplates.description')} </div> </div> <div class="overview-item"> <Icon name="settings" size="sm" /> <div> - <strong>Zone Configuration:</strong> Configure name servers, contact email, TTL values, and domain settings. + <strong>{$t('overview.zoneConfiguration.title')}</strong> + {$t('overview.zoneConfiguration.description')} </div> </div> </div> @@ -190,7 +198,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -214,16 +222,16 @@ <div class="card input-card"> <!-- CIDR Input --> <div class="input-group"> - <label for="cidr-input" use:tooltip={'Enter a CIDR block to generate reverse zones for'}> + <label for="cidr-input" use:tooltip={$t('form.cidrBlock.tooltip')}> <Icon name="network" size="sm" /> - CIDR Block + {$t('form.cidrBlock.label')} </label> <input id="cidr-input" type="text" bind:value={cidrInput} oninput={handleInputChange} - placeholder="192.168.1.0/24 or 2001:db8::/64" + placeholder={$t('form.cidrBlock.placeholder')} class="cidr-input {results?.success === true ? 'valid' : results?.success === false ? 'invalid' : ''}" spellcheck="false" /> @@ -231,26 +239,23 @@ <!-- Hostname Template --> <div class="input-group"> - <label - for="template-input" - use:tooltip={"Use placeholders like ${'{ip}'}, ${'{ip-dashes}'} to customize hostnames"} - > + <label for="template-input" use:tooltip={$t('form.hostnameTemplate.tooltip')}> <Icon name="tag" size="sm" /> - Hostname Template + {$t('form.hostnameTemplate.label')} </label> <input id="template-input" type="text" bind:value={hostnameTemplate} oninput={handleInputChange} - placeholder="host-[ip-dashes].example.com." + placeholder={$t('form.hostnameTemplate.placeholder')} class="template-input" spellcheck="false" /> <!-- Template Help --> <div class="template-help"> - <h4>Available Placeholders:</h4> + <h4>{$t('templateHelp.title')}</h4> <div class="placeholder-grid"> {#each templateHelp as item (item.placeholder)} <div class="placeholder-item"> @@ -264,19 +269,19 @@ <!-- Zone Configuration --> <div class="config-section"> - <h3>Zone Configuration</h3> + <h3>{$t('form.zoneConfiguration')}</h3> <div class="config-grid"> <div class="config-group"> - <label for="nameservers-input" use:tooltip={'One name server per line, automatically adds trailing dots'}> + <label for="nameservers-input" use:tooltip={$t('form.nameServers.tooltip')}> <Icon name="server" size="sm" /> - Name Servers + {$t('form.nameServers.label')} </label> <textarea id="nameservers-input" bind:value={nameServers} oninput={handleInputChange} - placeholder="ns1.example.com ns2.example.com" + placeholder={$t('form.nameServers.placeholder')} class="nameservers-input" rows="3" spellcheck="false" @@ -284,32 +289,32 @@ </div> <div class="config-group"> - <label for="contact-input" use:tooltip={'DNS zone contact email address'}> + <label for="contact-input" use:tooltip={$t('form.contactEmail.tooltip')}> <Icon name="mail" size="sm" /> - Contact Email + {$t('form.contactEmail.label')} </label> <input id="contact-input" type="email" bind:value={contactEmail} oninput={handleInputChange} - placeholder="hostmaster.example.com." + placeholder={$t('form.contactEmail.placeholder')} class="contact-input" spellcheck="false" /> </div> <div class="config-group"> - <label for="ttl-input" use:tooltip={'Default TTL for zone records in seconds'}> + <label for="ttl-input" use:tooltip={$t('form.defaultTTL.tooltip')}> <Icon name="clock" size="sm" /> - Default TTL (seconds) + {$t('form.defaultTTL.label')} </label> <input id="ttl-input" type="number" bind:value={ttl} oninput={handleInputChange} - placeholder="86400" + placeholder={$t('form.defaultTTL.placeholder')} class="ttl-input" min="60" max="2147483647" @@ -324,15 +329,15 @@ <div class="card results-card"> {#if results.success} <div class="results-header"> - <h3>Generated Zone Files</h3> + <h3>{$t('results.title')}</h3> <div class="summary-stats"> <div class="stat-item"> <span class="stat-value">{results.summary.totalZones}</span> - <span class="stat-label">Zone Files</span> + <span class="stat-label">{$t('results.summary.zoneFiles')}</span> </div> <div class="stat-item"> <span class="stat-value">{results.summary.totalRecords}</span> - <span class="stat-label">PTR Records</span> + <span class="stat-label">{$t('results.summary.ptrRecords')}</span> </div> </div> </div> @@ -346,7 +351,7 @@ <h4>{zone.zone}</h4> <div class="zone-meta"> <span class="zone-type {zone.type.toLowerCase()}">{zone.type}</span> - <span class="record-count">{zone.recordCount} records</span> + <span class="record-count">{$t('results.recordCount', { count: zone.recordCount })}</span> </div> </div> <button @@ -354,7 +359,7 @@ onclick={() => clipboard.copy(zone.content, `zone-${index}`)} > <Icon name={clipboard.isCopied(`zone-${index}`) ? 'check' : 'copy'} size="sm" /> - Copy Zone File + {$t('results.copyZoneFile')} </button> </div> @@ -367,13 +372,13 @@ {:else} <div class="error-result"> <Icon name="alert-triangle" size="lg" /> - <h4>Generation Error</h4> + <h4>{$t('results.error.title')}</h4> <p>{results.error}</p> <div class="error-help"> - <strong>Valid formats:</strong> + <strong>{$t('results.error.validFormats')}</strong> <ul> - <li>IPv4 CIDR: 192.168.1.0/24, 10.0.0.0/16</li> - <li>IPv6 CIDR: 2001:db8::/64, fe80::/10</li> + <li>{$t('results.error.ipv4CIDR')}</li> + <li>{$t('results.error.ipv6CIDR')}</li> </ul> </div> </div> @@ -385,34 +390,30 @@ <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>Zone File Structure</h4> + <h4>{$t('education.zoneFileStructure.title')}</h4> <p> - Generated zone files include proper SOA records with serial numbers, refresh/retry/expire timers, and NS - records for delegation. All PTR records are automatically generated based on your template. + {$t('education.zoneFileStructure.content')} </p> </div> <div class="education-item info-panel"> - <h4>Hostname Templates</h4> + <h4>{$t('education.hostnameTemplates.title')}</h4> <p> - Use placeholders to create consistent naming patterns. <code>[ip-dashes]</code> is popular for creating - hostnames like <code>host-192-168-1-100.example.com</code> from IP addresses. + {$t('education.hostnameTemplates.content')} </p> </div> <div class="education-item info-panel"> - <h4>Zone Delegation</h4> + <h4>{$t('education.zoneDelegation.title')}</h4> <p> - The generated zones need to be properly delegated by your ISP or DNS provider. Ensure your name servers are - configured to serve these zones and are reachable from the internet. + {$t('education.zoneDelegation.content')} </p> </div> <div class="education-item info-panel"> - <h4>Best Practices</h4> + <h4>{$t('education.bestPractices.title')}</h4> <p> - Keep TTL values reasonable (3600-86400 seconds). Use descriptive hostnames that help with network - troubleshooting. Ensure forward DNS (A/AAAA) records exist for consistency. + {$t('education.bestPractices.content')} </p> </div> </div> @@ -436,14 +437,6 @@ gap: var(--spacing-sm); color: var(--text-secondary); - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } - strong { color: var(--text-primary); } @@ -932,14 +925,6 @@ line-height: 1.6; margin: 0; } - - code { - background-color: var(--bg-tertiary); - color: var(--text-primary); - padding: 2px var(--spacing-xs); - border-radius: var(--radius-sm); - font-family: var(--font-mono); - } } @media (max-width: 768px) { diff --git a/src/lib/components/tools/ReverseZonesCalculator.svelte b/src/lib/components/tools/ReverseZonesCalculator.svelte index 746aaf1d..7e279fa9 100644 --- a/src/lib/components/tools/ReverseZonesCalculator.svelte +++ b/src/lib/components/tools/ReverseZonesCalculator.svelte @@ -3,6 +3,9 @@ import Icon from '$lib/components/global/Icon.svelte'; import { calculateReverseZones, type ReverseZoneInfo } from '$lib/utils/reverse-dns.js'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; let cidrInput = $state('192.168.1.0/24'); let results = $state<{ @@ -21,38 +24,43 @@ let selectedExample = $state<string | null>(null); let _userModified = $state(false); - const examples = [ + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); + + const examples = $derived([ { - label: 'IPv4 /24 Network', + label: $t('tools.reverse_zones_calculator.examples.ipv4_24.label'), cidr: '192.168.1.0/24', - description: 'Single class C zone delegation', + description: $t('tools.reverse_zones_calculator.examples.ipv4_24.description'), }, { - label: 'IPv4 /16 Network', + label: $t('tools.reverse_zones_calculator.examples.ipv4_16.label'), cidr: '10.0.0.0/16', - description: 'Class B with multiple /24 zones', + description: $t('tools.reverse_zones_calculator.examples.ipv4_16.description'), }, { - label: 'IPv4 /20 Block', + label: $t('tools.reverse_zones_calculator.examples.ipv4_20.label'), cidr: '172.16.32.0/20', - description: '16 class C zones needed', + description: $t('tools.reverse_zones_calculator.examples.ipv4_20.description'), }, { - label: 'IPv4 /28 Subnet', + label: $t('tools.reverse_zones_calculator.examples.ipv4_28.label'), cidr: '192.168.1.16/28', - description: 'Small subnet within /24 zone', + description: $t('tools.reverse_zones_calculator.examples.ipv4_28.description'), }, { - label: 'IPv6 /64 Network', + label: $t('tools.reverse_zones_calculator.examples.ipv6_64.label'), cidr: '2001:db8:1000::/64', - description: 'IPv6 nibble boundary delegation', + description: $t('tools.reverse_zones_calculator.examples.ipv6_64.description'), }, { - label: 'IPv6 /48 Prefix', + label: $t('tools.reverse_zones_calculator.examples.ipv6_48.label'), cidr: '2001:db8::/48', - description: 'IPv6 /48 delegation zone', + description: $t('tools.reverse_zones_calculator.examples.ipv6_48.description'), }, - ]; + ]); function loadExample(example: (typeof examples)[0]) { cidrInput = example.cidr; @@ -72,7 +80,7 @@ const zones = calculateReverseZones(trimmed); if (zones.length === 0) { - throw new Error('No reverse zones could be calculated for this CIDR'); + throw new Error($t('tools.reverse_zones_calculator.error.noZones')); } // Analyze the results @@ -82,13 +90,15 @@ let delegationType = ''; if (ipv4Zones > 0) { if (ipv4Zones === 1) { - delegationType = zones[0].delegation.includes('/24') ? 'Class C (/24)' : `Custom (${zones[0].delegation})`; + delegationType = zones[0].delegation.includes('/24') + ? $t('tools.reverse_zones_calculator.delegationTypes.classC') + : `${$t('tools.reverse_zones_calculator.delegationTypes.custom')} (${zones[0].delegation})`; } else { - delegationType = `Multiple zones (${ipv4Zones} x /24)`; + delegationType = `${$t('tools.reverse_zones_calculator.delegationTypes.multipleZones')} (${ipv4Zones} x /24)`; } } else if (ipv6Zones > 0) { const nibbleDepth = zones[0].nibbleDepth || 0; - delegationType = `IPv6 nibble boundary (${nibbleDepth} nibbles)`; + delegationType = `${$t('tools.reverse_zones_calculator.delegationTypes.ipv6Nibble')} (${nibbleDepth} nibbles)`; } results = { @@ -147,8 +157,8 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <div class="card"> <header class="card-header"> - <h1>Reverse Zones Calculator</h1> - <p>Calculate the minimal set of reverse DNS zones needed to delegate a CIDR block</p> + <h1>{$t('tools.reverse_zones_calculator.title')}</h1> + <p>{$t('tools.reverse_zones_calculator.description')}</p> </header> <!-- Educational Overview Card --> @@ -157,20 +167,22 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <div class="overview-item"> <Icon name="layers" size="sm" /> <div> - <strong>Zone Boundaries:</strong> IPv4 uses octet boundaries (/8, /16, /24) and IPv6 uses nibble boundaries (4-bit - increments). + <strong>{$t('tools.reverse_zones_calculator.overview.zoneBoundaries.title')}:</strong> + {$t('tools.reverse_zones_calculator.overview.zoneBoundaries.description')} </div> </div> <div class="overview-item"> <Icon name="share" size="sm" /> <div> - <strong>Delegation:</strong> DNS zones must be properly delegated by upstream providers at natural boundaries. + <strong>{$t('tools.reverse_zones_calculator.overview.delegation.title')}:</strong> + {$t('tools.reverse_zones_calculator.overview.delegation.description')} </div> </div> <div class="overview-item"> <Icon name="calculator" size="sm" /> <div> - <strong>Optimization:</strong> Calculate the minimal number of zones needed to avoid unnecessary complexity. + <strong>{$t('tools.reverse_zones_calculator.overview.optimization.title')}:</strong> + {$t('tools.reverse_zones_calculator.overview.optimization.description')} </div> </div> </div> @@ -181,7 +193,7 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools.reverse_zones_calculator.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -203,16 +215,16 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <!-- Input Card --> <div class="card input-card"> <div class="input-group"> - <label for="cidr-input" use:tooltip={'Enter a CIDR block to calculate reverse zones for'}> + <label for="cidr-input" use:tooltip={$t('tools.reverse_zones_calculator.input.help')}> <Icon name="network" size="sm" /> - CIDR Block + {$t('tools.reverse_zones_calculator.input.label')} </label> <input id="cidr-input" type="text" bind:value={cidrInput} oninput={handleInputChange} - placeholder="192.168.1.0/24 or 2001:db8::/64" + placeholder={$t('tools.reverse_zones_calculator.input.placeholder')} class="cidr-input {results?.success === true ? 'valid' : results?.success === false ? 'invalid' : ''}" spellcheck="false" /> @@ -224,15 +236,15 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <div class="card results-card"> {#if results.success} <div class="results-header"> - <h3>Reverse Zone Analysis</h3> + <h3>{$t('tools.reverse_zones_calculator.results.title')}</h3> <div class="summary-stats"> <div class="stat-item"> <span class="stat-value">{results.analysis.totalZones}</span> - <span class="stat-label">Total Zones</span> + <span class="stat-label">{$t('tools.reverse_zones_calculator.results.analysis.totalZones')}</span> </div> <div class="stat-item"> <span class="stat-value">{results.analysis.delegationType}</span> - <span class="stat-label">Delegation Type</span> + <span class="stat-label">{$t('tools.reverse_zones_calculator.results.analysis.delegationType')}</span> </div> </div> </div> @@ -241,7 +253,7 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <div class="zones-section"> <h4> <Icon name="list" size="sm" /> - Required Reverse Zones + {$t('tools.reverse_zones_calculator.results.zones.title')} </h4> <div class="zones-grid"> {#each results.zones as zone, index (zone.zone)} @@ -290,20 +302,20 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <div class="config-section"> <h4> <Icon name="settings" size="sm" /> - Configuration Examples + {$t('tools.reverse_zones_calculator.results.configuration.title')} </h4> <div class="config-examples"> <!-- BIND Configuration --> <div class="config-example"> <div class="config-header"> - <h5>BIND9 Configuration</h5> + <h5>{$t('tools.reverse_zones_calculator.results.configuration.bindConfig')}</h5> <button class="copy-button {clipboard.isCopied('bind-config') ? 'copied' : ''}" onclick={() => results && clipboard.copy(generateBindConfig(results.zones), 'bind-config')} > <Icon name={clipboard.isCopied('bind-config') ? 'check' : 'copy'} size="sm" /> - Copy Config + {$t('common.buttons.copy')} </button> </div> <pre class="config-content"><code>{generateBindConfig(results.zones)}</code></pre> @@ -312,13 +324,13 @@ chmod 644 /etc/bind/zones/${zone.zone}`; <!-- Delegation Commands --> <div class="config-example"> <div class="config-header"> - <h5>Zone File Setup Commands</h5> + <h5>{$t('tools.reverse_zones_calculator.results.configuration.delegationCommands')}</h5> <button class="copy-button {clipboard.isCopied('setup-commands') ? 'copied' : ''}" onclick={() => results && clipboard.copy(generateDelegationCommands(results.zones), 'setup-commands')} > <Icon name={clipboard.isCopied('setup-commands') ? 'check' : 'copy'} size="sm" /> - Copy Commands + {$t('common.buttons.copy')} </button> </div> <pre class="config-content"><code>{generateDelegationCommands(results.zones)}</code></pre> @@ -328,7 +340,7 @@ chmod 644 /etc/bind/zones/${zone.zone}`; {:else} <div class="error-result"> <Icon name="alert-triangle" size="lg" /> - <h4>Calculation Error</h4> + <h4>{$t('tools.reverse_zones_calculator.error.title')}</h4> <p>{results.error}</p> <div class="error-help"> <strong>Valid formats:</strong> diff --git a/src/lib/components/tools/SVCBHTTPSBuilder.svelte b/src/lib/components/tools/SVCBHTTPSBuilder.svelte index 4be5616a..57a7ae2a 100644 --- a/src/lib/components/tools/SVCBHTTPSBuilder.svelte +++ b/src/lib/components/tools/SVCBHTTPSBuilder.svelte @@ -1,8 +1,15 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + import { locale, loadTranslations, t } from '$lib/stores/language.js'; import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { useClipboard } from '$lib/composables'; + onMount(async () => { + await loadTranslations(get(locale), 'tools/svcb-https-builder'); + }); + interface ServiceParameter { key: string; value: string; @@ -37,15 +44,15 @@ // Button success states const clipboard = useClipboard(); - const parameterDescriptions = { - mandatory: 'Mandatory parameters that must be understood by the client', - alpn: 'Application-Layer Protocol Negotiation identifiers (e.g., h2, h3)', - 'no-default-alpn': 'Indicates that no default ALPN should be assumed', - port: 'Alternative port number for the service', - ipv4hint: 'IPv4 address hints to avoid additional DNS lookups', - ech: 'Encrypted Client Hello configuration', - ipv6hint: 'IPv6 address hints to avoid additional DNS lookups', - }; + const parameterDescriptions = $derived({ + mandatory: $t('parameters.descriptions.mandatory'), + alpn: $t('parameters.descriptions.alpn'), + 'no-default-alpn': $t('parameters.descriptions.no-default-alpn'), + port: $t('parameters.descriptions.port'), + ipv4hint: $t('parameters.descriptions.ipv4hint'), + ech: $t('parameters.descriptions.ech'), + ipv6hint: $t('parameters.descriptions.ipv6hint'), + }); const parameterKeyMap: Record<string, number> = { mandatory: 0, @@ -110,23 +117,23 @@ // Check domain format if (!domain.trim()) { - errors.push('Domain is required'); + errors.push($t('validation.errors.domainRequired')); } else if (!domain.includes('.')) { - warnings.push('Domain should include TLD (e.g., .com, .org)'); + warnings.push($t('validation.errors.domainTld')); } // Check priority if (priority < 0 || priority > 65535) { - errors.push('Priority must be between 0 and 65535'); + errors.push($t('validation.errors.priorityRange')); } if (priority === 0 && targetName !== '.') { - warnings.push('Priority 0 should typically use "." as target (alias mode)'); + warnings.push($t('validation.errors.priorityZeroTarget')); } // Check target name if (targetName && targetName !== '.' && !targetName.includes('.')) { - warnings.push('Target name should be a FQDN or "." for same domain'); + warnings.push($t('validation.errors.targetFqdn')); } // Validate parameters @@ -136,19 +143,19 @@ if (param.key === 'port') { const port = parseInt(param.value); if (isNaN(port) || port < 1 || port > 65535) { - errors.push('Port must be a number between 1 and 65535'); + errors.push($t('validation.errors.portRange')); } } if (param.key === 'alpn' && !param.value.trim()) { - errors.push('ALPN parameter requires at least one protocol identifier'); + errors.push($t('validation.errors.alpnRequired')); } if (param.key === 'ipv4hint') { const ips = param.value.split(',').map((ip) => ip.trim()); for (const ip of ips) { if (ip && !/^(\d{1,3}\.){3}\d{1,3}$/.test(ip)) { - errors.push(`Invalid IPv4 address in ipv4hint: ${ip}`); + errors.push($t('validation.errors.invalidIPv4', { ip })); } } } @@ -157,7 +164,7 @@ const ips = param.value.split(',').map((ip) => ip.trim()); for (const ip of ips) { if (ip && !ip.includes(':')) { - errors.push(`Invalid IPv6 address in ipv6hint: ${ip}`); + errors.push($t('validation.errors.invalidIPv6', { ip })); } } } @@ -168,14 +175,14 @@ const hasNoDefaultAlpn = enabledParams.some((p) => p.key === 'no-default-alpn'); if (hasAlpn && hasNoDefaultAlpn) { - warnings.push('Using both alpn and no-default-alpn may cause conflicts'); + warnings.push($t('validation.errors.alpnConflict')); } // Check record type specific recommendations if (recordType === 'HTTPS' && priority > 0) { const hasPort = enabledParams.some((p) => p.key === 'port'); if (!hasPort) { - warnings.push('HTTPS records typically benefit from port parameter'); + warnings.push($t('validation.errors.httpsPortRecommendation')); } } @@ -211,10 +218,10 @@ } } - const exampleConfigurations = [ + const exampleConfigurations = $derived([ { - name: 'HTTPS with HTTP/2', - description: 'Basic HTTPS service with HTTP/2 support', + name: $t('examples.items.0.name'), + description: $t('examples.items.0.description'), recordType: 'HTTPS' as const, domain: 'example.com', priority: 1, @@ -225,8 +232,8 @@ ], }, { - name: 'CDN Endpoint', - description: 'HTTPS service pointing to CDN with IP hints', + name: $t('examples.items.1.name'), + description: $t('examples.items.1.description'), recordType: 'HTTPS' as const, domain: 'www.example.com', priority: 1, @@ -238,8 +245,8 @@ ], }, { - name: 'Alternative Service', - description: 'Alternative HTTPS service on different port', + name: $t('examples.items.2.name'), + description: $t('examples.items.2.description'), recordType: 'HTTPS' as const, domain: 'api.example.com', priority: 2, @@ -250,7 +257,7 @@ { key: 'ipv4hint', value: '203.0.113.10', enabled: true }, ], }, - ]; + ]); function loadExample(example: (typeof exampleConfigurations)[0]): void { domain = example.domain; @@ -273,21 +280,14 @@ selectedExample = example.name; } - const usageNotes = [ - 'Priority 0 creates an alias record (AliasMode), priority >0 creates a service record (ServiceMode)', - 'Use "." as target name to indicate the same domain as the owner name', - 'ALPN values should match the protocols actually supported by the service', - 'IP hints can improve connection performance by avoiding additional DNS lookups', - 'ECH parameter enables Encrypted Client Hello for enhanced privacy', - ]; + const usageNotes = $derived($t('usageNotes')); </script> <div class="card"> <div class="card-header"> - <h1>SVCB/HTTPS Builder</h1> + <h1>{$t('title')}</h1> <p class="card-subtitle"> - Build SVCB and HTTPS resource records with service parameters for enhanced service discovery and connection - optimization. + {$t('description')} </p> </div> @@ -297,19 +297,19 @@ <div class="section-header"> <h3> <Icon name="globe" size="sm" /> - Service Configuration + {$t('sections.serviceConfiguration')} </h3> </div> <div class="service-config-grid"> <div class="input-group"> - <label for="domain" use:tooltip={'Domain name for the SVCB/HTTPS record'}> Domain: </label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('form.domain.tooltip')}> {$t('form.domain.label')} </label> + <input id="domain" type="text" bind:value={domain} placeholder={$t('form.domain.placeholder')} /> </div> <div class="input-group"> - <label for="recordType" use:tooltip={'Record type: HTTPS for HTTP services, SVCB for general services'}> - Record Type: + <label for="recordType" use:tooltip={$t('form.recordType.tooltip')}> + {$t('form.recordType.label')} </label> <select id="recordType" bind:value={recordType}> <option value="HTTPS">HTTPS</option> @@ -318,13 +318,25 @@ </div> <div class="input-group"> - <label for="priority" use:tooltip={'Priority: 0 for alias mode, >0 for service mode'}> Priority: </label> - <input id="priority" type="number" bind:value={priority} min="0" max="65535" placeholder="1" /> + <label for="priority" use:tooltip={$t('form.priority.tooltip')}> {$t('form.priority.label')} </label> + <input + id="priority" + type="number" + bind:value={priority} + min="0" + max="65535" + placeholder={$t('form.priority.placeholder')} + /> </div> <div class="input-group"> - <label for="targetName" use:tooltip={"Target domain name or '.' for same domain"}> Target Name: </label> - <input id="targetName" type="text" bind:value={targetName} placeholder=". (same domain)" /> + <label for="targetName" use:tooltip={$t('form.targetName.tooltip')}> {$t('form.targetName.label')} </label> + <input + id="targetName" + type="text" + bind:value={targetName} + placeholder={$t('form.targetName.placeholder')} + /> </div> </div> </div> @@ -333,7 +345,7 @@ <div class="section-header"> <h3> <Icon name="settings" size="sm" /> - Service Parameters + {$t('sections.serviceParameters')} </h3> </div> @@ -360,18 +372,18 @@ bind:value={parameter.value} disabled={!parameter.enabled} placeholder={parameter.key === 'alpn' - ? 'h2,h3' + ? $t('parameters.placeholders.alpn') : parameter.key === 'port' - ? '443' + ? $t('parameters.placeholders.port') : parameter.key === 'ipv4hint' - ? '203.0.113.1,203.0.113.2' + ? $t('parameters.placeholders.ipv4hint') : parameter.key === 'ipv6hint' - ? '2001:db8::1,2001:db8::2' + ? $t('parameters.placeholders.ipv6hint') : parameter.key === 'ech' - ? 'base64-encoded-config' + ? $t('parameters.placeholders.ech') : parameter.key === 'mandatory' - ? '1,3' - : 'value'} + ? $t('parameters.placeholders.mandatory') + : $t('parameters.placeholders.default')} class="parameter-input" /> </div> @@ -385,27 +397,29 @@ <div class="results-section"> <div class="record-section"> <div class="section-header"> - <h3>Generated {recordType} Record</h3> + <h3>{$t('results.generatedRecord', { recordType })}</h3> <div class="actions"> <button type="button" class="copy-btn" class:success={clipboard.isCopied('copy-svcb')} onclick={() => clipboard.copy(dnsRecord, 'copy-svcb')} - use:tooltip={'Copy record to clipboard'} + use:tooltip={$t('results.actions.copy.tooltip')} > <Icon name={clipboard.isCopied('copy-svcb') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('copy-svcb') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('copy-svcb') ? $t('results.actions.copy.copied') : $t('results.actions.copy.button')} </button> <button type="button" class="export-btn" class:success={clipboard.isCopied('export-svcb')} onclick={exportAsZoneFile} - use:tooltip={'Download as zone file'} + use:tooltip={$t('results.actions.export.tooltip')} > <Icon name={clipboard.isCopied('export-svcb') ? 'check' : 'download'} size="sm" /> - {clipboard.isCopied('export-svcb') ? 'Downloaded!' : 'Export'} + {clipboard.isCopied('export-svcb') + ? $t('results.actions.export.downloaded') + : $t('results.actions.export.button')} </button> </div> </div> @@ -417,22 +431,22 @@ </div> <div class="record-breakdown"> - <h4>Record Breakdown:</h4> + <h4>{$t('results.recordBreakdown')}</h4> <div class="breakdown-grid"> <div class="breakdown-item"> - <strong>Type:</strong> + <strong>{$t('results.breakdown.type')}</strong> {recordType} </div> <div class="breakdown-item"> - <strong>Priority:</strong> - {priority} ({priority === 0 ? 'Alias Mode' : 'Service Mode'}) + <strong>{$t('results.breakdown.priority')}</strong> + {priority} ({priority === 0 ? $t('results.breakdown.aliasMode') : $t('results.breakdown.serviceMode')}) </div> <div class="breakdown-item"> - <strong>Target:</strong> + <strong>{$t('results.breakdown.target')}</strong> {serviceRecord.targetName} </div> <div class="breakdown-item"> - <strong>Parameters:</strong> + <strong>{$t('results.breakdown.parameters')}</strong> {validation.parameterCount} </div> </div> @@ -443,15 +457,15 @@ <div class="section-header"> <h3> <Icon name="bar-chart" size="sm" /> - Validation + {$t('validation.title')} </h3> </div> <div class="validation-status"> <div class="status-item"> - <span class="status-label">Status:</span> + <span class="status-label">{$t('validation.status.label')}</span> <span class="status-value" class:success={validation.isValid} class:error={!validation.isValid}> - {validation.isValid ? 'Valid' : 'Invalid'} + {validation.isValid ? $t('validation.status.valid') : $t('validation.status.invalid')} </span> </div> </div> @@ -481,7 +495,7 @@ {#if validation.isValid && validation.errors.length === 0 && validation.warnings.length === 0} <div class="validation-messages success"> <Icon name="check-circle" size="sm" /> - <div class="message">{recordType} record is valid and ready to deploy!</div> + <div class="message">{$t('validation.success', { recordType })}</div> </div> {/if} </div> @@ -490,7 +504,7 @@ <div class="section-header"> <h3> <Icon name="info" size="sm" /> - Usage Notes + {$t('sections.usageNotes')} </h3> </div> @@ -509,7 +523,7 @@ <details class="examples-toggle" bind:open={showExamples}> <summary> <Icon name="lightbulb" size="sm" /> - Example Configurations + {$t('examples.title')} </summary> <div class="examples-grid"> {#each exampleConfigurations as example (example.name)} @@ -524,10 +538,14 @@ </div> <p class="example-description">{example.description}</p> <div class="example-config"> - <div>Type: <code>{example.recordType}</code>, Priority: <code>{example.priority}</code></div> - <div>Target: <code>{example.targetName}</code></div> + <div> + {$t('examples.config.type')} <code>{example.recordType}</code>, {$t('examples.config.priority')} + <code>{example.priority}</code> + </div> + <div>{$t('examples.config.target')} <code>{example.targetName}</code></div> <div class="example-params"> - Params: {example.parameters.map((p) => `${p.key}=${p.value}`).join(', ')} + {$t('examples.config.params')} + {example.parameters.map((p) => `${p.key}=${p.value}`).join(', ')} </div> </div> </button> diff --git a/src/lib/components/tools/SubnetCalculator.svelte b/src/lib/components/tools/SubnetCalculator.svelte index 12316213..e2adab5e 100644 --- a/src/lib/components/tools/SubnetCalculator.svelte +++ b/src/lib/components/tools/SubnetCalculator.svelte @@ -11,6 +11,14 @@ import { useClipboard } from '$lib/composables'; import { goto } from '$app/navigation'; import type { SubnetInfo } from '$lib/types/ip.js'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); const versionOptions = [ { value: 'ipv4' as const, label: 'IPv4' }, @@ -54,15 +62,19 @@ </script> <ToolContentContainer - title="Subnet Calculator" - description="Calculate network, broadcast, and host information for any subnet." + title={$t('tools.subnet_calculator.title')} + description={$t('tools.subnet_calculator.description')} navOptions={versionOptions} bind:selectedNav={selectedVersion} onNavChange={handleVersionChange} > <!-- Input --> <div class="form-group"> - <CIDRInput bind:value={cidrInput} label="Network Address (CIDR)" placeholder="192.168.1.0/24" /> + <CIDRInput + bind:value={cidrInput} + label={$t('tools.subnet_calculator.input.cidr_label')} + placeholder={$t('tools.subnet_calculator.input.cidr_placeholder')} + /> </div> <!-- Results --> @@ -73,23 +85,27 @@ <h3 style="margin-bottom: var(--spacing-md); border-bottom: 1px solid var(--border-primary); padding-bottom: var(--spacing-xs);" > - Network Information + {$t('tools.subnet_calculator.sections.network_info')} </h3> <div class="info-cards"> <div class="info-card"> - <span class="info-label" use:tooltip={'First IP in subnet - identifies the network'}>Network Address</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.network_address')} + >{$t('tools.subnet_calculator.fields.network_address')}</span + > <div class="value-copy"> <code class="ip-value success">{subnetInfo.network.octets.join('.')}</code> <Tooltip - text={clipboard.isCopied('network') ? 'Copied!' : 'Copy network address to clipboard'} + text={clipboard.isCopied('network') + ? $t('tools.subnet_calculator.actions.copied') + : $t('tools.subnet_calculator.actions.copy_network')} position="top" > <button type="button" class="btn-icon copy-btn {clipboard.isCopied('network') ? 'copied' : ''}" onclick={() => clipboard.copy(subnetInfo!.network.octets.join('.'), 'network')} - aria-label="Copy network address" + aria-label={$t('tools.subnet_calculator.actions.copy_network_aria')} > <SvgIcon icon={clipboard.isCopied('network') ? 'check' : 'clipboard'} size="md" /> </button> @@ -98,18 +114,22 @@ </div> <div class="info-card"> - <span class="info-label" use:tooltip={'Last IP in subnet - sends to all hosts'}>Broadcast Address</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.broadcast_address')} + >{$t('tools.subnet_calculator.fields.broadcast_address')}</span + > <div class="value-copy"> <code class="ip-value error">{subnetInfo.broadcast.octets.join('.')}</code> <Tooltip - text={clipboard.isCopied('broadcast') ? 'Copied!' : 'Copy broadcast address to clipboard'} + text={clipboard.isCopied('broadcast') + ? $t('tools.subnet_calculator.actions.copied') + : $t('tools.subnet_calculator.actions.copy_broadcast')} position="top" > <button type="button" class="btn-icon copy-btn {clipboard.isCopied('broadcast') ? 'copied' : ''}" onclick={() => clipboard.copy(subnetInfo!.broadcast.octets.join('.'), 'broadcast')} - aria-label="Copy broadcast address" + aria-label={$t('tools.subnet_calculator.actions.copy_broadcast_aria')} > <SvgIcon icon={clipboard.isCopied('broadcast') ? 'check' : 'clipboard'} size="md" /> </button> @@ -118,7 +138,9 @@ </div> <div class="info-card"> - <span class="info-label" use:tooltip={'Defines network vs host portion of IP'}>Subnet Mask</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.subnet_mask')} + >{$t('tools.subnet_calculator.fields.subnet_mask')}</span + > <div class="value-copy"> <code class="ip-value info">{subnetInfo.subnet.octets.join('.')}</code> <span class="cidr">/{subnetInfo.cidr}</span> @@ -126,7 +148,9 @@ </div> <div class="info-card"> - <span class="info-label" use:tooltip={'Inverse of subnet mask - used in ACLs'}>Wildcard Mask</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.wildcard_mask')} + >{$t('tools.subnet_calculator.fields.wildcard_mask')}</span + > <code class="ip-value warning">{subnetInfo.wildcardMask.octets.join('.')}</code> </div> </div> @@ -137,29 +161,35 @@ <h3 style="margin-bottom: var(--spacing-md); border-bottom: 1px solid var(--border-primary); padding-bottom: var(--spacing-xs);" > - Host Information + {$t('tools.subnet_calculator.sections.host_info')} </h3> <div class="info-cards"> <div class="info-card"> - <span class="info-label" use:tooltip={'All IP addresses in this subnet'}>Total Hosts</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.total_hosts')} + >{$t('tools.subnet_calculator.fields.total_hosts')}</span + > <span class="metric-value info">{formatNumber(subnetInfo.hostCount)}</span> </div> <div class="info-card"> - <span class="info-label" use:tooltip={'IPs available for devices (excludes network/broadcast)'} - >Usable Hosts</span + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.usable_hosts')} + >{$t('tools.subnet_calculator.fields.usable_hosts')}</span > <span class="metric-value success">{formatNumber(subnetInfo.usableHosts)}</span> </div> <div class="info-card"> - <span class="info-label" use:tooltip={'First IP address available for devices'}>First Host</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.first_host')} + >{$t('tools.subnet_calculator.fields.first_host')}</span + > <code class="ip-value success">{subnetInfo.firstHost.octets.join('.')}</code> </div> <div class="info-card"> - <span class="info-label" use:tooltip={'Last IP address available for devices'}>Last Host</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.last_host')} + >{$t('tools.subnet_calculator.fields.last_host')}</span + > <code class="ip-value success">{subnetInfo.lastHost.octets.join('.')}</code> </div> </div> @@ -168,18 +198,24 @@ <!-- Binary Representation --> <section class="info-panel" style="margin-top: var(--spacing-lg);"> - <h3>Binary Representation</h3> + <h3>{$t('tools.subnet_calculator.sections.binary_representation')}</h3> <div class="binary-display"> <div class="binary-row"> - <span class="info-label" use:tooltip={'Network address in binary format'}>Network:</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.network_binary')} + >{$t('tools.subnet_calculator.binary.network_label')}</span + > <code class="binary-value success">{subnetInfo.network.binary}</code> </div> <div class="binary-row"> - <span class="info-label" use:tooltip={'Subnet mask in binary format'}>Mask:</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.mask_binary')} + >{$t('tools.subnet_calculator.binary.mask_label')}</span + > <code class="binary-value info">{subnetInfo.subnet.binary}</code> </div> <div class="binary-row"> - <span class="info-label" use:tooltip={'Broadcast address in binary format'}>Broadcast:</span> + <span class="info-label" use:tooltip={$t('tools.subnet_calculator.tooltips.broadcast_binary')} + >{$t('tools.subnet_calculator.binary.broadcast_label')}</span + > <code class="binary-value error">{subnetInfo.broadcast.binary}</code> </div> </div> @@ -195,75 +231,65 @@ {#if isCalculating} <div class="loading" style="justify-content: center; padding: var(--spacing-xl);"> <div class="spinner"></div> - Calculating subnet... + {$t('tools.subnet_calculator.actions.calculating')} </div> {/if} <!-- Explainer Section --> <section class="explainer-section"> - <h3>Understanding Subnet Calculations</h3> + <h3>{$t('tools.subnet_calculator.explainer.title')}</h3> <div class="explainer-grid"> <div class="explainer-card no-hover"> - <h4>Network Address</h4> + <h4>{$t('tools.subnet_calculator.explainer.network_address.title')}</h4> <p> - The first IP address in a subnet, used to identify the network itself. Hosts cannot be assigned this address - as it represents the entire network segment. + {$t('tools.subnet_calculator.explainer.network_address.description')} </p> </div> <div class="explainer-card no-hover"> - <h4>Broadcast Address</h4> + <h4>{$t('tools.subnet_calculator.explainer.broadcast_address.title')}</h4> <p> - The last IP address in a subnet, used to send messages to all devices on the network. When a packet is sent to - this address, it reaches every host in the subnet. + {$t('tools.subnet_calculator.explainer.broadcast_address.description')} </p> </div> <div class="explainer-card no-hover"> - <h4>Subnet Mask</h4> + <h4>{$t('tools.subnet_calculator.explainer.subnet_mask.title')}</h4> <p> - Defines which portion of an IP address represents the network and which represents the host. A mask of /24 - means the first 24 bits identify the network. + {$t('tools.subnet_calculator.explainer.subnet_mask.description')} </p> </div> <div class="explainer-card no-hover"> - <h4>Wildcard Mask</h4> + <h4>{$t('tools.subnet_calculator.explainer.wildcard_mask.title')}</h4> <p> - The inverse of a subnet mask, used in access control lists. Where the subnet mask has 1s, the wildcard has 0s, - and vice versa. + {$t('tools.subnet_calculator.explainer.wildcard_mask.description')} </p> </div> <div class="explainer-card no-hover"> - <h4>Usable Hosts</h4> + <h4>{$t('tools.subnet_calculator.explainer.usable_hosts.title')}</h4> <p> - The number of IP addresses available for devices. Always 2 less than total addresses because network and - broadcast addresses are reserved. + {$t('tools.subnet_calculator.explainer.usable_hosts.description')} </p> </div> <div class="explainer-card no-hover"> - <h4>CIDR Notation</h4> + <h4>{$t('tools.subnet_calculator.explainer.cidr_notation.title')}</h4> <p> - Classless Inter-Domain Routing notation (e.g., /24) indicates how many bits are used for the network portion. - Higher numbers mean smaller subnets with fewer hosts. + {$t('tools.subnet_calculator.explainer.cidr_notation.description')} </p> </div> </div> <div class="tips-box"> - <h4>πŸ’‘ Pro Tips</h4> + <h4>{$t('tools.subnet_calculator.tips.title')}</h4> <ul> - <li><strong>Plan for Growth:</strong> Choose subnet sizes that accommodate future expansion</li> - <li><strong>Binary Understanding:</strong> Learning binary helps understand how subnetting works</li> - <li> - <strong>Common Sizes:</strong> /24 (254 hosts), /25 (126 hosts), /26 (62 hosts), /30 (2 hosts for point-to-point) - </li> - <li> - <strong>Private Networks:</strong> Use RFC 1918 addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x) for internal networks - </li> + <li>{$t('tools.subnet_calculator.tips.plan_growth')}</li> + <li>{$t('tools.subnet_calculator.tips.binary_understanding')}</li> + <li>{$t('tools.subnet_calculator.tips.common_sizes')}</li> + <li>{$t('tools.subnet_calculator.tips.private_networks')}</li> </ul> </div> </section> diff --git a/src/lib/components/tools/SubnetPlanner.svelte b/src/lib/components/tools/SubnetPlanner.svelte index 9ea6d9fa..a1bf11f4 100644 --- a/src/lib/components/tools/SubnetPlanner.svelte +++ b/src/lib/components/tools/SubnetPlanner.svelte @@ -6,6 +6,14 @@ import ToolContentContainer from '$lib/components/global/ToolContentContainer.svelte'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let parentCIDR = $state('192.168.1.0/24'); let strategy = $state<'preserve-order' | 'fit-best'>('fit-best'); @@ -15,50 +23,57 @@ let showVisualization = $state(true); let draggedIndex = $state<number | null>(null); - let requests = $state<SubnetRequest[]>([ - { id: crypto.randomUUID(), name: 'Sales', size: 50, priority: 1 }, - { id: crypto.randomUUID(), name: 'Engineering', size: 30, priority: 2 }, - { id: crypto.randomUUID(), name: 'Marketing', size: 20, priority: 3 }, - { id: crypto.randomUUID(), name: 'Servers', size: 10, priority: 4 }, - ]); + let requests = $state<SubnetRequest[]>([]); + + // Initialize default requests after translations load + onMount(() => { + if (requests.length === 0) { + requests = [ + { id: crypto.randomUUID(), name: $t('tools.subnet_planner.defaultSubnets.sales'), size: 50, priority: 1 }, + { id: crypto.randomUUID(), name: $t('tools.subnet_planner.defaultSubnets.engineering'), size: 30, priority: 2 }, + { id: crypto.randomUUID(), name: $t('tools.subnet_planner.defaultSubnets.marketing'), size: 20, priority: 3 }, + { id: crypto.randomUUID(), name: $t('tools.subnet_planner.defaultSubnets.servers'), size: 10, priority: 4 }, + ]; + } + }); - const examples = [ + const examples = $derived([ { - label: 'Office Network', + label: $t('tools.subnet_planner.examples.officeNetwork.label'), parent: '192.168.0.0/24', requests: [ - { name: 'Sales', size: 50 }, - { name: 'Engineering', size: 30 }, - { name: 'Servers', size: 10 }, + { name: $t('tools.subnet_planner.examples.officeNetwork.subnets.sales'), size: 50 }, + { name: $t('tools.subnet_planner.examples.officeNetwork.subnets.engineering'), size: 30 }, + { name: $t('tools.subnet_planner.examples.officeNetwork.subnets.servers'), size: 10 }, ], }, { - label: 'Large Corporate', + label: $t('tools.subnet_planner.examples.largeCorporate.label'), parent: '10.0.0.0/16', requests: [ - { name: 'HQ', size: 2000 }, - { name: 'Branch Office', size: 500 }, - { name: 'DMZ', size: 100 }, - { name: 'Management', size: 20 }, + { name: $t('tools.subnet_planner.examples.largeCorporate.subnets.hq'), size: 2000 }, + { name: $t('tools.subnet_planner.examples.largeCorporate.subnets.branchOffice'), size: 500 }, + { name: $t('tools.subnet_planner.examples.largeCorporate.subnets.dmz'), size: 100 }, + { name: $t('tools.subnet_planner.examples.largeCorporate.subnets.management'), size: 20 }, ], }, { - label: 'Data Center', + label: $t('tools.subnet_planner.examples.dataCenter.label'), parent: '172.16.0.0/20', requests: [ - { name: 'Web Servers', size: 200 }, - { name: 'Database Cluster', size: 50 }, - { name: 'Load Balancers', size: 10 }, - { name: 'Monitoring', size: 5 }, + { name: $t('tools.subnet_planner.examples.dataCenter.subnets.webServers'), size: 200 }, + { name: $t('tools.subnet_planner.examples.dataCenter.subnets.databaseCluster'), size: 50 }, + { name: $t('tools.subnet_planner.examples.dataCenter.subnets.loadBalancers'), size: 10 }, + { name: $t('tools.subnet_planner.examples.dataCenter.subnets.monitoring'), size: 5 }, ], }, - ]; + ]); /* Add new subnet request */ function addRequest() { requests.push({ id: crypto.randomUUID(), - name: `Subnet ${requests.length + 1}`, + name: $t('tools.subnet_planner.newSubnet.defaultName', { number: requests.length + 1 }), size: 10, priority: requests.length + 1, }); @@ -73,6 +88,12 @@ performPlanning(); } + /* Clear all subnet requests */ + function clearRequests() { + requests = []; + result = null; + } + /* Set example */ function setExample(example: (typeof examples)[0]) { parentCIDR = example.parent; @@ -103,7 +124,15 @@ 2, ); } else { - const headers = ['Name', 'CIDR', 'Network', 'Broadcast', 'Usable Hosts', 'Requested', 'Efficiency %']; + const headers = [ + $t('tools.subnet_planner.export.headers.name'), + $t('tools.subnet_planner.export.headers.cidr'), + $t('tools.subnet_planner.export.headers.network'), + $t('tools.subnet_planner.export.headers.broadcast'), + $t('tools.subnet_planner.export.headers.usableHosts'), + $t('tools.subnet_planner.export.headers.requested'), + $t('tools.subnet_planner.export.headers.efficiency'), + ]; const rows = result.allocated.map((subnet) => [ `"${subnet.name}"`, subnet.cidr, @@ -157,12 +186,6 @@ draggedIndex = null; } - /* Clear all requests */ - function clearRequests() { - requests = []; - result = null; - } - /* Perform subnet planning */ function performPlanning() { if (!parentCIDR.trim() || requests.length === 0) { @@ -195,11 +218,17 @@ type: 'allocated' | 'leftover', ): string { if (type === 'allocated' && item.name) { - const size = item.start && item.end ? formatNumber(Number(item.end - item.start + 1n)) : 'Unknown'; - return `${item.name}\n${item.cidr}\nSize: ${size} addresses`; + const size = + item.start && item.end + ? formatNumber(Number(item.end - item.start + 1n)) + : $t('tools.subnet_planner.visualization.unknown'); + return `${item.name}\n${item.cidr}\n${$t('tools.subnet_planner.visualization.sizeLabel')}: ${size} ${$t('tools.subnet_planner.visualization.addresses')}`; } else { - const size = item.start && item.end ? formatNumber(Number(item.end - item.start + 1n)) : 'Unknown'; - return `Leftover Space\n${item.cidr}\nSize: ${size} addresses`; + const size = + item.start && item.end + ? formatNumber(Number(item.end - item.start + 1n)) + : $t('tools.subnet_planner.visualization.unknown'); + return `${$t('tools.subnet_planner.visualization.leftoverSpace')}\n${item.cidr}\n${$t('tools.subnet_planner.visualization.sizeLabel')}: ${size} ${$t('tools.subnet_planner.visualization.addresses')}`; } } @@ -211,13 +240,10 @@ }); </script> -<ToolContentContainer - title="Subnet Planner (VLSM)" - description="Design Variable Length Subnet Mask (VLSM) allocations with drag-and-drop reordering and space optimization." -> +<ToolContentContainer title={$t('tools.subnet_planner.title')} description={$t('tools.subnet_planner.description')}> <!-- Strategy Selection --> <div class="strategy-section"> - <h3>Allocation Strategy</h3> + <h3>{$t('tools.subnet_planner.strategy.title')}</h3> <div class="strategy-tabs"> <button type="button" @@ -227,9 +253,9 @@ strategy = 'fit-best'; performPlanning(); }} - use:tooltip={{ text: 'Sort by size (largest first) for optimal space usage', position: 'top' }} + use:tooltip={{ text: $t('tools.subnet_planner.strategy.fitBest.tooltip'), position: 'top' }} > - Fit Best + {$t('tools.subnet_planner.strategy.fitBest.label')} </button> <button type="button" @@ -239,15 +265,15 @@ strategy = 'preserve-order'; performPlanning(); }} - use:tooltip={{ text: 'Allocate in the order specified (may waste space)', position: 'top' }} + use:tooltip={{ text: $t('tools.subnet_planner.strategy.preserveOrder.tooltip'), position: 'top' }} > - Preserve Order + {$t('tools.subnet_planner.strategy.preserveOrder.label')} </button> <div class="options"> <label class="checkbox-label"> <input type="checkbox" bind:checked={usableHosts} onchange={() => performPlanning()} /> - <span class="checkbox-text" use:tooltip={'For IPv4, treat network and broadcast addresses as unusable'}> - IPv4 usable hosts (exclude network/broadcast) + <span class="checkbox-text" use:tooltip={$t('tools.subnet_planner.strategy.usableHosts.tooltip')}> + {$t('tools.subnet_planner.strategy.usableHosts.label')} </span> </label> </div> @@ -257,15 +283,15 @@ <!-- Parent Network --> <div class="parent-section"> <div class="input-group"> - <label for="parent-cidr" use:tooltip={'The parent CIDR to subdivide (e.g., 192.168.1.0/24)'}> - Parent Network + <label for="parent-cidr" use:tooltip={$t('tools.subnet_planner.parentNetwork.tooltip')}> + {$t('tools.subnet_planner.parentNetwork.label')} </label> <input id="parent-cidr" type="text" bind:value={parentCIDR} oninput={() => performPlanning()} - placeholder="192.168.1.0/24" + placeholder={$t('tools.subnet_planner.parentNetwork.placeholder')} class="input-field" /> </div> @@ -274,15 +300,15 @@ <!-- Subnet Requests --> <div class="requests-section"> <div class="requests-header"> - <h3>Subnet Requirements</h3> + <h3>{$t('tools.subnet_planner.requirements.title')}</h3> <div class="requests-actions"> <button type="button" class="btn btn-primary btn-sm" onclick={addRequest}> <Icon name="plus" size="sm" /> - Add Subnet + {$t('tools.subnet_planner.requirements.addSubnet')} </button> <button type="button" class="btn btn-secondary btn-sm" onclick={clearRequests} disabled={requests.length === 0}> <Icon name="trash" size="sm" /> - Clear All + {$t('tools.subnet_planner.requirements.clearAll')} </button> </div> </div> @@ -303,7 +329,7 @@ <Icon name="draggable" size="sm" /> </div> - <div class="request-priority" use:tooltip={'Processing priority - drag to reorder'}> + <div class="request-priority" use:tooltip={$t('tools.subnet_planner.requirements.priorityTooltip')}> #{request.priority} </div> @@ -312,7 +338,7 @@ type="text" bind:value={request.name} oninput={() => performPlanning()} - placeholder="Subnet name" + placeholder={$t('tools.subnet_planner.requirements.namePlaceholder')} class="name-field" /> <input @@ -320,10 +346,10 @@ bind:value={request.size} oninput={() => performPlanning()} min="1" - placeholder="Host count" + placeholder={$t('tools.subnet_planner.requirements.sizePlaceholder')} class="size-field" /> - <span class="hosts-label">hosts</span> + <span class="hosts-label">{$t('tools.subnet_planner.requirements.hostsLabel')}</span> </div> <button type="button" class="btn btn-danger btn-xs" onclick={() => removeRequest(request.id)}> @@ -334,7 +360,7 @@ {#if requests.length === 0} <div class="empty-state"> - <p>No subnet requirements defined. Add some subnets to get started.</p> + <p>{$t('tools.subnet_planner.requirements.emptyState')}</p> </div> {/if} </div> @@ -342,7 +368,7 @@ <!-- Examples --> <div class="examples-section"> - <h4>Quick Examples</h4> + <h4>{$t('tools.subnet_planner.examples.title')}</h4> <div class="examples-grid"> {#each examples as example (example.label)} <button type="button" class="example-btn" onclick={() => setExample(example)}> @@ -358,7 +384,7 @@ <!-- Errors --> {#if result.errors.length > 0} <div class="info-panel error"> - <h3>Errors</h3> + <h3>{$t('tools.subnet_planner.results.errors.title')}</h3> <ul> {#each result.errors as error, index (index)} <li>{error}</li> @@ -370,7 +396,7 @@ <!-- Warnings --> {#if result.warnings.length > 0} <div class="info-panel warning"> - <h3>Warnings</h3> + <h3>{$t('tools.subnet_planner.results.warnings.title')}</h3> <ul> {#each result.warnings as warning, index (index)} <li>{warning}</li> @@ -383,7 +409,7 @@ <!-- Statistics --> <div class="stats-section"> <div class="summary-header"> - <h3>Allocation Results</h3> + <h3>{$t('tools.subnet_planner.results.title')}</h3> <div class="export-buttons"> <button type="button" @@ -392,7 +418,7 @@ onclick={() => exportResults('csv')} > <Icon name={clipboard.isCopied('export-csv') ? 'check' : 'download'} size="sm" /> - CSV + {$t('tools.subnet_planner.actions.exportCSV')} </button> <button type="button" @@ -401,37 +427,43 @@ onclick={() => exportResults('json')} > <Icon name={clipboard.isCopied('export-json') ? 'check' : 'download'} size="sm" /> - JSON + {$t('tools.subnet_planner.actions.exportJSON')} </button> </div> </div> <div class="stats-grid"> <div class="stat-card parent"> - <span class="stat-label" use:tooltip={'The original network being subdivided into smaller subnets'} - >Parent Network</span + <span class="stat-label" use:tooltip={$t('tools.subnet_planner.results.stats.parentNetwork.tooltip')} + >{$t('tools.subnet_planner.results.stats.parentNetwork.label')}</span > <span class="stat-value">{result.stats.parentCIDR}</span> - <span class="stat-detail">total space</span> + <span class="stat-detail">{$t('tools.subnet_planner.results.stats.parentNetwork.detail')}</span> </div> <div class="stat-card allocated"> - <span class="stat-label" use:tooltip={'Total addresses assigned to subnets'}>Allocated</span> + <span class="stat-label" use:tooltip={$t('tools.subnet_planner.results.stats.allocated.tooltip')} + >{$t('tools.subnet_planner.results.stats.allocated.label')}</span + > <span class="stat-value">{result.stats.totalAllocated}</span> - <span class="stat-detail">{result.stats.successfulAllocations} subnets</span> + <span class="stat-detail" + >{$t('tools.subnet_planner.results.stats.allocated.detail', { + count: result.stats.successfulAllocations, + })}</span + > </div> <div class="stat-card leftover"> - <span class="stat-label" use:tooltip={'Unallocated address space that could be used for future subnets'} - >Leftover</span + <span class="stat-label" use:tooltip={$t('tools.subnet_planner.results.stats.leftover.tooltip')} + >{$t('tools.subnet_planner.results.stats.leftover.label')}</span > <span class="stat-value">{result.stats.totalLeftover}</span> - <span class="stat-detail">addresses</span> + <span class="stat-detail">{$t('tools.subnet_planner.results.stats.leftover.detail')}</span> </div> <div class="stat-card efficiency"> - <span class="stat-label" use:tooltip={'Percentage of parent network space that has been allocated'} - >Efficiency</span + <span class="stat-label" use:tooltip={$t('tools.subnet_planner.results.stats.efficiency.tooltip')} + >{$t('tools.subnet_planner.results.stats.efficiency.label')}</span > <span class="stat-value">{result.stats.efficiency}%</span> - <span class="stat-detail">space utilized</span> + <span class="stat-detail">{$t('tools.subnet_planner.results.stats.efficiency.detail')}</span> </div> </div> </div> @@ -440,14 +472,14 @@ {#if showVisualization && result.visualization} <div class="visualization-section"> <div class="viz-header"> - <h4>Address Space Layout</h4> + <h4>{$t('tools.subnet_planner.results.visualization.title')}</h4> <button type="button" class="btn btn-secondary btn-xs" onclick={() => (showVisualization = !showVisualization)} > <Icon name="hide" size="xs" /> - Hide + {$t('tools.subnet_planner.results.visualization.hide')} </button> </div> @@ -474,11 +506,11 @@ <div class="viz-legend"> <div class="legend-item"> <div class="legend-color allocated-color"></div> - <span>Allocated Subnets</span> + <span>{$t('tools.subnet_planner.results.visualization.legend.allocated')}</span> </div> <div class="legend-item"> <div class="legend-color leftover-color"></div> - <span>Leftover Space</span> + <span>{$t('tools.subnet_planner.results.visualization.legend.leftover')}</span> </div> </div> </div> @@ -486,19 +518,28 @@ <!-- Allocation Table --> <div class="allocations-section"> - <h4>Allocated Subnets ({result.allocated.length})</h4> + <h4>{$t('tools.subnet_planner.results.allocations.title', { count: result.allocated.length })}</h4> <div class="allocations-table"> <div class="table-header"> - <div class="col-name" use:tooltip={'User-defined name for the subnet'}>Name</div> - <div class="col-cidr" use:tooltip={'Network address in CIDR notation'}>CIDR</div> - <div class="col-range" use:tooltip={'Network/broadcast range and usable host addresses'}> - Address Range + <div class="col-name" use:tooltip={$t('tools.subnet_planner.results.allocations.table.name.tooltip')}> + {$t('tools.subnet_planner.results.allocations.table.name.label')} + </div> + <div class="col-cidr" use:tooltip={$t('tools.subnet_planner.results.allocations.table.cidr.tooltip')}> + {$t('tools.subnet_planner.results.allocations.table.cidr.label')} </div> - <div class="col-hosts" use:tooltip={'Number of usable and total host addresses'}>Hosts</div> - <div class="col-efficiency" use:tooltip={'How well the allocated subnet matches the requested size'}> - Efficiency + <div class="col-range" use:tooltip={$t('tools.subnet_planner.results.allocations.table.range.tooltip')}> + {$t('tools.subnet_planner.results.allocations.table.range.label')} </div> - <div class="col-actions">Actions</div> + <div class="col-hosts" use:tooltip={$t('tools.subnet_planner.results.allocations.table.hosts.tooltip')}> + {$t('tools.subnet_planner.results.allocations.table.hosts.label')} + </div> + <div + class="col-efficiency" + use:tooltip={$t('tools.subnet_planner.results.allocations.table.efficiency.tooltip')} + > + {$t('tools.subnet_planner.results.allocations.table.efficiency.label')} + </div> + <div class="col-actions">{$t('tools.subnet_planner.results.allocations.table.actions')}</div> </div> {#each result.allocated as subnet (subnet.cidr)} @@ -515,20 +556,31 @@ {subnet.network} - {subnet.broadcast} </div> <div class="host-range"> - Hosts: {subnet.firstHost} - {subnet.lastHost} + {$t('tools.subnet_planner.results.allocations.table.hostsRange', { + first: subnet.firstHost, + last: subnet.lastHost, + })} </div> </div> </div> <div class="col-hosts"> <div class="hosts-info"> - <div class="usable-hosts">{subnet.usableHosts} usable</div> - <div class="total-hosts">{subnet.totalHosts} total</div> + <div class="usable-hosts"> + {$t('tools.subnet_planner.results.allocations.table.usableHosts', { count: subnet.usableHosts })} + </div> + <div class="total-hosts"> + {$t('tools.subnet_planner.results.allocations.table.totalHosts', { count: subnet.totalHosts })} + </div> </div> </div> <div class="col-efficiency"> <div class="efficiency-info"> <span class="efficiency-percent">{subnet.efficiency}%</span> - <span class="requested-size">({subnet.requestedSize} req.)</span> + <span class="requested-size" + >({$t('tools.subnet_planner.results.allocations.table.requested', { + count: subnet.requestedSize, + })})</span + > </div> </div> <div class="col-actions"> @@ -549,12 +601,14 @@ <!-- Leftover Space --> {#if result.leftover.length > 0} <div class="leftover-section"> - <h4>Leftover Space ({result.leftover.length} blocks)</h4> + <h4>{$t('tools.subnet_planner.results.leftover.title', { count: result.leftover.length })}</h4> <div class="leftover-grid"> {#each result.leftover as block, index (index)} <div class="leftover-card"> <code class="leftover-cidr">{block.cidr}</code> - <span class="leftover-size">{block.size} addresses</span> + <span class="leftover-size" + >{$t('tools.subnet_planner.results.leftover.size', { count: block.size })}</span + > </div> {/each} </div> @@ -562,12 +616,12 @@ {/if} {:else} <div class="info-panel info"> - <h3>No Allocations</h3> - <p>Unable to allocate any subnets. Check that:</p> + <h3>{$t('tools.subnet_planner.results.noAllocations.title')}</h3> + <p>{$t('tools.subnet_planner.results.noAllocations.message')}</p> <ul> - <li>Parent CIDR is valid</li> - <li>Requested subnet sizes are reasonable</li> - <li>There's sufficient address space</li> + <li>{$t('tools.subnet_planner.results.noAllocations.checks.validCIDR')}</li> + <li>{$t('tools.subnet_planner.results.noAllocations.checks.reasonableSizes')}</li> + <li>{$t('tools.subnet_planner.results.noAllocations.checks.sufficientSpace')}</li> </ul> </div> {/if} diff --git a/src/lib/components/tools/SupernetCalculator.svelte b/src/lib/components/tools/SupernetCalculator.svelte index cce774c8..a1d45d73 100644 --- a/src/lib/components/tools/SupernetCalculator.svelte +++ b/src/lib/components/tools/SupernetCalculator.svelte @@ -12,6 +12,7 @@ import { tooltip } from '$lib/actions/tooltip.js'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t } from '$lib/stores/language'; let networks = $state<NetworkInput[]>([]); let supernetResult = $state<SupernetResult | null>(null); @@ -166,8 +167,8 @@ </script> <ToolContentContainer - title="Supernet Calculator" - description="Aggregate multiple networks into a single supernet for route summarization and efficient routing table management." + title={$t('tools/supernet-calculator.title')} + description={$t('tools/supernet-calculator.description')} contentClass="supernet-calc-car" > <!-- Quick Examples --> @@ -175,7 +176,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Quick Examples</h3> + <h3>{$t('tools/supernet-calculator.examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example (example.label)} @@ -184,15 +185,18 @@ onclick={() => loadExample(example)} > <div class="example-header"> - <div class="example-label">{example.label}</div> + <div class="example-label">{$t(`tools/supernet-calculator.examples.${example.type}.label`)}</div> <div class="example-type {example.type}"> - {example.networks.length} Networks + {$t('tools/supernet-calculator.examples.networks', { count: example.networks.length })} </div> </div> <code class="example-input"> - {example.networks[0].network}/{example.networks[0].cidr} + {example.networks.length - 1} more + {example.networks[0].network}/{example.networks[0].cidr} + {$t('tools/supernet-calculator.examples.moreNetworks', { count: example.networks.length - 1 })} </code> - <div class="example-description">{example.description}</div> + <div class="example-description"> + {$t(`tools/supernet-calculator.examples.${example.type}.description`)} + </div> </button> {/each} </div> @@ -202,10 +206,10 @@ <!-- Network Inputs --> <div class="network-inputs"> <div class="inputs-header"> - <h3>Input Networks</h3> + <h3>{$t('tools/supernet-calculator.input.title')}</h3> <button type="button" class="btn btn-primary" onclick={addNetwork}> <Icon name="plus" size="sm" /> - Add Network + {$t('tools/supernet-calculator.input.addNetwork')} </button> </div> @@ -219,7 +223,7 @@ <IPInput bind:value={network.network} placeholder="192.168.1.0" /> </div> <div class="cidr-input"> - <label for="cidr-{index}">CIDR</label> + <label for="cidr-{index}">{$t('tools/supernet-calculator.input.cidr')}</label> <div class="cidr-controls"> <span class="cidr-display">/{network.cidr}</span> <input @@ -247,7 +251,7 @@ <div class="network-description"> <input type="text" - placeholder="Description (optional)" + placeholder={$t('tools/supernet-calculator.input.descriptionPlaceholder')} bind:value={network.description} class="description-input" /> @@ -260,12 +264,12 @@ <!-- Aggregation Analysis --> {#if aggregationAnalysis && networks.filter((n) => n.network.trim()).length > 1} <div class="analysis-section"> - <h3>Aggregation Analysis</h3> + <h3>{$t('tools/supernet-calculator.analysis.title')}</h3> <div class="analysis-card"> <div class="analysis-header"> <div class="analysis-stat"> - <span class="stat-label" use:tooltip={'How efficiently the networks can be aggregated - higher is better'} - >Aggregation Efficiency</span + <span class="stat-label" use:tooltip={$t('tools/supernet-calculator.analysis.efficiencyTooltip')} + >{$t('tools/supernet-calculator.analysis.efficiency')}</span > <span class="stat-value" style="color: {getEfficiencyColor(aggregationAnalysis.efficiency)}"> {aggregationAnalysis.efficiency.toFixed(1)}% @@ -273,13 +277,15 @@ </div> <div class="analysis-status" class:can-aggregate={aggregationAnalysis.canAggregate}> <Icon name={aggregationAnalysis.canAggregate ? 'check-circle' : 'alert-triangle'} size="sm" /> - {aggregationAnalysis.canAggregate ? 'Can Aggregate' : 'Limited Aggregation'} + {aggregationAnalysis.canAggregate + ? $t('tools/supernet-calculator.analysis.canAggregate') + : $t('tools/supernet-calculator.analysis.limitedAggregation')} </div> </div> {#if aggregationAnalysis.recommendations.length > 0} <div class="recommendations"> - <h4>Recommendations</h4> + <h4>{$t('tools/supernet-calculator.analysis.recommendations')}</h4> <ul> {#each aggregationAnalysis.recommendations as recommendation, index (index)} <li>{recommendation}</li> @@ -297,13 +303,11 @@ {#if supernetResult.success && supernetResult.supernet} <!-- Summary Statistics --> <div class="info-panel success"> - <h3>Supernet Summary</h3> + <h3>{$t('tools/supernet-calculator.summary.title')}</h3> <div class="summary-grid"> <div class="summary-item"> - <span - class="summary-label" - use:tooltip={'The aggregated network address that encompasses all input networks'} - >Supernet Address</span + <span class="summary-label" use:tooltip={$t('tools/supernet-calculator.summary.addressTooltip')} + >{$t('tools/supernet-calculator.summary.address')}</span > <div class="value-copy"> <span class="ip-value success">{supernetResult.supernet.network}/{supernetResult.supernet.cidr}</span> @@ -319,8 +323,8 @@ </div> </div> <div class="summary-item"> - <span class="summary-label" use:tooltip={'Total number of host addresses available in the supernet'} - >Total Hosts</span + <span class="summary-label" use:tooltip={$t('tools/supernet-calculator.summary.totalHostsTooltip')} + >{$t('tools/supernet-calculator.summary.totalHosts')}</span > <span class="summary-value">{formatNumber(supernetResult.supernet.totalHosts)}</span> </div> @@ -330,28 +334,32 @@ <!-- Route Aggregation Savings --> {#if supernetResult.savingsAnalysis} <div class="info-panel info"> - <h3>Route Aggregation Benefits</h3> + <h3>{$t('tools/supernet-calculator.benefits.title')}</h3> <div class="savings-grid"> <div class="savings-item"> - <span class="savings-label" use:tooltip={'Number of individual routes before aggregation'} - >Original Routes</span + <span class="savings-label" use:tooltip={$t('tools/supernet-calculator.benefits.originalRoutesTooltip')} + >{$t('tools/supernet-calculator.benefits.originalRoutes')}</span > <span class="savings-value">{supernetResult.savingsAnalysis.originalRoutes}</span> </div> <div class="savings-item"> - <span class="savings-label" use:tooltip={'Number of routes after supernet aggregation'} - >Aggregated Routes</span + <span + class="savings-label" + use:tooltip={$t('tools/supernet-calculator.benefits.aggregatedRoutesTooltip')} + >{$t('tools/supernet-calculator.benefits.aggregatedRoutes')}</span > <span class="savings-value success">{supernetResult.savingsAnalysis.aggregatedRoutes}</span> </div> <div class="savings-item"> - <span class="savings-label" use:tooltip={'Number of routes eliminated through aggregation'} - >Routes Saved</span + <span class="savings-label" use:tooltip={$t('tools/supernet-calculator.benefits.routesSavedTooltip')} + >{$t('tools/supernet-calculator.benefits.routesSaved')}</span > <span class="savings-value success">{supernetResult.savingsAnalysis.routeReduction}</span> </div> <div class="savings-item"> - <span class="savings-label" use:tooltip={'Percentage reduction in routing table size'}>Reduction</span> + <span class="savings-label" use:tooltip={$t('tools/supernet-calculator.benefits.reductionTooltip')} + >{$t('tools/supernet-calculator.benefits.reduction')}</span + > <span class="savings-value success" >{supernetResult.savingsAnalysis.reductionPercentage.toFixed(1)}%</span > @@ -363,16 +371,14 @@ <!-- Detailed Information --> <div class="details-section"> <div class="details-header"> - <h3>Supernet Details</h3> + <h3>{$t('tools/supernet-calculator.details.title')}</h3> </div> <div class="details-grid"> <div class="detail-item"> <div class="detail-label-wrapper"> - <span - class="detail-label" - use:tooltip={'The first IP address in the supernet that identifies the network itself'} - >Network Address</span + <span class="detail-label" use:tooltip={$t('tools/supernet-calculator.details.networkAddressTooltip')} + >{$t('tools/supernet-calculator.details.networkAddress')}</span > </div> <div class="value-copy"> @@ -389,10 +395,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span - class="detail-label" - use:tooltip={'Defines which portion of the IP address represents the network vs host bits'} - >Subnet Mask</span + <span class="detail-label" use:tooltip={$t('tools/supernet-calculator.details.subnetMaskTooltip')} + >{$t('tools/supernet-calculator.details.subnetMask')}</span > </div> <div class="value-copy"> @@ -409,10 +413,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span - class="detail-label" - use:tooltip={'Inverse of subnet mask, used in access control lists and routing protocols'} - >Wildcard Mask</span + <span class="detail-label" use:tooltip={$t('tools/supernet-calculator.details.wildcardMaskTooltip')} + >{$t('tools/supernet-calculator.details.wildcardMask')}</span > </div> <div class="value-copy"> @@ -430,10 +432,8 @@ <div class="detail-item"> <div class="detail-label-wrapper"> - <span - class="detail-label" - use:tooltip={'First and last usable IP addresses in the supernet (excluding network and broadcast)'} - >Address Range</span + <span class="detail-label" use:tooltip={$t('tools/supernet-calculator.details.addressRangeTooltip')} + >{$t('tools/supernet-calculator.details.addressRange')}</span > </div> <div class="value-copy"> @@ -457,10 +457,8 @@ <div class="detail-item full-width"> <div class="detail-label-wrapper"> - <span - class="detail-label" - use:tooltip={'Binary representation of the subnet mask showing network (1) and host (0) bits'} - >Binary Subnet Mask</span + <span class="detail-label" use:tooltip={$t('tools/supernet-calculator.details.binaryMaskTooltip')} + >{$t('tools/supernet-calculator.details.binaryMask')}</span > </div> <div class="value-copy"> @@ -481,16 +479,16 @@ <!-- Network Visualization --> {#if showVisualization} <div class="visualization-section"> - <h3>Network Visualization</h3> + <h3>{$t('tools/supernet-calculator.visualization.title')}</h3> <div class="visualization-card"> <div class="visualization-header"> - <h4>Input Networks vs Supernet</h4> - <p>Visual representation of how individual networks are aggregated into a supernet</p> + <h4>{$t('tools/supernet-calculator.visualization.heading')}</h4> + <p>{$t('tools/supernet-calculator.visualization.description')}</p> </div> <div class="network-diagram"> <div class="input-networks"> - <h5>Input Networks</h5> + <h5>{$t('tools/supernet-calculator.visualization.inputNetworks')}</h5> {#each supernetResult.inputNetworks as network, index (network.id)} <div class="network-visual"> <div class="network-bar" style="--network-index: {index}"> @@ -505,14 +503,18 @@ <div class="aggregation-arrow"> <Icon name="arrow-down" size="lg" /> - <span>Aggregates to</span> + <span>{$t('tools/supernet-calculator.visualization.aggregatesTo')}</span> </div> <div class="supernet-visual"> - <h5>Supernet</h5> + <h5>{$t('tools/supernet-calculator.visualization.supernet')}</h5> <div class="supernet-bar"> <span class="supernet-label">{supernetResult.supernet.network}/{supernetResult.supernet.cidr}</span> - <span class="supernet-hosts">{formatNumber(supernetResult.supernet.totalHosts)} hosts</span> + <span class="supernet-hosts" + >{$t('tools/supernet-calculator.visualization.hosts', { + count: formatNumber(supernetResult.supernet.totalHosts), + })}</span + > </div> </div> </div> @@ -522,7 +524,7 @@ {:else} <!-- Error Display --> <div class="info-panel error"> - <h3>Calculation Error</h3> + <h3>{$t('tools/supernet-calculator.error.title')}</h3> <p class="error-message">{supernetResult.error}</p> </div> {/if} diff --git a/src/lib/components/tools/TLSAGenerator.svelte b/src/lib/components/tools/TLSAGenerator.svelte index 8afc560b..b6037b2f 100644 --- a/src/lib/components/tools/TLSAGenerator.svelte +++ b/src/lib/components/tools/TLSAGenerator.svelte @@ -1,7 +1,15 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; import Icon from '$lib/components/global/Icon.svelte'; import { tooltip } from '$lib/actions/tooltip'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let domain = $state('example.com'); let port = $state(443); @@ -18,23 +26,23 @@ let selectedExample = $state<string | null>(null); const clipboard = useClipboard(); - const usageDescriptions = { - 0: 'CA Constraint - Certificate must be issued by the CA represented in the TLSA record', - 1: 'Service Certificate Constraint - Certificate must match the one in the TLSA record', - 2: 'Trust Anchor Assertion - Certificate must chain to the CA in the TLSA record', - 3: 'Domain-Issued Certificate - Certificate must match the one specified (most common)', - }; + const usageDescriptions = $derived({ + 0: $t('tools.tlsa_generator.parameters.usage.descriptions.caConstraint'), + 1: $t('tools.tlsa_generator.parameters.usage.descriptions.serviceConstraint'), + 2: $t('tools.tlsa_generator.parameters.usage.descriptions.trustAnchor'), + 3: $t('tools.tlsa_generator.parameters.usage.descriptions.domainIssued'), + }); - const selectorDescriptions = { - 0: 'Full Certificate - Use the entire certificate', - 1: 'Subject Public Key Info - Use only the public key portion (recommended)', - }; + const selectorDescriptions = $derived({ + 0: $t('tools.tlsa_generator.parameters.selector.descriptions.fullCert'), + 1: $t('tools.tlsa_generator.parameters.selector.descriptions.spki'), + }); - const matchingTypeDescriptions = { - 0: 'Exact Match - Use the certificate/key data as-is (not recommended)', - 1: 'SHA-256 Hash - Use SHA-256 hash of the certificate/key (recommended)', - 2: 'SHA-512 Hash - Use SHA-512 hash of the certificate/key', - }; + const matchingTypeDescriptions = $derived({ + 0: $t('tools.tlsa_generator.parameters.matching.descriptions.exact'), + 1: $t('tools.tlsa_generator.parameters.matching.descriptions.sha256'), + 2: $t('tools.tlsa_generator.parameters.matching.descriptions.sha512'), + }); const tlsaRecord = $derived.by(() => { let associationData = ''; @@ -46,7 +54,7 @@ .toLowerCase(); } else if (inputType === 'certificate') { if (certificateInput.trim()) { - associationData = 'Generated hash would appear here (requires certificate parsing)'; + associationData = $t('tools.tlsa_generator.output.generated_hash_placeholder'); } } @@ -70,46 +78,46 @@ const errors = []; if (!domain.trim()) { - errors.push('Domain is required'); + errors.push($t('tools.tlsa_generator.validation.errors.domainRequired')); } else if (!domain.includes('.')) { - warnings.push('Domain should include TLD (e.g., .com, .org)'); + warnings.push($t('tools.tlsa_generator.validation.warnings.domainTld')); } if (port < 1 || port > 65535) { - errors.push('Port must be between 1 and 65535'); + errors.push($t('tools.tlsa_generator.validation.errors.portRange')); } if (inputType === 'certificate') { if (!certificateInput.trim()) { - errors.push('Certificate data is required'); + errors.push($t('tools.tlsa_generator.validation.errors.certificateRequired')); } else if (!certificateInput.includes('BEGIN CERTIFICATE') && !certificateInput.includes('BEGIN PUBLIC KEY')) { - warnings.push('Certificate should be in PEM format'); + warnings.push($t('tools.tlsa_generator.validation.warnings.pemFormat')); } } else if (inputType === 'hash') { if (!hashInput.trim()) { - errors.push('Hash value is required'); + errors.push($t('tools.tlsa_generator.validation.errors.hashRequired')); } else { const cleanHash = hashInput.trim().replace(/[^a-fA-F0-9]/g, ''); if (matchingType === 1 && cleanHash.length !== 64) { - warnings.push('SHA-256 hash should be exactly 64 hexadecimal characters'); + warnings.push($t('tools.tlsa_generator.validation.warnings.sha256Length')); } else if (matchingType === 2 && cleanHash.length !== 128) { - warnings.push('SHA-512 hash should be exactly 128 hexadecimal characters'); + warnings.push($t('tools.tlsa_generator.validation.warnings.sha512Length')); } else if (!/^[a-fA-F0-9]+$/.test(cleanHash)) { - errors.push('Hash must contain only hexadecimal characters'); + errors.push($t('tools.tlsa_generator.validation.errors.hexOnly')); } } } if (usage === 0 || usage === 2) { - warnings.push('Usage types 0 and 2 require careful CA certificate management'); + warnings.push($t('tools.tlsa_generator.validation.warnings.usageTypes02')); } if (selector === 0) { - warnings.push('Full certificate selector (0) is less flexible than SPKI selector (1)'); + warnings.push($t('tools.tlsa_generator.validation.warnings.selectorFull')); } if (matchingType === 0) { - warnings.push('Exact match (0) is not recommended - use SHA-256 (1) or SHA-512 (2)'); + warnings.push($t('tools.tlsa_generator.validation.warnings.exactMatch')); } return { @@ -151,10 +159,10 @@ } } - const exampleConfigurations = [ + const exampleConfigurations = $derived([ { - name: 'HTTPS Certificate Pin', - description: 'Pin a specific certificate for HTTPS', + name: $t('tools.tlsa_generator.examples.https.name'), + description: $t('tools.tlsa_generator.examples.https.description'), domain: 'example.com', port: 443, protocol: 'tcp', @@ -164,8 +172,8 @@ hash: 'a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890', }, { - name: 'SMTP TLS Certificate', - description: 'DANE for email server TLS', + name: $t('tools.tlsa_generator.examples.smtp.name'), + description: $t('tools.tlsa_generator.examples.smtp.description'), domain: 'mail.example.com', port: 587, protocol: 'tcp', @@ -175,8 +183,8 @@ hash: 'fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321', }, { - name: 'CA Trust Anchor', - description: 'Trust anchor for certificate authority', + name: $t('tools.tlsa_generator.examples.ca.name'), + description: $t('tools.tlsa_generator.examples.ca.description'), domain: 'secure.example.com', port: 443, protocol: 'tcp', @@ -185,7 +193,7 @@ matchingType: 2, hash: 'abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890', }, - ]; + ]); function loadExample(example: { name: string; @@ -210,22 +218,21 @@ selectedExample = example.name; } - const securityTips = [ - 'Use usage type 3 (Domain-Issued Certificate) for most scenarios', - 'Prefer selector 1 (SPKI) over selector 0 (full certificate) for flexibility', - 'Use SHA-256 (1) or SHA-512 (2) matching types, avoid exact match (0)', - 'Pin multiple certificates to avoid service disruption during certificate rotation', - 'Test TLSA records with DANE validation tools before deployment', - ]; + const securityTips = $derived([ + $t('tools.tlsa_generator.security.tips.usage3'), + $t('tools.tlsa_generator.security.tips.selectorSpki'), + $t('tools.tlsa_generator.security.tips.hashTypes'), + $t('tools.tlsa_generator.security.tips.multipleCerts'), + $t('tools.tlsa_generator.security.tips.testRecords'), + ]); </script> <div class="container"> <div class="card"> <div class="card-header"> - <h1>TLSA Generator</h1> + <h1>{$t('tools.tlsa_generator.title')}</h1> <p> - Create TLSA (DNS-based Authentication of Named Entities) records for certificate pinning and DANE - implementation. + {$t('tools.tlsa_generator.subtitle')} </p> </div> @@ -235,27 +242,43 @@ <div class="card sub-card"> <h3 class="section-title"> <Icon name="globe" size="sm" /> - Service Configuration + {$t('tools.tlsa_generator.service.title')} </h3> <div class="service-grid"> <div class="input-group"> - <label for="domain" use:tooltip={'Domain name for the TLSA record'}>Domain:</label> - <input id="domain" type="text" bind:value={domain} placeholder="example.com" /> + <label for="domain" use:tooltip={$t('tools.tlsa_generator.service.domain.tooltip')} + >{$t('tools.tlsa_generator.service.domain.label')}</label + > + <input + id="domain" + type="text" + bind:value={domain} + placeholder={$t('tools.tlsa_generator.service.domain.placeholder')} + /> </div> <div class="input-group"> - <label for="port" use:tooltip={'Port number for the service (e.g., 443 for HTTPS, 25 for SMTP)'} - >Port:</label + <label for="port" use:tooltip={$t('tools.tlsa_generator.service.port.tooltip')} + >{$t('tools.tlsa_generator.service.port.label')}</label > - <input id="port" type="number" bind:value={port} min="1" max="65535" placeholder="443" /> + <input + id="port" + type="number" + bind:value={port} + min="1" + max="65535" + placeholder={$t('tools.tlsa_generator.service.port.placeholder')} + /> </div> <div class="input-group"> - <label for="protocol" use:tooltip={'Protocol type (tcp or udp)'}>Protocol:</label> + <label for="protocol" use:tooltip={$t('tools.tlsa_generator.service.protocol.tooltip')} + >{$t('tools.tlsa_generator.service.protocol.label')}</label + > <select id="protocol" bind:value={protocol}> - <option value="tcp">TCP</option> - <option value="udp">UDP</option> + <option value="tcp">{$t('tools.tlsa_generator.service.protocol.options.tcp')}</option> + <option value="udp">{$t('tools.tlsa_generator.service.protocol.options.udp')}</option> </select> </div> </div> @@ -265,37 +288,41 @@ <div class="card sub-card"> <h3 class="section-title"> <Icon name="settings" size="sm" /> - TLSA Parameters + {$t('tools.tlsa_generator.parameters.title')} </h3> <div class="input-group"> - <label for="usage" use:tooltip={'Certificate usage - how the certificate should be used for authentication'} - >Certificate Usage:</label + <label for="usage" use:tooltip={$t('tools.tlsa_generator.parameters.usage.tooltip')} + >{$t('tools.tlsa_generator.parameters.usage.label')}</label > <select id="usage" bind:value={usage}> - <option value={0}>0 - CA Constraint</option> - <option value={1}>1 - Service Certificate Constraint</option> - <option value={2}>2 - Trust Anchor Assertion</option> - <option value={3}>3 - Domain-Issued Certificate</option> + <option value={0}>{$t('tools.tlsa_generator.parameters.usage.options.caConstraint')}</option> + <option value={1}>{$t('tools.tlsa_generator.parameters.usage.options.serviceConstraint')}</option> + <option value={2}>{$t('tools.tlsa_generator.parameters.usage.options.trustAnchor')}</option> + <option value={3}>{$t('tools.tlsa_generator.parameters.usage.options.domainIssued')}</option> </select> <p class="description">{usageDescriptions[usage as keyof typeof usageDescriptions]}</p> </div> <div class="input-group"> - <label for="selector" use:tooltip={'Which part of the certificate to use'}>Selector:</label> + <label for="selector" use:tooltip={$t('tools.tlsa_generator.parameters.selector.tooltip')} + >{$t('tools.tlsa_generator.parameters.selector.label')}</label + > <select id="selector" bind:value={selector}> - <option value={0}>0 - Full Certificate</option> - <option value={1}>1 - Subject Public Key Info</option> + <option value={0}>{$t('tools.tlsa_generator.parameters.selector.options.fullCert')}</option> + <option value={1}>{$t('tools.tlsa_generator.parameters.selector.options.spki')}</option> </select> <p class="description">{selectorDescriptions[selector as keyof typeof selectorDescriptions]}</p> </div> <div class="input-group"> - <label for="matchingType" use:tooltip={'How to process the certificate data'}>Matching Type:</label> + <label for="matchingType" use:tooltip={$t('tools.tlsa_generator.parameters.matching.tooltip')} + >{$t('tools.tlsa_generator.parameters.matching.label')}</label + > <select id="matchingType" bind:value={matchingType}> - <option value={0}>0 - Exact Match</option> - <option value={1}>1 - SHA-256 Hash</option> - <option value={2}>2 - SHA-512 Hash</option> + <option value={0}>{$t('tools.tlsa_generator.parameters.matching.options.exact')}</option> + <option value={1}>{$t('tools.tlsa_generator.parameters.matching.options.sha256')}</option> + <option value={2}>{$t('tools.tlsa_generator.parameters.matching.options.sha512')}</option> </select> <p class="description">{matchingTypeDescriptions[matchingType as keyof typeof matchingTypeDescriptions]}</p> </div> @@ -305,24 +332,24 @@ <div class="card sub-card"> <h3 class="section-title"> <Icon name="key" size="sm" /> - Certificate Data + {$t('tools.tlsa_generator.certificate.title')} </h3> <div class="radio-group"> <label class="radio-option"> <input type="radio" bind:group={inputType} value="certificate" /> - <span>Certificate/Public Key (PEM)</span> + <span>{$t('tools.tlsa_generator.certificate.pemOption')}</span> </label> <label class="radio-option"> <input type="radio" bind:group={inputType} value="hash" /> - <span>Hash Value</span> + <span>{$t('tools.tlsa_generator.certificate.hashOption')}</span> </label> </div> {#if inputType === 'certificate'} <div class="input-group"> - <label for="certificate" use:tooltip={'Paste the PEM-encoded certificate or public key'} - >Certificate/Public Key:</label + <label for="certificate" use:tooltip={$t('tools.tlsa_generator.certificate.pemTooltip')} + >{$t('tools.tlsa_generator.certificate.pemLabel')}</label > <textarea id="certificate" @@ -335,18 +362,24 @@ onclick={generateHashFromInput} disabled={!certificateInput.trim()} class="btn btn-secondary" - use:tooltip={'Generate hash from certificate (demo mode)'} + use:tooltip={$t('tools.tlsa_generator.certificate.generateTooltip')} > <Icon name="arrow-right" size="sm" /> - Generate Hash + {$t('tools.tlsa_generator.certificate.generateButton')} </button> </div> {:else} <div class="input-group"> <label for="hash" - use:tooltip={`Enter the ${matchingType === 1 ? 'SHA-256' : matchingType === 2 ? 'SHA-512' : 'exact'} hash value`} - >Hash Value:</label + use:tooltip={$t('tools.tlsa_generator.certificate.hashTooltip', { + type: + matchingType === 1 + ? 'SHA-256' + : matchingType === 2 + ? 'SHA-512' + : $t('tools.tlsa_generator.certificate.exact'), + })}>{$t('tools.tlsa_generator.certificate.hashLabel')}</label > <textarea id="hash" @@ -368,27 +401,31 @@ <!-- Generated Record --> <div class="card"> <div class="card-header-with-actions"> - <h3>Generated TLSA Record</h3> + <h3>{$t('tools.tlsa_generator.output.title')}</h3> <div class="actions"> <button type="button" class="btn btn-primary" class:success={clipboard.isCopied('copy-tlsa')} onclick={() => copyToClipboard(dnsRecord, 'copy-tlsa')} - use:tooltip={'Copy TLSA record to clipboard'} + use:tooltip={$t('tools.tlsa_generator.output.copy.tooltip')} > <Icon name={clipboard.isCopied('copy-tlsa') ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied('copy-tlsa') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('copy-tlsa') + ? $t('tools.tlsa_generator.output.copy.copied') + : $t('tools.tlsa_generator.output.copy.button')} </button> <button type="button" class="btn btn-success" class:success={clipboard.isCopied('export-tlsa')} onclick={exportAsZoneFile} - use:tooltip={'Download as zone file'} + use:tooltip={$t('tools.tlsa_generator.output.export.tooltip')} > <Icon name={clipboard.isCopied('export-tlsa') ? 'check' : 'download'} size="sm" /> - {clipboard.isCopied('export-tlsa') ? 'Downloaded!' : 'Export'} + {clipboard.isCopied('export-tlsa') + ? $t('tools.tlsa_generator.output.export.downloaded') + : $t('tools.tlsa_generator.output.export.button')} </button> </div> </div> @@ -398,21 +435,21 @@ </div> <div class="breakdown"> - <h4>Record Breakdown:</h4> + <h4>{$t('tools.tlsa_generator.output.breakdown.title')}</h4> <div class="breakdown-grid"> <div class="breakdown-item"> - <strong>Service:</strong> _{port}._{protocol} + <strong>{$t('tools.tlsa_generator.output.breakdown.service')}</strong> _{port}._{protocol} </div> <div class="breakdown-item"> - <strong>Usage:</strong> + <strong>{$t('tools.tlsa_generator.output.breakdown.usage')}</strong> {usage} ({Object.values(usageDescriptions)[usage].split(' - ')[0]}) </div> <div class="breakdown-item"> - <strong>Selector:</strong> + <strong>{$t('tools.tlsa_generator.output.breakdown.selector')}</strong> {selector} ({Object.values(selectorDescriptions)[selector].split(' - ')[0]}) </div> <div class="breakdown-item"> - <strong>Matching:</strong> + <strong>{$t('tools.tlsa_generator.output.breakdown.matching')}</strong> {matchingType} ({Object.values(matchingTypeDescriptions)[matchingType].split(' - ')[0]}) </div> </div> @@ -424,14 +461,16 @@ <div class="card"> <h3 class="section-title"> <Icon name="bar-chart" size="sm" /> - Validation + {$t('tools.tlsa_generator.validation.title')} </h3> <div class="status-center"> <div class="status-item"> - <span>Status:</span> + <span>{$t('tools.tlsa_generator.validation.status.label')}</span> <span class="status" class:valid={validation.isValid} class:invalid={!validation.isValid}> - {validation.isValid ? 'Valid' : 'Invalid'} + {validation.isValid + ? $t('tools.tlsa_generator.validation.status.valid') + : $t('tools.tlsa_generator.validation.status.invalid')} </span> </div> </div> @@ -461,7 +500,7 @@ {#if validation.isValid && validation.errors.length === 0 && validation.warnings.length === 0} <div class="message success"> <Icon name="check-circle" size="sm" /> - <div>TLSA record is valid and ready to deploy!</div> + <div>{$t('tools.tlsa_generator.validation.readyToDeploy')}</div> </div> {/if} </div> @@ -470,7 +509,7 @@ <div class="card"> <h3 class="section-title"> <Icon name="shield" size="sm" /> - Security Best Practices + {$t('tools.tlsa_generator.security.title')} </h3> <ul class="tips-list"> @@ -487,7 +526,7 @@ <details bind:open={showExamples}> <summary class="examples-summary"> <Icon name="lightbulb" size="sm" /> - Example Configurations + {$t('tools.tlsa_generator.examples.title')} <span class="chevron"><Icon name="chevron-down" size="sm" /></span> </summary> <div class="examples-grid"> @@ -501,9 +540,13 @@ <div class="example-name">{example.name}</div> <p class="example-description">{example.description}</p> <div class="example-config"> - <div>Port: <code>{example.port}/{example.protocol}</code></div> <div> - Usage: <code>{example.usage}</code>, Selector: <code>{example.selector}</code>, Type: + {$t('tools.tlsa_generator.examples.config.port')}: <code>{example.port}/{example.protocol}</code> + </div> + <div> + {$t('tools.tlsa_generator.examples.config.usage')}: <code>{example.usage}</code>, {$t( + 'tools.tlsa_generator.examples.config.selector', + )}: <code>{example.selector}</code>, {$t('tools.tlsa_generator.examples.config.type')}: <code>{example.matchingType}</code> </div> </div> diff --git a/src/lib/components/tools/TTLCalculator.svelte b/src/lib/components/tools/TTLCalculator.svelte index 324dc594..d7c02bf6 100644 --- a/src/lib/components/tools/TTLCalculator.svelte +++ b/src/lib/components/tools/TTLCalculator.svelte @@ -4,6 +4,13 @@ import { humanizeTTL, calculateCacheExpiry, type TTLInfo } from '$lib/utils/dns-validation.js'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools/ttl-calculator'); + }); let ttlInput = $state('3600'); let customDate = $state(''); @@ -37,23 +44,23 @@ const examples = [ { ttl: '300', - scenario: 'Load Balancer IP', - description: 'Short TTL for quick failover capability', + scenario: $t('examples.useCases.scenarios.loadBalancer'), + description: $t('examples.useCases.descriptions.loadBalancer'), }, { ttl: '3600', - scenario: 'Web Server A Record', - description: 'Standard TTL for web services', + scenario: $t('examples.useCases.scenarios.webServer'), + description: $t('examples.useCases.descriptions.webServer'), }, { ttl: '86400', - scenario: 'MX Record', - description: 'Stable mail server configuration', + scenario: $t('examples.useCases.scenarios.mxRecord'), + description: $t('examples.useCases.descriptions.mxRecord'), }, { ttl: '604800', - scenario: 'NS Record', - description: 'Authoritative name servers rarely change', + scenario: $t('examples.useCases.scenarios.nsRecord'), + description: $t('examples.useCases.descriptions.nsRecord'), }, ]; @@ -143,8 +150,8 @@ <div class="card"> <header class="card-header"> - <h1>TTL Calculator</h1> - <p>Humanize DNS TTL values and compute cache expiry times from now or specific dates</p> + <h1>{$t('title')}</h1> + <p>{$t('description')}</p> </header> <!-- Educational Overview --> @@ -153,19 +160,22 @@ <div class="overview-item"> <Icon name="clock" size="sm" /> <div> - <strong>TTL Humanization:</strong> Convert seconds to human-readable formats like "2 hours" or "1 day". + <strong>{$t('overview.humanization.title')}</strong> + {$t('overview.humanization.content')} </div> </div> <div class="overview-item"> <Icon name="calendar" size="sm" /> <div> - <strong>Cache Expiry:</strong> Calculate when DNS records will expire from resolver caches. + <strong>{$t('overview.cacheExpiry.title')}</strong> + {$t('overview.cacheExpiry.content')} </div> </div> <div class="overview-item"> <Icon name="target" size="sm" /> <div> - <strong>TTL Guidelines:</strong> Get recommendations based on record stability and use case. + <strong>{$t('overview.guidelines.title')}</strong> + {$t('overview.guidelines.content')} </div> </div> </div> @@ -176,7 +186,7 @@ <details class="common-details"> <summary class="common-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Common TTL Values</h4> + <h4>{$t('examples.commonValues.title')}</h4> </summary> <div class="ttls-grid"> {#each commonTTLs as ttl, index (ttl.seconds)} @@ -198,7 +208,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>TTL by Use Case</h4> + <h4>{$t('examples.useCases.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, index (example.scenario)} @@ -219,16 +229,16 @@ <div class="card input-card"> <!-- TTL Input --> <div class="input-group"> - <label for="ttl-input" use:tooltip={'Enter TTL value in seconds'}> + <label for="ttl-input" use:tooltip={$t('input.tooltip')}> <Icon name="clock" size="sm" /> - TTL (seconds) + {$t('input.label')} </label> <input id="ttl-input" type="number" bind:value={ttlInput} oninput={handleInputChange} - placeholder="3600" + placeholder={$t('input.placeholder')} class="ttl-input" min="0" max="2147483647" @@ -239,7 +249,7 @@ <div class="input-group"> <label class="checkbox-label"> <input type="checkbox" class="styled-checkbox" bind:checked={useCustomDate} onchange={handleInputChange} /> - Calculate expiry from custom date/time + {$t('input.customDateLabel')} </label> {#if useCustomDate} @@ -257,10 +267,10 @@ {#if results} <div class="card results-card"> <div class="results-header"> - <h3>TTL Analysis</h3> + <h3>{$t('results.title')}</h3> <button class="copy-button {clipboard.isCopied() ? 'copied' : ''}" onclick={() => clipboard.copy(ttlInput)}> <Icon name={clipboard.isCopied() ? 'check' : 'copy'} size="sm" /> - Copy TTL + {$t('results.copyTTL')} </button> </div> @@ -273,20 +283,20 @@ </div> <div class="ttl-seconds-display"> <span class="seconds-value">{formatNumber(results.ttlInfo.seconds)}</span> - <span class="seconds-label">seconds</span> + <span class="seconds-label">{$t('results.secondsLabel')}</span> </div> </div> </div> <!-- Cache Expiry Times --> <div class="expiry-section"> - <h4>Cache Expiry Times</h4> + <h4>{$t('results.cacheExpiry')}</h4> <div class="expiry-cards"> <div class="expiry-card"> <div class="expiry-label"> <Icon name="clock" size="sm" /> - From Now + {$t('results.fromNow')} </div> <div class="expiry-time">{formatDateTime(results.expiryFromNow)}</div> <div class="expiry-relative">{formatRelativeTime(results.expiryFromNow)}</div> @@ -296,7 +306,7 @@ <div class="expiry-card"> <div class="expiry-label"> <Icon name="calendar" size="sm" /> - From Custom Date + {$t('results.fromCustomDate')} </div> <div class="expiry-time">{formatDateTime(results.expiryFromCustom)}</div> <div class="expiry-relative">{formatRelativeTime(results.expiryFromCustom)}</div> @@ -307,7 +317,7 @@ <!-- Recommendations --> <div class="recommendations-section"> - <h4>Summary</h4> + <h4>{$t('results.summary')}</h4> <ul class="recommendations-list"> {#each results.ttlInfo.recommendations as recommendation, index (index)} <li class="recommendation-item">{recommendation}</li> @@ -317,27 +327,27 @@ <!-- TTL Guidelines --> <div class="guidelines-section"> - <h4>TTL Guidelines by Category</h4> + <h4>{$t('results.guidelinesTitle')}</h4> <div class="guidelines-grid"> <div class="guideline-item"> - <div class="guideline-category very-short">Very Short (< 5 min)</div> - <div class="guideline-text">High DNS load, instant propagation</div> + <div class="guideline-category very-short">{$t('guidelines.veryShort.label')}</div> + <div class="guideline-text">{$t('guidelines.veryShort.description')}</div> </div> <div class="guideline-item"> - <div class="guideline-category short">Short (5 min - 1 hr)</div> - <div class="guideline-text">Frequent changes, good for testing</div> + <div class="guideline-category short">{$t('guidelines.short.label')}</div> + <div class="guideline-text">{$t('guidelines.short.description')}</div> </div> <div class="guideline-item"> - <div class="guideline-category medium">Medium (1 hr - 1 day)</div> - <div class="guideline-text">Balanced performance and flexibility</div> + <div class="guideline-category medium">{$t('guidelines.medium.label')}</div> + <div class="guideline-text">{$t('guidelines.medium.description')}</div> </div> <div class="guideline-item"> - <div class="guideline-category long">Long (1 day - 1 week)</div> - <div class="guideline-text">Stable records, reduced DNS queries</div> + <div class="guideline-category long">{$t('guidelines.long.label')}</div> + <div class="guideline-text">{$t('guidelines.long.description')}</div> </div> <div class="guideline-item"> - <div class="guideline-category very-long">Very Long (> 1 week)</div> - <div class="guideline-text">Infrastructure, rarely changes</div> + <div class="guideline-category very-long">{$t('guidelines.veryLong.label')}</div> + <div class="guideline-text">{$t('guidelines.veryLong.description')}</div> </div> </div> </div> @@ -348,34 +358,30 @@ <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>TTL Trade-offs</h4> + <h4>{$t('education.tradeoffs.title')}</h4> <p> - Lower TTLs allow faster propagation of DNS changes but increase DNS query load. Higher TTLs reduce DNS traffic - but slow down change propagation. Balance based on your needs. + {$t('education.tradeoffs.content')} </p> </div> <div class="education-item info-panel"> - <h4>Cache Behavior</h4> + <h4>{$t('education.cacheBehavior.title')}</h4> <p> - DNS resolvers cache records for the TTL duration. Once expired, they must query authoritative servers again. - Some resolvers may cache slightly longer or shorter than the exact TTL. + {$t('education.cacheBehavior.content')} </p> </div> <div class="education-item info-panel"> - <h4>Change Planning</h4> + <h4>{$t('education.changePlanning.title')}</h4> <p> - Before making DNS changes, consider lowering TTLs in advance. This reduces the time users see old records. - After changes stabilize, you can increase TTLs again. + {$t('education.changePlanning.content')} </p> </div> <div class="education-item info-panel"> - <h4>Monitoring Impact</h4> + <h4>{$t('education.monitoringImpact.title')}</h4> <p> - Monitor DNS query volumes when changing TTLs. Very short TTLs can significantly increase load on authoritative - servers and may impact DNS provider costs. + {$t('education.monitoringImpact.content')} </p> </div> </div> diff --git a/src/lib/components/tools/ULAGenerator.svelte b/src/lib/components/tools/ULAGenerator.svelte index c0d9ae40..d4c2fc8d 100644 --- a/src/lib/components/tools/ULAGenerator.svelte +++ b/src/lib/components/tools/ULAGenerator.svelte @@ -1,6 +1,14 @@ <script lang="ts"> + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + import { t, loadTranslations, locale } from '$lib/stores/language'; import { generateULAAddresses, parseULA, type ULAResult } from '$lib/utils/ula'; + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); + let count = 1; let subnetIds = ''; let result: ULAResult | null = null; @@ -41,47 +49,54 @@ <div class="container"> <div class="card"> - <h2>ULA Generator</h2> - <p>Generate RFC 4193 Unique Local Addresses with cryptographically secure Global IDs.</p> + <h2>{$t('tools.ula_generator.title')}</h2> + <p>{$t('tools.ula_generator.description')}</p> <div class="input-section"> <div class="input-group"> - <label for="count">Number of ULAs to generate (1-100):</label> - <input id="count" type="number" min="1" max="100" bind:value={count} placeholder="1" /> + <label for="count">{$t('tools.ula_generator.form.count.label')}</label> + <input + id="count" + type="number" + min="1" + max="100" + bind:value={count} + placeholder={$t('tools.ula_generator.form.count.placeholder')} + /> </div> <div class="input-group"> - <label for="subnet-ids">Subnet IDs (optional, comma/newline separated):</label> + <label for="subnet-ids">{$t('tools.ula_generator.form.subnetIds.label')}</label> <textarea id="subnet-ids" bind:value={subnetIds} rows="3" - placeholder="0001, 0002, 0003 or leave empty for random generation" + placeholder={$t('tools.ula_generator.form.subnetIds.placeholder')} ></textarea> - <small>If provided, must be 1-4 hex digits. Leave empty for random generation.</small> + <small>{$t('tools.ula_generator.form.subnetIds.helpText')}</small> </div> <button on:click={generateULAs} disabled={loading || count < 1 || count > 100} class="generate-btn"> - {loading ? 'Generating...' : 'Generate ULA Addresses'} + {loading ? $t('tools.ula_generator.buttons.generating') : $t('tools.ula_generator.buttons.generate')} </button> </div> {#if result} <div class="results-section"> <div class="summary"> - <h3>Generation Summary</h3> + <h3>{$t('tools.ula_generator.results.summary.title')}</h3> <div class="summary-stats"> <div class="stat"> - <span class="label">Total Requested:</span> + <span class="label">{$t('tools.ula_generator.results.summary.stats.totalRequested')}</span> <span class="value">{result.summary.totalRequests}</span> </div> <div class="stat"> - <span class="label">Successfully Generated:</span> + <span class="label">{$t('tools.ula_generator.results.summary.stats.successfullyGenerated')}</span> <span class="value success">{result.summary.successfulGenerations}</span> </div> {#if result.summary.failedGenerations > 0} <div class="stat"> - <span class="label">Failed:</span> + <span class="label">{$t('tools.ula_generator.results.summary.stats.failed')}</span> <span class="value error">{result.summary.failedGenerations}</span> </div> {/if} @@ -90,7 +105,7 @@ {#if result.errors.length > 0} <div class="errors"> - <h4>Errors</h4> + <h4>{$t('tools.ula_generator.results.errors.title')}</h4> <ul> {#each result.errors as error, index (index)} <li class="error">{error}</li> @@ -101,16 +116,16 @@ {#if result.generations.some((g) => g.isValid)} <div class="generations"> - <h3>Generated ULA Addresses</h3> + <h3>{$t('tools.ula_generator.results.addresses.title')}</h3> {#each result.generations as generation, i (generation.globalID)} {#if generation.isValid} <div class="generation-result"> <div class="generation-header"> - <h4>ULA #{i + 1}</h4> + <h4>{$t('tools.ula_generator.results.addresses.ulaNumber')}{i + 1}</h4> <button class="copy-btn" on:click={() => copyToClipboard(generation.network)} - title="Copy network address" + title={$t('tools.ula_generator.buttons.copyTooltip')} > πŸ“‹ </button> @@ -118,30 +133,30 @@ <div class="address-info"> <div class="address-row"> - <span class="label">Network:</span> + <span class="label">{$t('tools.ula_generator.results.addresses.network')}</span> <code class="network">{generation.network}</code> </div> <div class="address-row"> - <span class="label">Prefix:</span> + <span class="label">{$t('tools.ula_generator.results.addresses.prefix')}</span> <code>{generation.fullPrefix}::/64</code> </div> </div> <div class="components"> - <h5>Address Components</h5> + <h5>{$t('tools.ula_generator.results.components.title')}</h5> <div class="component-grid"> <div class="component"> - <span class="comp-label">ULA Prefix:</span> + <span class="comp-label">{$t('tools.ula_generator.results.components.ulaPrefix')}</span> <code>{generation.prefix}</code> <small>{generation.details.prefixBinary}</small> </div> <div class="component"> - <span class="comp-label">Global ID:</span> + <span class="comp-label">{$t('tools.ula_generator.results.components.globalId')}</span> <code>{generation.globalID}</code> <small>{generation.details.globalIDBinary}</small> </div> <div class="component"> - <span class="comp-label">Subnet ID:</span> + <span class="comp-label">{$t('tools.ula_generator.results.components.subnetId')}</span> <code>{generation.subnetID}</code> <small>{generation.details.subnetIDBinary}</small> </div> @@ -149,18 +164,18 @@ </div> <div class="generation-details"> - <h5>Generation Details</h5> + <h5>{$t('tools.ula_generator.results.details.title')}</h5> <div class="detail-grid"> <div class="detail"> - <span class="detail-label">Algorithm:</span> + <span class="detail-label">{$t('tools.ula_generator.results.details.algorithm')}</span> <span>{generation.details.algorithm}</span> </div> <div class="detail"> - <span class="detail-label">Timestamp:</span> + <span class="detail-label">{$t('tools.ula_generator.results.details.timestamp')}</span> <span>{new Date(generation.details.timestamp).toISOString()}</span> </div> <div class="detail"> - <span class="detail-label">Entropy:</span> + <span class="detail-label">{$t('tools.ula_generator.results.details.entropy')}</span> <code>{generation.details.entropy}</code> </div> </div> @@ -168,7 +183,10 @@ </div> {:else} <div class="generation-result error-result"> - <h4>ULA #{i + 1} - Error</h4> + <h4> + {$t('tools.ula_generator.results.addresses.ulaNumber')}{i + 1} + {$t('tools.ula_generator.results.addresses.error')} + </h4> <p class="error">{generation.error}</p> </div> {/if} @@ -180,40 +198,40 @@ </div> <div class="card"> - <h3>ULA Address Parser</h3> - <p>Parse and analyze existing ULA addresses to extract their components.</p> + <h3>{$t('tools.ula_generator.parser.title')}</h3> + <p>{$t('tools.ula_generator.parser.description')}</p> <div class="input-group"> - <label for="parse-input">ULA Address:</label> + <label for="parse-input">{$t('tools.ula_generator.parser.form.address.label')}</label> <input id="parse-input" type="text" bind:value={parseInput} on:input={parseULAAddress} - placeholder="fd12:3456:789a:0001::/64" + placeholder={$t('tools.ula_generator.parser.form.address.placeholder')} /> </div> {#if parseResult} {#if parseResult.isValid} <div class="parse-results"> - <h4>Parsed Components</h4> + <h4>{$t('tools.ula_generator.parser.results.title')}</h4> <div class="component-grid"> <div class="component"> - <span class="comp-label">ULA Prefix:</span> + <span class="comp-label">{$t('tools.ula_generator.parser.results.components.ulaPrefix')}</span> <code>{parseResult.prefix}</code> </div> <div class="component"> - <span class="comp-label">Global ID:</span> + <span class="comp-label">{$t('tools.ula_generator.parser.results.components.globalId')}</span> <code>{parseResult.globalID}</code> </div> <div class="component"> - <span class="comp-label">Subnet ID:</span> + <span class="comp-label">{$t('tools.ula_generator.parser.results.components.subnetId')}</span> <code>{parseResult.subnetID}</code> </div> {#if parseResult.interfaceID} <div class="component"> - <span class="comp-label">Interface ID:</span> + <span class="comp-label">{$t('tools.ula_generator.parser.results.components.interfaceId')}</span> <code>{parseResult.interfaceID}</code> </div> {/if} diff --git a/src/lib/components/tools/VLSMCalculator.svelte b/src/lib/components/tools/VLSMCalculator.svelte index e28bed5c..cb0d2cf0 100644 --- a/src/lib/components/tools/VLSMCalculator.svelte +++ b/src/lib/components/tools/VLSMCalculator.svelte @@ -15,6 +15,14 @@ import { tooltip } from '$lib/actions/tooltip.js'; import { SvelteSet } from 'svelte/reactivity'; import { formatNumber } from '$lib/utils/formatters'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + // Load translations for this tool + onMount(async () => { + await loadTranslations(get(locale), 'tools'); + }); let networkIP = $state('192.168.1.0'); let cidr = $state(24); @@ -36,7 +44,7 @@ function addSubnet() { subnets.push({ id: generateSubnetId(), - name: `Subnet ${subnets.length + 1}`, + name: $t('tools.vlsm_calculator.defaultSubnetName', { number: subnets.length + 1 }), hostsNeeded: 50, description: '', }); @@ -136,19 +144,20 @@ }); </script> -<ToolContentContainer - title="VLSM Calculator" - description="Design efficient subnets with Variable Length Subnet Masking for optimal address space utilization." -> +<ToolContentContainer title={$t('tools.vlsm_calculator.title')} description={$t('tools.vlsm_calculator.description')}> <!-- Network Configuration --> <div class="network-config"> - <h3>Network Configuration</h3> + <h3>{$t('tools.vlsm_calculator.networkConfig.title')}</h3> <div class="grid grid-2"> <div class="form-group"> - <IPInput bind:value={networkIP} label="Network Address" placeholder="192.168.1.0" /> + <IPInput + bind:value={networkIP} + label={$t('tools.vlsm_calculator.networkConfig.networkAddress.label')} + placeholder={$t('tools.vlsm_calculator.networkConfig.networkAddress.placeholder')} + /> </div> <div class="form-group"> - <label for="cidr-input">CIDR Notation</label> + <label for="cidr-input">{$t('tools.vlsm_calculator.networkConfig.cidrNotation.label')}</label> <div class="cidr-input"> <span class="cidr-prefix">/{cidr}</span> <input id="cidr-input" type="range" min="8" max="30" bind:value={cidr} class="cidr-slider" /> @@ -161,10 +170,10 @@ <!-- Subnet Requirements --> <div class="subnet-requirements"> <div class="requirements-header"> - <h3>Subnet Requirements</h3> + <h3>{$t('tools.vlsm_calculator.subnetRequirements.title')}</h3> <button type="button" class="btn btn-primary" onclick={addSubnet}> <Icon name="plus" size="sm" /> - Add Subnet + {$t('tools.vlsm_calculator.subnetRequirements.addSubnet')} </button> </div> @@ -176,13 +185,13 @@ <div class="requirement-inputs"> <input type="text" - placeholder="Subnet name" + placeholder={$t('tools.vlsm_calculator.subnetRequirements.subnetName.placeholder')} bind:value={subnet.name} oninput={(e) => updateSubnet(subnet.id, 'name', (e.target as HTMLInputElement)?.value)} class="subnet-name-input" /> <div class="hosts-input"> - <label for="hosts-{index}">Hosts needed:</label> + <label for="hosts-{index}">{$t('tools.vlsm_calculator.subnetRequirements.hostsNeeded')}</label> <input id="hosts-{index}" type="number" @@ -208,7 +217,7 @@ <div class="requirement-description"> <input type="text" - placeholder="Description (optional)" + placeholder={$t('tools.vlsm_calculator.subnetRequirements.description.placeholder')} bind:value={subnet.description} oninput={(e) => updateSubnet(subnet.id, 'description', (e.target as HTMLTextAreaElement)?.value)} class="description-input" @@ -225,26 +234,26 @@ {#if vlsmResult.success} <!-- Summary --> <div class="info-panel success"> - <h3>VLSM Calculation Results</h3> + <h3>{$t('tools.vlsm_calculator.summary.title')}</h3> <div class="summary-stats"> <div class="stat-item"> - <span class="stat-label">Total Subnets</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.totalSubnets')}</span> <span class="stat-value">{vlsmResult.subnets.length}</span> </div> <div class="stat-item"> - <span class="stat-label">Hosts Requested</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.hostsRequested')}</span> <span class="stat-value">{formatNumber(vlsmResult.totalHostsRequested)}</span> </div> <div class="stat-item"> - <span class="stat-label">Hosts Provided</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.hostsProvided')}</span> <span class="stat-value">{formatNumber(vlsmResult.totalHostsProvided)}</span> </div> <div class="stat-item"> - <span class="stat-label">Wasted Hosts</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.wastedHosts')}</span> <span class="stat-value danger">{formatNumber(vlsmResult.totalWastedHosts)}</span> </div> <div class="stat-item"> - <span class="stat-label">Efficiency</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.efficiency')}</span> <span class="stat-value" style="color: {getEfficiencyColor(vlsmResult.totalWastedHosts, vlsmResult.totalHostsProvided)}" @@ -253,7 +262,7 @@ </span> </div> <div class="stat-item"> - <span class="stat-label">Remaining Addresses</span> + <span class="stat-label">{$t('tools.vlsm_calculator.summary.remainingAddresses')}</span> <span class="stat-value">{formatNumber(vlsmResult.remainingAddresses)}</span> </div> </div> @@ -261,15 +270,15 @@ <!-- Subnets Table --> <div class="subnets-table-container"> - <h3>Calculated Subnets</h3> + <h3>{$t('tools.vlsm_calculator.table.title')}</h3> <div class="subnets-table"> <div class="table-header"> - <div class="col-name">Subnet</div> - <div class="col-network">Network</div> - <div class="col-hosts">Hosts</div> - <div class="col-mask">Mask</div> - <div class="col-efficiency">Efficiency</div> - <div class="col-actions">Actions</div> + <div class="col-name">{$t('tools.vlsm_calculator.table.columns.subnet')}</div> + <div class="col-network">{$t('tools.vlsm_calculator.table.columns.network')}</div> + <div class="col-hosts">{$t('tools.vlsm_calculator.table.columns.hosts')}</div> + <div class="col-mask">{$t('tools.vlsm_calculator.table.columns.mask')}</div> + <div class="col-efficiency">{$t('tools.vlsm_calculator.table.columns.efficiency')}</div> + <div class="col-actions">{$t('tools.vlsm_calculator.table.columns.actions')}</div> </div> {#each vlsmResult.subnets as subnet (subnet.id)} @@ -292,10 +301,16 @@ <div class="col-hosts"> <div class="hosts-info"> - <div class="hosts-needed">{subnet.hostsNeeded} needed</div> - <div class="hosts-provided">{subnet.hostsProvided} provided</div> + <div class="hosts-needed"> + {$t('tools.vlsm_calculator.table.hostsNeeded', { count: subnet.hostsNeeded })} + </div> + <div class="hosts-provided"> + {$t('tools.vlsm_calculator.table.hostsProvided', { count: subnet.hostsProvided })} + </div> {#if subnet.wastedHosts > 0} - <div class="hosts-wasted">{subnet.wastedHosts} wasted</div> + <div class="hosts-wasted"> + {$t('tools.vlsm_calculator.table.hostsWasted', { count: subnet.wastedHosts })} + </div> {/if} </div> </div> @@ -324,7 +339,7 @@ > <Icon name="chevron-down" size="sm" /> </button> - <Tooltip text="Copy network info" position="left"> + <Tooltip text={$t('tools.vlsm_calculator.actions.copyNetworkInfo')} position="left"> <button type="button" class="btn btn-ghost {clipboard.isCopied(`copy-${subnet.id}`) ? 'copied' : ''}" @@ -340,52 +355,62 @@ <div class="subnet-details"> <div class="details-grid"> <div class="detail-item"> - <span class="detail-label" use:tooltip={'First IP address in the subnet - identifies the network'} - >Network Address</span + <span + class="detail-label" + use:tooltip={$t('tools.vlsm_calculator.details.networkAddress.tooltip')} + >{$t('tools.vlsm_calculator.details.networkAddress.label')}</span > <code class="detail-value">{subnet.networkAddress}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Last IP address in the subnet - sends to all hosts'} - >Broadcast Address</span + <span + class="detail-label" + use:tooltip={$t('tools.vlsm_calculator.details.broadcastAddress.tooltip')} + >{$t('tools.vlsm_calculator.details.broadcastAddress.label')}</span > <code class="detail-value">{subnet.broadcastAddress}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'First IP address available for host assignment'} - >First Usable Host</span + <span + class="detail-label" + use:tooltip={$t('tools.vlsm_calculator.details.firstUsableHost.tooltip')} + >{$t('tools.vlsm_calculator.details.firstUsableHost.label')}</span > <code class="detail-value">{subnet.firstUsableHost}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Last IP address available for host assignment'} - >Last Usable Host</span + <span + class="detail-label" + use:tooltip={$t('tools.vlsm_calculator.details.lastUsableHost.tooltip')} + >{$t('tools.vlsm_calculator.details.lastUsableHost.label')}</span > <code class="detail-value">{subnet.lastUsableHost}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Defines which portion of IP represents network vs host'} - >Subnet Mask</span + <span class="detail-label" use:tooltip={$t('tools.vlsm_calculator.details.subnetMask.tooltip')} + >{$t('tools.vlsm_calculator.details.subnetMask.label')}</span > <code class="detail-value">{subnet.subnetMask}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Inverse of subnet mask - used in access control lists'} - >Wildcard Mask</span + <span class="detail-label" use:tooltip={$t('tools.vlsm_calculator.details.wildcardMask.tooltip')} + >{$t('tools.vlsm_calculator.details.wildcardMask.label')}</span > <code class="detail-value">{subnet.wildcardMask}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Binary representation of the subnet mask'} - >Binary Mask</span + <span class="detail-label" use:tooltip={$t('tools.vlsm_calculator.details.binaryMask.tooltip')} + >{$t('tools.vlsm_calculator.details.binaryMask.label')}</span > <code class="detail-value binary-mask">{subnet.binaryMask}</code> </div> <div class="detail-item"> - <span class="detail-label" use:tooltip={'Number of bits available for host addressing'} - >Host Bits</span + <span class="detail-label" use:tooltip={$t('tools.vlsm_calculator.details.hostBits.tooltip')} + >{$t('tools.vlsm_calculator.details.hostBits.label')}</span + > + <code class="detail-value" + >{$t('tools.vlsm_calculator.details.hostBits.value', { count: subnet.actualHostBits })}</code > - <code class="detail-value">{subnet.actualHostBits} bits</code> </div> </div> </div> @@ -407,7 +432,7 @@ {:else} <!-- Error --> <div class="info-panel error"> - <h3>Calculation Error</h3> + <h3>{$t('tools.vlsm_calculator.error.title')}</h3> <p class="error-message">{vlsmResult.error}</p> </div> {/if} diff --git a/src/lib/components/tools/WildcardMask.svelte b/src/lib/components/tools/WildcardMask.svelte index cddbfcdc..19b80fc1 100644 --- a/src/lib/components/tools/WildcardMask.svelte +++ b/src/lib/components/tools/WildcardMask.svelte @@ -5,6 +5,7 @@ import '../../../styles/diagnostics-pages.scss'; import { useClipboard } from '$lib/composables'; import { formatNumber } from '$lib/utils/formatters'; + import { t } from '$lib/stores/language'; let inputText = $state('192.168.1.0/24\n10.0.0.0 255.255.255.0\n172.16.0.0 0.0.255.255'); let result = $state<WildcardResult | null>(null); @@ -14,49 +15,55 @@ let selectedExampleIndex = $state<number | null>(null); let _userModified = $state(false); - const examples = [ + const examples = $derived([ { - label: 'Basic CIDR to Wildcard', + label: $t('tools/wildcard-mask.examples.items.basicCidr.label'), input: `192.168.1.0/24 10.0.0.0/16 172.16.0.0/20`, generateACL: false, + preview: $t('tools/wildcard-mask.examples.items.basicCidr.preview'), }, { - label: 'Subnet Mask Format', + label: $t('tools/wildcard-mask.examples.items.subnetMask.label'), input: `192.168.1.0 255.255.255.0 10.0.0.0 255.255.0.0 172.16.0.0 255.255.240.0`, generateACL: false, + preview: $t('tools/wildcard-mask.examples.items.subnetMask.preview'), }, { - label: 'Wildcard Mask Input', + label: $t('tools/wildcard-mask.examples.items.wildcardInput.label'), input: `192.168.0.0 0.0.255.255 10.0.0.0 0.255.255.255 172.16.0.0 0.0.15.255`, generateACL: false, + preview: $t('tools/wildcard-mask.examples.items.wildcardInput.preview'), }, { - label: 'Mixed Formats', + label: $t('tools/wildcard-mask.examples.items.mixedFormats.label'), input: `192.168.1.0/24 10.0.0.0 255.255.0.0 172.16.0.0 0.0.255.255`, generateACL: false, + preview: $t('tools/wildcard-mask.examples.items.mixedFormats.preview'), }, { - label: 'Cisco ACL Generation', + label: $t('tools/wildcard-mask.examples.items.ciscoAcl.label'), input: `192.168.1.0/24 10.0.0.0/16`, generateACL: true, + preview: $t('tools/wildcard-mask.examples.items.ciscoAcl.preview'), }, { - label: 'Complex Network ACLs', + label: $t('tools/wildcard-mask.examples.items.complexAcl.label'), input: `192.168.0.0/22 10.1.0.0/20 172.16.100.0/24`, generateACL: true, + preview: $t('tools/wildcard-mask.examples.items.complexAcl.preview'), }, - ]; + ]); // ACL options let generateACL = $state(false); @@ -163,8 +170,8 @@ <div class="card"> <header class="card-header"> - <h2>Wildcard Mask Converter</h2> - <p>Convert between CIDR notation, subnet masks, and wildcard masks with ACL rule generation</p> + <h2>{$t('tools/wildcard-mask.title')}</h2> + <p>{$t('tools/wildcard-mask.subtitle')}</p> </header> <!-- Examples --> @@ -172,7 +179,7 @@ <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="xs" /> - <h4>Quick Examples</h4> + <h4>{$t('tools/wildcard-mask.examples.title')}</h4> </summary> <div class="examples-grid"> {#each examples as example, i (example.label)} @@ -183,7 +190,7 @@ > <div class="example-label">{example.label}</div> <div class="example-preview"> - {example.generateACL ? 'With ACL' : 'Conversion only'} + {example.preview} </div> </button> {/each} @@ -193,59 +200,72 @@ <div class="input-section"> <div class="inputs-section"> - <h3 use:tooltip={'Enter networks in various formats for wildcard mask conversion'}>Network Inputs</h3> + <h3 use:tooltip={$t('tools/wildcard-mask.networkInputs.titleTooltip')}> + {$t('tools/wildcard-mask.networkInputs.title')} + </h3> <div class="input-group"> - <label for="inputs" use:tooltip={'Enter networks in CIDR, subnet mask, or wildcard mask format'}> - IP Addresses, CIDRs, or Ranges + <label for="inputs" use:tooltip={$t('tools/wildcard-mask.networkInputs.labelTooltip')}> + {$t('tools/wildcard-mask.networkInputs.label')} </label> <textarea id="inputs" bind:value={inputText} oninput={handleInputChange} - placeholder="192.168.1.0/24 10.0.0.0 255.255.255.0 172.16.0.0 0.0.255.255" + placeholder={$t('tools/wildcard-mask.networkInputs.placeholder')} rows="6" ></textarea> <div class="input-help"> - Enter one per line: CIDR (192.168.1.0/24), network + subnet mask (10.0.0.0 255.255.255.0), or network + - wildcard mask (172.16.0.0 0.0.255.255) + {$t('tools/wildcard-mask.networkInputs.help')} </div> </div> </div> <div class="acl-section"> - <h3 use:tooltip={'Configure access control list rule generation for network devices'}>ACL Options</h3> + <h3 use:tooltip={$t('tools/wildcard-mask.aclOptions.titleTooltip')}> + {$t('tools/wildcard-mask.aclOptions.title')} + </h3> <div class="checkbox-group"> - <label class="checkbox-label" use:tooltip={'Generate access control list rules for network devices'}> + <label class="checkbox-label" use:tooltip={$t('tools/wildcard-mask.aclOptions.generateTooltip')}> <input type="checkbox" bind:checked={generateACL} onchange={handleInputChange} /> - <span class="checkbox-text">Generate ACL Rules</span> + <span class="checkbox-text">{$t('tools/wildcard-mask.aclOptions.generateLabel')}</span> </label> </div> {#if generateACL} <div class="acl-settings"> <div class="input-group"> - <label for="acl-type" use:tooltip={'Whether to permit or deny traffic matching this rule'}> Action </label> + <label for="acl-type" use:tooltip={$t('tools/wildcard-mask.aclOptions.action.tooltip')} + >{$t('tools/wildcard-mask.aclOptions.action.label')}</label + > <select id="acl-type" bind:value={aclType} onchange={handleInputChange}> - <option value="permit">Permit</option> - <option value="deny">Deny</option> + <option value="permit">{$t('tools/wildcard-mask.aclOptions.action.permit')}</option> + <option value="deny">{$t('tools/wildcard-mask.aclOptions.action.deny')}</option> </select> </div> <div class="input-group"> - <label for="protocol" use:tooltip={'Network protocol (ip, tcp, udp, icmp, etc.)'}> Protocol </label> - <input id="protocol" type="text" bind:value={protocol} oninput={handleInputChange} placeholder="ip" /> + <label for="protocol" use:tooltip={$t('tools/wildcard-mask.aclOptions.protocol.tooltip')} + >{$t('tools/wildcard-mask.aclOptions.protocol.label')}</label + > + <input + id="protocol" + type="text" + bind:value={protocol} + oninput={handleInputChange} + placeholder={$t('tools/wildcard-mask.aclOptions.protocol.placeholder')} + /> </div> <div class="input-group"> - <label for="destination" use:tooltip={"Destination network or 'any' for all destinations"}> - Destination + <label for="destination" use:tooltip={$t('tools/wildcard-mask.aclOptions.destination.tooltip')}> + {$t('tools/wildcard-mask.aclOptions.destination.label')} </label> <input id="destination" type="text" bind:value={destination} oninput={handleInputChange} - placeholder="any" + placeholder={$t('tools/wildcard-mask.aclOptions.destination.placeholder')} /> </div> </div> @@ -256,7 +276,7 @@ {#if isLoading} <div class="loading"> <Icon name="loader" /> - Converting masks... + {$t('tools/wildcard-mask.loading')} </div> {/if} @@ -264,7 +284,7 @@ <div class="results"> {#if result.errors.length > 0} <div class="errors"> - <h3><Icon name="alert-triangle" /> Errors</h3> + <h3><Icon name="alert-triangle" /> {$t('tools/wildcard-mask.errors.title')}</h3> {#each result.errors as error, index (index)} <div class="error-item">{error}</div> {/each} @@ -273,34 +293,44 @@ {#if result.conversions.length > 0} <div class="summary"> - <h3 use:tooltip={'Overview of wildcard mask conversion results'}>Conversion Summary</h3> + <h3 use:tooltip={$t('tools/wildcard-mask.summary.titleTooltip')}> + {$t('tools/wildcard-mask.summary.title')} + </h3> <div class="summary-stats"> <div class="stat"> <span class="stat-value">{result.summary.totalInputs}</span> - <span class="stat-label" use:tooltip={'Total number of network inputs processed'}>Total Inputs</span> + <span class="stat-label" use:tooltip={$t('tools/wildcard-mask.summary.totalInputs.tooltip')} + >{$t('tools/wildcard-mask.summary.totalInputs.label')}</span + > </div> <div class="stat aligned"> <span class="stat-value">{result.summary.validInputs}</span> - <span class="stat-label" use:tooltip={'Successfully converted network inputs'}>Valid</span> + <span class="stat-label" use:tooltip={$t('tools/wildcard-mask.summary.valid.tooltip')} + >{$t('tools/wildcard-mask.summary.valid.label')}</span + > </div> <div class="stat misaligned"> <span class="stat-value">{result.summary.invalidInputs}</span> - <span class="stat-label" use:tooltip={'Network inputs that could not be converted'}>Invalid</span> + <span class="stat-label" use:tooltip={$t('tools/wildcard-mask.summary.invalid.tooltip')} + >{$t('tools/wildcard-mask.summary.invalid.label')}</span + > </div> </div> </div> <div class="conversions"> <div class="conversions-header"> - <h3 use:tooltip={'Detailed conversion results for each network input'}>Mask Conversions</h3> + <h3 use:tooltip={$t('tools/wildcard-mask.conversions.titleTooltip')}> + {$t('tools/wildcard-mask.conversions.title')} + </h3> <div class="export-buttons"> <button onclick={() => exportResults('csv')}> <Icon name="csv-file" /> - Export CSV + {$t('tools/wildcard-mask.conversions.exportCsv')} </button> <button onclick={() => exportResults('json')}> <Icon name="json-file" /> - Export JSON + {$t('tools/wildcard-mask.conversions.exportJson')} </button> </div> </div> @@ -316,10 +346,10 @@ <div class="check-status"> {#if conversion.isValid} <Icon name="check-circle" /> - Valid + {$t('tools/wildcard-mask.conversions.status.valid')} {:else} <Icon name="x-circle" /> - Invalid + {$t('tools/wildcard-mask.conversions.status.invalid')} {/if} </div> </div> @@ -327,7 +357,9 @@ {#if conversion.isValid} <div class="conversion-details"> <div class="detail-row"> - <span class="label" use:tooltip={'Classless Inter-Domain Routing notation'}>CIDR:</span> + <span class="label" use:tooltip={$t('tools/wildcard-mask.conversions.details.cidr.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.cidr.label')}</span + > <div class="code-container"> <code>{conversion.cidr}</code> <button @@ -335,7 +367,7 @@ class="btn btn-icon btn-xs" class:copied={clipboard.isCopied(conversion.cidr)} onclick={() => clipboard.copy(conversion.cidr, conversion.cidr)} - use:tooltip={{ text: 'Copy to clipboard', position: 'top' }} + use:tooltip={{ text: $t('tools/wildcard-mask.conversions.copyTooltip'), position: 'top' }} > <Icon name={clipboard.isCopied(conversion.cidr) ? 'check' : 'copy'} size="xs" /> </button> @@ -343,8 +375,8 @@ </div> <div class="detail-row"> - <span class="label" use:tooltip={'Standard subnet mask in dotted decimal notation'} - >Subnet Mask:</span + <span class="label" use:tooltip={$t('tools/wildcard-mask.conversions.details.subnetMask.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.subnetMask.label')}</span > <div class="code-container"> <code>{conversion.subnetMask}</code> @@ -353,7 +385,7 @@ class="btn btn-icon btn-xs" class:copied={clipboard.isCopied(conversion.subnetMask)} onclick={() => clipboard.copy(conversion.subnetMask, conversion.subnetMask)} - use:tooltip={{ text: 'Copy to clipboard', position: 'top' }} + use:tooltip={{ text: $t('tools/wildcard-mask.conversions.copyTooltip'), position: 'top' }} > <Icon name={clipboard.isCopied(conversion.subnetMask) ? 'check' : 'copy'} size="xs" /> </button> @@ -361,8 +393,10 @@ </div> <div class="detail-row"> - <span class="label" use:tooltip={'Inverse subnet mask used in Cisco ACLs and OSPF'} - >Wildcard Mask:</span + <span + class="label" + use:tooltip={$t('tools/wildcard-mask.conversions.details.wildcardMask.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.wildcardMask.label')}</span > <div class="code-container"> <code>{conversion.wildcardMask}</code> @@ -371,7 +405,7 @@ class="btn btn-icon btn-xs" class:copied={clipboard.isCopied(conversion.wildcardMask)} onclick={() => clipboard.copy(conversion.wildcardMask, conversion.wildcardMask)} - use:tooltip={{ text: 'Copy to clipboard', position: 'top' }} + use:tooltip={{ text: $t('tools/wildcard-mask.conversions.copyTooltip'), position: 'top' }} > <Icon name={clipboard.isCopied(conversion.wildcardMask) ? 'check' : 'copy'} size="xs" /> </button> @@ -381,24 +415,34 @@ <div class="network-info"> <div class="info-grid"> <div> - <span class="info-label" use:tooltip={'First address in the network range'}>Network:</span> + <span + class="info-label" + use:tooltip={$t('tools/wildcard-mask.conversions.details.network.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.network.label')}</span + > <span class="info-value">{conversion.networkAddress}</span> </div> <div> - <span class="info-label" use:tooltip={'Last address in the network range'}>Broadcast:</span> + <span + class="info-label" + use:tooltip={$t('tools/wildcard-mask.conversions.details.broadcast.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.broadcast.label')}</span + > <span class="info-value">{conversion.broadcastAddress}</span> </div> <div> - <span class="info-label" use:tooltip={'Number of bits available for host addresses'} - >Host Bits:</span + <span + class="info-label" + use:tooltip={$t('tools/wildcard-mask.conversions.details.hostBits.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.hostBits.label')}</span > <span class="info-value">{conversion.hostBits}</span> </div> <div> <span class="info-label" - use:tooltip={'Total assignable host addresses (excluding network and broadcast)'} - >Usable Hosts:</span + use:tooltip={$t('tools/wildcard-mask.conversions.details.usableHosts.tooltip')} + >{$t('tools/wildcard-mask.conversions.details.usableHosts.label')}</span > <span class="info-value">{formatNumber(conversion.usableHosts)}</span> </div> @@ -418,19 +462,25 @@ {#if generateACL && (result.aclRules.cisco.length > 0 || result.aclRules.juniper.length > 0 || result.aclRules.generic.length > 0)} <div class="acl-rules-container"> - <h3 use:tooltip={'Access control list rules generated for network devices'}>Generated ACL Rules</h3> + <h3 use:tooltip={$t('tools/wildcard-mask.aclRules.titleTooltip')}> + {$t('tools/wildcard-mask.aclRules.title')} + </h3> {#if result.aclRules.cisco.length > 0} <div class="acl-section"> <div class="acl-header"> - <h4 use:tooltip={'Cisco IOS access control list format'}>Cisco ACL</h4> + <h4 use:tooltip={$t('tools/wildcard-mask.aclRules.cisco.tooltip')}> + {$t('tools/wildcard-mask.aclRules.cisco.title')} + </h4> <button onclick={() => copyACLRules('cisco')} class="copy-btn {clipboard.isCopied('acl-cisco') ? 'copied' : ''}" - use:tooltip={'Copy all Cisco ACL rules to clipboard'} + use:tooltip={$t('tools/wildcard-mask.aclRules.cisco.copyTooltip')} > <Icon name={clipboard.isCopied('acl-cisco') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('acl-cisco') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('acl-cisco') + ? $t('tools/wildcard-mask.common.copied') + : $t('tools/wildcard-mask.common.copy')} </button> </div> <div class="acl-code"> @@ -444,14 +494,18 @@ {#if result.aclRules.juniper.length > 0} <div class="acl-section"> <div class="acl-header"> - <h4 use:tooltip={'Juniper JunOS firewall filter format'}>Juniper ACL</h4> + <h4 use:tooltip={$t('tools/wildcard-mask.aclRules.juniper.tooltip')}> + {$t('tools/wildcard-mask.aclRules.juniper.title')} + </h4> <button onclick={() => copyACLRules('juniper')} class="copy-btn {clipboard.isCopied('acl-juniper') ? 'copied' : ''}" - use:tooltip={'Copy all Juniper ACL rules to clipboard'} + use:tooltip={$t('tools/wildcard-mask.aclRules.juniper.copyTooltip')} > <Icon name={clipboard.isCopied('acl-juniper') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('acl-juniper') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('acl-juniper') + ? $t('tools/wildcard-mask.common.copied') + : $t('tools/wildcard-mask.common.copy')} </button> </div> <div class="acl-code"> @@ -465,14 +519,18 @@ {#if result.aclRules.generic.length > 0} <div class="acl-section"> <div class="acl-header"> - <h4 use:tooltip={'Generic access control list format'}>Generic ACL</h4> + <h4 use:tooltip={$t('tools/wildcard-mask.aclRules.generic.tooltip')}> + {$t('tools/wildcard-mask.aclRules.generic.title')} + </h4> <button onclick={() => copyACLRules('generic')} class="copy-btn {clipboard.isCopied('acl-generic') ? 'copied' : ''}" - use:tooltip={'Copy all generic ACL rules to clipboard'} + use:tooltip={$t('tools/wildcard-mask.aclRules.generic.copyTooltip')} > <Icon name={clipboard.isCopied('acl-generic') ? 'check' : 'copy'} size="xs" /> - {clipboard.isCopied('acl-generic') ? 'Copied!' : 'Copy'} + {clipboard.isCopied('acl-generic') + ? $t('tools/wildcard-mask.common.copied') + : $t('tools/wildcard-mask.common.copy')} </button> </div> <div class="acl-code"> diff --git a/src/lib/components/tools/ZoneStats.svelte b/src/lib/components/tools/ZoneStats.svelte index 5315ab7e..6014da10 100644 --- a/src/lib/components/tools/ZoneStats.svelte +++ b/src/lib/components/tools/ZoneStats.svelte @@ -3,15 +3,22 @@ import Icon from '$lib/components/global/Icon.svelte'; import { parseZoneFile, generateZoneStats, type ZoneStats } from '$lib/utils/zone-parser.js'; import { useClipboard } from '$lib/composables'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools/zone-stats'); + }); let zoneInput = $state(''); let results = $state<ZoneStats | null>(null); const clipboard = useClipboard(); let activeExampleIndex = $state<number | null>(null); - const examples = [ + const examples = $derived([ { - name: 'Simple Zone', + name: $t('examples.simple.name'), content: `$ORIGIN example.com. $TTL 86400 @ IN SOA ns1.example.com. admin.example.com. ( @@ -28,10 +35,10 @@ $TTL 86400 www 300 IN A 192.0.2.1 mail IN A 192.0.2.10 ftp IN CNAME www.example.com.`, - description: 'Basic zone with common record types', + description: $t('examples.simple.description'), }, { - name: 'Complex Zone', + name: $t('examples.complex.name'), content: `$ORIGIN example.com. $TTL 3600 @ IN SOA ns1.example.com. hostmaster.example.com. 2023010101 10800 3600 604800 86400 @@ -54,10 +61,10 @@ _sip._tcp IN SRV 10 60 5060 sip.example.com. blog IN CNAME www.example.com. shop IN CNAME www.example.com.`, - description: 'Comprehensive zone with diverse record types and TTLs', + description: $t('examples.complex.description'), }, { - name: 'Large Organization', + name: $t('examples.large.name'), content: `$ORIGIN bigcorp.com. $TTL 7200 @@ -98,9 +105,9 @@ dns4 IN A 203.0.113.113 london IN A 203.0.113.200 tokyo IN A 203.0.113.201 sydney IN A 203.0.113.202`, - description: 'Large organization with multiple services and locations', + description: $t('examples.large.description'), }, - ]; + ]); function loadExample(example: (typeof examples)[0], index: number) { zoneInput = example.content; @@ -135,12 +142,12 @@ sydney IN A 203.0.113.202`, function formatStatsForCopy(stats: ZoneStats): string { const lines: string[] = []; - lines.push(`DNS Zone Statistics Report`); - lines.push(`========================\n`); + lines.push($t('copyTemplate.title')); + lines.push($t('copyTemplate.separator') + '\n'); - lines.push(`Total Records: ${stats.totalRecords}\n`); + lines.push($t('copyTemplate.totalRecords', { count: stats.totalRecords }) + '\n'); - lines.push(`Records by Type:`); + lines.push($t('copyTemplate.recordsByType')); Object.entries(stats.recordsByType) .sort(([, a], [, b]) => b - a) .forEach(([type, count]) => { @@ -148,29 +155,34 @@ sydney IN A 203.0.113.202`, }); lines.push(''); - lines.push(`TTL Distribution:`); + lines.push($t('copyTemplate.ttlDistribution')); Object.entries(stats.ttlDistribution) .sort(([a], [b]) => parseInt(a) - parseInt(b)) .forEach(([ttl, count]) => { - lines.push(` ${ttl}s: ${count} record${count !== 1 ? 's' : ''}`); + const plural = count !== 1 ? 's' : ''; + lines.push(` ${$t('copyTemplate.ttlEntry', { ttl, count, plural })}`); }); lines.push(''); - lines.push(`Name Statistics:`); - lines.push(` Shortest name: ${stats.nameDepths.min} characters`); - lines.push(` Longest name: ${stats.nameDepths.max} characters`); - lines.push(` Average length: ${stats.nameDepths.average.toFixed(1)} characters`); + lines.push($t('copyTemplate.nameStats')); + lines.push(` ${$t('copyTemplate.shortestName', { length: stats.nameDepths.min })}`); + lines.push(` ${$t('copyTemplate.longestName', { length: stats.nameDepths.max })}`); + lines.push(` ${$t('copyTemplate.averageLength', { length: stats.nameDepths.average.toFixed(1) })}`); lines.push(''); - lines.push(`Largest Record: ${stats.largestRecord.size} bytes`); + lines.push($t('copyTemplate.largestRecord', { size: stats.largestRecord.size })); lines.push(` ${stats.largestRecord.record.owner} ${stats.largestRecord.record.type}`); lines.push(''); - lines.push(`Zone Health:`); - lines.push(` Has SOA: ${stats.sanityChecks.hasSoa ? 'Yes' : 'No'}`); - lines.push(` Has NS records: ${stats.sanityChecks.hasNs ? 'Yes' : 'No'}`); - lines.push(` Duplicate records: ${stats.sanityChecks.duplicates.length}`); - lines.push(` Orphaned glue: ${stats.sanityChecks.orphanedGlue.length}`); + lines.push($t('copyTemplate.zoneHealth')); + lines.push( + ` ${$t('copyTemplate.hasSoa', { status: stats.sanityChecks.hasSoa ? $t('copyTemplate.yes') : $t('copyTemplate.no') })}`, + ); + lines.push( + ` ${$t('copyTemplate.hasNs', { status: stats.sanityChecks.hasNs ? $t('copyTemplate.yes') : $t('copyTemplate.no') })}`, + ); + lines.push(` ${$t('copyTemplate.duplicates', { count: stats.sanityChecks.duplicates.length })}`); + lines.push(` ${$t('copyTemplate.orphanedGlue', { count: stats.sanityChecks.orphanedGlue.length })}`); return lines.join('\n'); } @@ -188,17 +200,17 @@ sydney IN A 203.0.113.202`, } function getTTLLabel(ttl: number): string { - if (ttl < 300) return 'Very Short'; - if (ttl < 3600) return 'Short'; - if (ttl < 86400) return 'Medium'; - return 'Long'; + if (ttl < 300) return $t('results.ttlDistribution.labels.veryShort'); + if (ttl < 3600) return $t('results.ttlDistribution.labels.short'); + if (ttl < 86400) return $t('results.ttlDistribution.labels.medium'); + return $t('results.ttlDistribution.labels.long'); } </script> <div class="card"> <header class="card-header"> - <h1>DNS Zone Statistics</h1> - <p>Analyze zone file structure, record distribution, and configuration health</p> + <h1>{$t('title')}</h1> + <p>{$t('description')}</p> </header> <!-- Educational Overview --> @@ -207,19 +219,22 @@ sydney IN A 203.0.113.202`, <div class="overview-item"> <Icon name="bar-chart" size="sm" /> <div> - <strong>Record Analysis:</strong> Count and categorize all DNS records by type and TTL. + <strong>{$t('overview.recordAnalysis.title')}</strong> + {$t('overview.recordAnalysis.content')} </div> </div> <div class="overview-item"> <Icon name="ruler" size="sm" /> <div> - <strong>Size Metrics:</strong> Identify largest records and analyze name length distribution. + <strong>{$t('overview.sizeMetrics.title')}</strong> + {$t('overview.sizeMetrics.content')} </div> </div> <div class="overview-item"> <Icon name="shield" size="sm" /> <div> - <strong>Health Checks:</strong> Validate zone structure and identify potential issues. + <strong>{$t('overview.healthChecks.title')}</strong> + {$t('overview.healthChecks.content')} </div> </div> </div> @@ -230,7 +245,7 @@ sydney IN A 203.0.113.202`, <details class="examples-details"> <summary class="examples-summary"> <Icon name="chevron-right" size="sm" /> - <h3>Zone Analysis Examples</h3> + <h3>{$t('examples.title')}</h3> </summary> <div class="examples-grid"> {#each examples as example, index (example.name)} @@ -249,9 +264,9 @@ sydney IN A 203.0.113.202`, <!-- Input Section --> <div class="card input-card"> <div class="input-group"> - <label for="zone-input" use:tooltip={'Paste your DNS zone file for comprehensive statistical analysis'}> + <label for="zone-input" use:tooltip={$t('input.tooltip')}> <Icon name="file" size="sm" /> - Zone File Content + {$t('input.label')} </label> <textarea id="zone-input" @@ -280,13 +295,13 @@ www IN A 192.0.2.1" {#if results} <section class="results-section"> <div class="results-header"> - <h3>Zone Analysis Report</h3> + <h3>{$t('results.title')}</h3> <button class="copy-button {clipboard.isCopied() ? 'copied' : ''}" onclick={() => results && clipboard.copy(formatStatsForCopy(results))} > <Icon name={clipboard.isCopied() ? 'check' : 'copy'} size="sm" /> - {clipboard.isCopied() ? 'Copied!' : 'Copy Report'} + {clipboard.isCopied() ? $t('results.copied') : $t('results.copy')} </button> </div> @@ -299,7 +314,7 @@ www IN A 192.0.2.1" </div> <div class="stat-info"> <div class="stat-value">{results.totalRecords}</div> - <div class="stat-label">Total Records</div> + <div class="stat-label">{$t('results.stats.totalRecords')}</div> </div> </div> @@ -309,7 +324,7 @@ www IN A 192.0.2.1" </div> <div class="stat-info"> <div class="stat-value">{Object.keys(results.recordsByType).length}</div> - <div class="stat-label">Record Types</div> + <div class="stat-label">{$t('results.stats.recordTypes')}</div> </div> </div> @@ -319,7 +334,7 @@ www IN A 192.0.2.1" </div> <div class="stat-info"> <div class="stat-value">{Object.keys(results.ttlDistribution).length}</div> - <div class="stat-label">Unique TTLs</div> + <div class="stat-label">{$t('results.stats.uniqueTtls')}</div> </div> </div> @@ -329,7 +344,7 @@ www IN A 192.0.2.1" </div> <div class="stat-info"> <div class="stat-value">{results.nameDepths.average.toFixed(1)}</div> - <div class="stat-label">Avg Name Length</div> + <div class="stat-label">{$t('results.stats.avgNameLength')}</div> </div> </div> </div> @@ -338,14 +353,19 @@ www IN A 192.0.2.1" <div class="chart-card"> <h4> <Icon name="pie" size="sm" /> - Record Type Distribution + {$t('results.recordDistribution.title')} </h4> <div class="record-types-chart"> {#each Object.entries(results.recordsByType).sort(([, a], [, b]) => b - a) as [type, count] (type)} <div class="type-row"> <div class="type-info"> <span class="type-name">{type}</span> - <span class="type-count">{count} record{count !== 1 ? 's' : ''}</span> + <span class="type-count" + >{count} + {count !== 1 + ? $t('results.recordDistribution.recordsPlural') + : $t('results.recordDistribution.records')}</span + > </div> <div class="type-bar-container"> <div class="type-bar" style="width: {(count / results.totalRecords) * 100}%"></div> @@ -362,7 +382,7 @@ www IN A 192.0.2.1" <div class="chart-card"> <h4> <Icon name="clock" size="sm" /> - TTL Distribution + {$t('results.ttlDistribution.title')} </h4> <div class="ttl-distribution"> {#each Object.entries(results.ttlDistribution).sort(([a], [b]) => parseInt(a) - parseInt(b)) as [ttl, count] (ttl)} @@ -373,7 +393,12 @@ www IN A 192.0.2.1" </span> <span class="ttl-label">({getTTLLabel(parseInt(ttl))})</span> </div> - <div class="ttl-count">{count} record{count !== 1 ? 's' : ''}</div> + <div class="ttl-count"> + {count} + {count !== 1 + ? $t('results.recordDistribution.recordsPlural') + : $t('results.recordDistribution.records')} + </div> </div> {/each} </div> @@ -383,21 +408,24 @@ www IN A 192.0.2.1" <div class="analysis-card"> <h4> <Icon name="ruler" size="sm" /> - Name Length Analysis + {$t('results.nameAnalysis.title')} </h4> <div class="name-stats"> <div class="name-stat"> - <div class="name-stat-label">Shortest Name</div> - <div class="name-stat-value">{results.nameDepths.min} chars</div> + <div class="name-stat-label">{$t('results.nameAnalysis.shortestName')}</div> + <div class="name-stat-value">{results.nameDepths.min} {$t('results.nameAnalysis.chars')}</div> </div> <div class="name-stat"> - <div class="name-stat-label">Longest Name</div> - <div class="name-stat-value">{results.nameDepths.max} chars</div> + <div class="name-stat-label">{$t('results.nameAnalysis.longestName')}</div> + <div class="name-stat-value">{results.nameDepths.max} {$t('results.nameAnalysis.chars')}</div> <div class="name-stat-detail">{results.longestName.name}</div> </div> <div class="name-stat"> - <div class="name-stat-label">Average Length</div> - <div class="name-stat-value">{results.nameDepths.average.toFixed(1)} chars</div> + <div class="name-stat-label">{$t('results.nameAnalysis.averageLength')}</div> + <div class="name-stat-value"> + {results.nameDepths.average.toFixed(1)} + {$t('results.nameAnalysis.chars')} + </div> </div> </div> </div> @@ -406,10 +434,10 @@ www IN A 192.0.2.1" <div class="analysis-card"> <h4> <Icon name="maximize" size="sm" /> - Largest Record + {$t('results.largestRecord.title')} </h4> <div class="largest-record"> - <div class="record-size">{results.largestRecord.size} bytes</div> + <div class="record-size">{results.largestRecord.size} {$t('results.largestRecord.bytes')}</div> <div class="record-details"> <div class="record-owner">{results.largestRecord.record.owner}</div> <div class="record-type-data"> @@ -424,25 +452,25 @@ www IN A 192.0.2.1" <div class="health-card"> <h4> <Icon name="shield" size="sm" /> - Zone Health Checks + {$t('results.healthChecks.title')} </h4> <div class="health-checks"> <div class="health-check {results.sanityChecks.hasSoa ? 'pass' : 'fail'}"> <Icon name={results.sanityChecks.hasSoa ? 'check-circle' : 'x-circle'} size="sm" /> - <span>SOA Record Present</span> + <span>{$t('results.healthChecks.soaPresent')}</span> </div> <div class="health-check {results.sanityChecks.hasNs ? 'pass' : 'fail'}"> <Icon name={results.sanityChecks.hasNs ? 'check-circle' : 'x-circle'} size="sm" /> - <span>NS Records Present</span> + <span>{$t('results.healthChecks.nsPresent')}</span> </div> <div class="health-check {results.sanityChecks.duplicates.length === 0 ? 'pass' : 'warn'}"> <Icon name={results.sanityChecks.duplicates.length === 0 ? 'check-circle' : 'alert-triangle'} size="sm" /> <span> {results.sanityChecks.duplicates.length === 0 - ? 'No Duplicate Records' - : `${results.sanityChecks.duplicates.length} Duplicate Record${results.sanityChecks.duplicates.length !== 1 ? 's' : ''}`} + ? $t('results.healthChecks.noDuplicates') + : `${results.sanityChecks.duplicates.length} ${results.sanityChecks.duplicates.length !== 1 ? $t('results.healthChecks.duplicateRecordsPlural') : $t('results.healthChecks.duplicateRecords')}`} </span> </div> @@ -453,8 +481,8 @@ www IN A 192.0.2.1" /> <span> {results.sanityChecks.orphanedGlue.length === 0 - ? 'No Orphaned Glue Records' - : `${results.sanityChecks.orphanedGlue.length} Orphaned Glue Record${results.sanityChecks.orphanedGlue.length !== 1 ? 's' : ''}`} + ? $t('results.healthChecks.noOrphanedGlue') + : `${results.sanityChecks.orphanedGlue.length} ${results.sanityChecks.orphanedGlue.length !== 1 ? $t('results.healthChecks.orphanedGluePlural') : $t('results.healthChecks.orphanedGlue')}`} </span> </div> </div> @@ -467,34 +495,30 @@ www IN A 192.0.2.1" <div class="education-card"> <div class="education-grid"> <div class="education-item info-panel"> - <h4>Zone Statistics</h4> + <h4>{$t('education.statistics.title')}</h4> <p> - Zone statistics help understand DNS structure, identify optimization opportunities, and spot potential issues. - Analyze record distribution, TTL patterns, and naming conventions for better zone management. + {$t('education.statistics.content')} </p> </div> <div class="education-item info-panel"> - <h4>TTL Strategy</h4> + <h4>{$t('education.ttlStrategy.title')}</h4> <p> - TTL distribution reveals caching patterns. Short TTLs enable quick changes but increase DNS load. Long TTLs - reduce queries but slow propagation. Balance based on change frequency and traffic patterns. + {$t('education.ttlStrategy.content')} </p> </div> <div class="education-item info-panel"> - <h4>Record Analysis</h4> + <h4>{$t('education.recordAnalysis.title')}</h4> <p> - Record type distribution shows zone complexity. Heavy A/AAAA records suggest web services, many MX records - indicate mail infrastructure, and diverse types show comprehensive DNS usage. + {$t('education.recordAnalysis.content')} </p> </div> <div class="education-item info-panel"> - <h4>Health Monitoring</h4> + <h4>{$t('education.healthMonitoring.title')}</h4> <p> - Regular zone analysis catches configuration drift, identifies duplicates, and ensures essential records exist. - Use statistics to track zone growth and optimize DNS performance over time. + {$t('education.healthMonitoring.content')} </p> </div> </div> diff --git a/src/lib/composables/useExamples.svelte.ts b/src/lib/composables/useExamples.svelte.ts index 0daaf5fa..5723c2c7 100644 --- a/src/lib/composables/useExamples.svelte.ts +++ b/src/lib/composables/useExamples.svelte.ts @@ -3,7 +3,7 @@ * Handles example loading and selection tracking */ -export function useExamples<T extends { [key: string]: any }>(examples: T[]) { +export function useExamples<T extends { [key: string]: any }>(getExamples: () => T[]) { let selectedIndex = $state<number | null>(null); function select(index: number) { @@ -20,6 +20,7 @@ export function useExamples<T extends { [key: string]: any }>(examples: T[]) { function getSelected(): T | null { if (selectedIndex === null) return null; + const examples = getExamples(); return examples[selectedIndex] ?? null; } diff --git a/src/lib/content/examples/toolName.ts b/src/lib/content/examples/toolName.ts new file mode 100644 index 00000000..ab92cf09 --- /dev/null +++ b/src/lib/content/examples/toolName.ts @@ -0,0 +1 @@ +export const toolNameExamples = [{ name: '', description: '', value: '' }]; diff --git a/src/lib/content/ipv6-privacy-addresses.ts b/src/lib/content/ipv6-privacy-addresses.ts index 5c0cc8a8..85fc64da 100644 --- a/src/lib/content/ipv6-privacy-addresses.ts +++ b/src/lib/content/ipv6-privacy-addresses.ts @@ -1,340 +1,192 @@ -export const ipv6PrivacyContent = { - title: 'IPv6 Privacy Addresses (RFC 4941/8981)', - description: - 'SLAAC privacy extensions: temporary vs stable interface identifiers, how they protect privacy, and configuration guidance.', - - sections: { - overview: { - title: 'What are IPv6 Privacy Addresses?', - content: `IPv6 privacy addresses (temporary addresses) are automatically generated to prevent tracking based on stable interface identifiers. They're created alongside stable addresses and change periodically. - -Without privacy extensions, devices use predictable interface identifiers (often based on MAC addresses), making them trackable across networks.`, - }, - - problem: { - title: 'The Privacy Problem', - content: `Standard IPv6 addresses often contain predictable interface identifiers that remain constant across different networks, creating privacy concerns similar to a permanent device fingerprint.`, - }, - }, - - addressTypes: [ - { - type: 'Stable Address (Standard SLAAC)', - formation: 'Prefix + EUI-64 or configured interface ID', - example: '2001:db8:1234:5678:21a:2bff:fe3c:4d5e', - characteristics: [ - 'Interface identifier stays the same across networks', - 'Often derived from MAC address using EUI-64', - 'Predictable and trackable across network changes', - 'Required for some services that need consistent addressing', - ], - privacy: 'Poor - enables tracking across networks', - }, - { - type: 'Temporary Address (Privacy Extension)', - formation: 'Prefix + cryptographically generated random bits', - example: '2001:db8:1234:5678:a1b2:c3d4:e5f6:7890', - characteristics: [ - 'Randomly generated interface identifier', - 'Changes periodically (daily by default)', - 'Multiple temporary addresses can coexist', - 'Used for outbound connections by default', - ], - privacy: 'Good - prevents cross-network tracking', - }, - { - type: 'Stable Private Address (RFC 7217)', - formation: 'Prefix + hash of secret key + network info', - example: '2001:db8:1234:5678:9abc:def0:1234:5678', - characteristics: [ - 'Stable within the same network', - 'Changes when moving to different networks', - 'More predictable than temporary addresses', - 'Good balance of privacy and stability', - ], - privacy: 'Better - network-specific but stable', - }, - ], - - howItWorks: { - title: 'How Privacy Extensions Work', - - addressGeneration: [ - 'Device receives Router Advertisement with prefix', - 'Creates stable address using EUI-64 or configured method', - 'Generates temporary address using cryptographic random bits', - 'Both addresses are assigned to the same interface', - 'Temporary address preferred for outbound connections', - ], - - temporaryLifecycle: [ - 'New temporary address generated periodically', - 'Old temporary addresses remain valid until expiry', - 'Multiple temporary addresses can coexist', - 'Addresses have preferred and valid lifetimes', - 'Deprecated addresses still accept incoming traffic', - ], - - defaultBehavior: [ - 'Outbound connections use temporary addresses', - 'Inbound services use stable addresses', - 'Applications can request specific address types', - 'Operating system manages address selection automatically', - ], - }, - - lifetimes: { - title: 'Address Lifetimes', - - preferredLifetime: { - description: 'How long address is preferred for new connections', - typical: '1 day (86400 seconds)', - behavior: 'After expiry, address can receive but not initiate connections', - }, - - validLifetime: { - description: 'How long address remains usable', - typical: '7 days (604800 seconds)', - behavior: 'After expiry, address is completely removed', - }, - - regenerationInterval: { - description: 'How often new temporary addresses are created', - typical: '5 minutes to 24 hours', - behavior: 'New address created before old one expires', - }, - - maxTempAddresses: { - description: 'Maximum temporary addresses per prefix', - typical: '5-10 addresses', - behavior: 'Oldest addresses removed when limit reached', - }, - }, - - osImplementations: { - title: 'Operating System Support', - - windows: { - os: 'Windows', - defaultBehavior: 'Privacy extensions enabled by default (Vista+)', - configuration: [ - 'netsh interface ipv6 set global randomizeidentifiers=enabled', - 'netsh interface ipv6 set privacy state=enabled', - 'Registry: HKLM\\System\\CurrentControlSet\\Services\\Tcpip6\\Parameters', - ], - commands: ['netsh interface ipv6 show privacy', 'netsh interface ipv6 show addresses'], - }, - - linux: { - os: 'Linux', - defaultBehavior: 'Varies by distribution, often disabled by default', - configuration: [ - 'sysctl net.ipv6.conf.all.use_tempaddr=2', - 'sysctl net.ipv6.conf.default.use_tempaddr=2', - '/proc/sys/net/ipv6/conf/*/use_tempaddr', - ], - values: ['0 = Disabled', '1 = Enabled but prefer stable', '2 = Enabled and prefer temporary'], - commands: ['ip -6 addr show scope global', 'cat /proc/sys/net/ipv6/conf/eth0/use_tempaddr'], - }, - - macos: { - os: 'macOS', - defaultBehavior: 'Privacy extensions enabled by default', - configuration: [ - 'Built into system preferences', - 'networksetup command line tool', - 'System-wide setting affects all interfaces', - ], - commands: ['ifconfig | grep inet6', 'networksetup -getinfo Wi-Fi'], - }, - - android: { - os: 'Android', - defaultBehavior: 'Privacy extensions enabled by default (Android 8+)', - configuration: [ - 'Settings > Network & Internet > Advanced', - 'Developer options for advanced control', - 'Per-network configuration possible', - ], - behavior: 'Randomizes MAC and uses privacy addresses', - }, - }, - - identifyingAddresses: [ - { - method: 'Interface Identifier Pattern', - stable: "Often contains 'fffe' in middle (EUI-64) or predictable pattern", - temporary: 'Random-looking interface identifier', - example: 'Stable: ::21a:2bff:fe3c:4d5e vs Temporary: ::a1b2:c3d4:e5f6:7890', - }, - { - method: 'Address Consistency', - stable: 'Same interface ID across different network prefixes', - temporary: 'Different interface ID on each network', - example: 'Device keeps same ::21a:2bff:fe3c:4d5e on all networks vs random on each', - }, - { - method: 'Command Output', - stable: "Often labeled as 'permanent' or primary", - temporary: "Labeled as 'temporary' or 'deprecated'", - example: "Linux ip command shows 'temporary' flag", - }, - ], - - troubleshooting: [ - { - issue: 'Privacy addresses not working', - symptoms: ['Same IPv6 address on different networks', 'Tracking concerns'], - diagnosis: 'Check OS privacy extension settings', - solutions: [ - 'Enable privacy extensions in OS settings', - 'Verify router supports SLAAC', - 'Check for disabled IPv6 privacy in network manager', - ], - }, - { - issue: 'Too many IPv6 addresses', - symptoms: ['Multiple IPv6 addresses per interface', 'Address list constantly changing'], - diagnosis: 'Privacy extensions working normally', - solutions: [ - 'This is normal behavior for privacy extensions', - 'Adjust regeneration timers if needed', - 'Reduce max temporary addresses if causing issues', - ], - }, - { - issue: 'Applications using wrong address', - symptoms: ['Server not reachable', 'Unexpected source addresses'], - diagnosis: 'Address selection preference issues', - solutions: [ - 'Configure application to bind specific addresses', - 'Adjust address selection policy', - 'Use stable addresses for server applications', - ], - }, - { - issue: 'Privacy addresses not preferred', - symptoms: ['Always using stable addresses for outbound'], - diagnosis: 'Address selection policy favoring stable addresses', - solutions: [ - 'Configure temporary address preference', - 'Check application-specific settings', - 'Verify privacy extension configuration', - ], - }, - ], - - securityConsiderations: [ - { - aspect: 'Privacy Protection', - benefits: [ - 'Prevents device tracking across networks', - 'Makes traffic analysis more difficult', - 'Reduces correlation of activities', - 'Protects against location tracking', - ], - limitations: [ - 'Application-layer tracking still possible', - 'DNS queries may reveal information', - 'Stable addresses still exposed for services', - 'Requires proper application configuration', - ], - }, - { - aspect: 'Network Management', - benefits: [ - 'Devices harder to target maliciously', - 'Reduces effectiveness of IP-based blocking', - 'Makes reconnaissance more difficult', - ], - challenges: [ - 'Harder to whitelist specific devices', - 'Complicates network troubleshooting', - 'May interfere with IP-based access control', - 'Requires different monitoring approaches', - ], - }, - ], - - bestPractices: [ - 'Enable privacy extensions on client devices', - 'Use stable addresses only for servers and infrastructure', - 'Configure appropriate regeneration intervals', - 'Monitor for privacy extension support in applications', - 'Balance privacy with network management needs', - 'Document which services require stable addressing', - 'Test applications with privacy addresses enabled', - 'Consider RFC 7217 stable privacy addresses for better balance', - ], - - whenToUse: [ - { - scenario: 'Client Devices', - recommendation: 'Enable privacy extensions', - reasoning: 'Protects user privacy without impacting functionality', - configuration: 'Prefer temporary addresses for outbound connections', - }, - { - scenario: 'Servers', - recommendation: 'Use stable addresses', - reasoning: 'Consistent addressing needed for services', - configuration: 'Disable privacy extensions or use stable addresses only', - }, - { - scenario: 'IoT Devices', - recommendation: 'Consider device requirements', - reasoning: 'Balance privacy with device management needs', - configuration: 'May need stable addresses for remote management', - }, - { - scenario: 'Enterprise Networks', - recommendation: 'Policy-based approach', - reasoning: 'Different requirements for different device types', - configuration: 'Client devices: privacy on, servers: stable addresses', - }, - ], - - commonMistakes: [ - 'Assuming all IPv6 addresses are permanent', - 'Not testing applications with privacy addresses', - 'Blocking temporary addresses in firewalls', - 'Using temporary addresses for server services', - 'Not understanding address selection preferences', - 'Confusing temporary addresses with link-local addresses', - 'Expecting consistent addressing with privacy extensions enabled', - ], - - quickReference: { - addressTypes: [ - 'Stable: Same interface ID everywhere (trackable)', - 'Temporary: Random interface ID, changes periodically (private)', - 'Stable Privacy (7217): Stable per-network, changes between networks', - ], - - identification: [ - 'EUI-64 pattern (fffe in middle) = stable address', - 'Random-looking interface ID = temporary address', - 'Multiple addresses per interface = privacy extensions active', - ], - - configuration: [ - 'Windows: netsh interface ipv6 set privacy state=enabled', - 'Linux: sysctl net.ipv6.conf.all.use_tempaddr=2', - 'macOS: System Preferences > Network > Advanced', - ], - - troubleshooting: [ - 'Multiple IPv6 addresses = normal with privacy extensions', - 'Same address everywhere = privacy extensions disabled', - 'Services unreachable = check stable address binding', - ], - }, - - tools: [ - { tool: 'ip -6 addr show', purpose: 'Show all IPv6 addresses with flags (Linux)' }, - { tool: 'ipconfig /all', purpose: 'Display IPv6 addresses and configuration (Windows)' }, - { tool: 'ifconfig', purpose: 'Show network interfaces and addresses (Unix/macOS)' }, - { tool: 'netsh interface ipv6 show addresses', purpose: 'Detailed IPv6 address info (Windows)' }, - { tool: 'sysctl net.ipv6.conf.all.use_tempaddr', purpose: 'Check privacy extension status (Linux)' }, - ], +import { get } from 'svelte/store'; +import { t, tRaw } from '$lib/stores/language'; + +export interface OSImplementation { + os: string; + defaultBehavior: string; + configuration: string[]; + commands?: string[]; + values?: string[]; + behavior?: string; +} + +export interface OSImplementations { + title: string; + windows?: OSImplementation; + linux?: OSImplementation; + macos?: OSImplementation; + android?: OSImplementation; + [key: string]: OSImplementation | string | undefined; +} + +// Type guard function +export function isOSImplementation(value: unknown): value is OSImplementation { + return ( + typeof value === 'object' && + value !== null && + 'os' in value && + 'defaultBehavior' in value && + 'configuration' in value + ); +} + +export const getIpv6PrivacyContent = () => { + const $t = get(t); + const $tRaw = get(tRaw); + + // Transform osImplementations array into object with OS keys + const osImplementationsRaw = $tRaw('pages/ipv6-privacy.ipv6Privacy.osImplementations') || {}; + const osImplementations: OSImplementations = (osImplementationsRaw.implementations || []).reduce( + (acc: OSImplementations, impl: OSImplementation) => { + if (impl.os) { + acc[impl.os.toLowerCase()] = impl; + } + return acc; + }, + { title: osImplementationsRaw.title || 'Operating System Support' }, + ); + + return { + title: $t('pages/ipv6-privacy.ipv6Privacy.title'), + description: $t('pages/ipv6-privacy.ipv6Privacy.description'), + + sections: { + overview: { + title: $t('pages/ipv6-privacy.ipv6Privacy.sections.overview.title'), + content: $t('pages/ipv6-privacy.ipv6Privacy.sections.overview.content'), + }, + problem: { + title: $t('pages/ipv6-privacy.ipv6Privacy.sections.problem.title'), + content: $t('pages/ipv6-privacy.ipv6Privacy.sections.problem.content'), + }, + }, + + addressTypes: { + title: $t('pages/ipv6-privacy.ipv6Privacy.addressTypes.title'), + types: $tRaw('pages/ipv6-privacy.ipv6Privacy.addressTypes.types') || [], + }, + + howItWorks: { + title: $t('pages/ipv6-privacy.ipv6Privacy.howItWorks.title'), + addressGeneration: $tRaw('pages/ipv6-privacy.ipv6Privacy.howItWorks.addressGeneration') || [], + temporaryLifecycle: $tRaw('pages/ipv6-privacy.ipv6Privacy.howItWorks.temporaryLifecycle') || [], + defaultBehavior: $tRaw('pages/ipv6-privacy.ipv6Privacy.howItWorks.defaultBehavior') || [], + }, + + lifetimes: { + title: $t('pages/ipv6-privacy.ipv6Privacy.lifetimes.title'), + preferredLifetime: + $tRaw('pages/ipv6-privacy.ipv6Privacy.lifetimes.preferredLifetime') || + ({} as { + description: string; + typical: string; + behavior: string; + }), + validLifetime: + $tRaw('pages/ipv6-privacy.ipv6Privacy.lifetimes.validLifetime') || + ({} as { + description: string; + typical: string; + behavior: string; + }), + regenerationInterval: + $tRaw('pages/ipv6-privacy.ipv6Privacy.lifetimes.regenerationInterval') || + ({} as { + description: string; + typical: string; + behavior: string; + }), + maxTempAddresses: + $tRaw('pages/ipv6-privacy.ipv6Privacy.lifetimes.maxTempAddresses') || + ({} as { + description: string; + typical: string; + behavior: string; + }), + }, + + osImplementations, + + identifyingAddresses: { + title: $t('pages/ipv6-privacy.ipv6Privacy.identifyingAddresses.title'), + methods: + $tRaw('pages/ipv6-privacy.ipv6Privacy.identifyingAddresses.methods') || + ([] as Array<{ + method: string; + stable: string; + temporary: string; + example: string; + }>), + }, + + troubleshooting: { + title: $t('pages/ipv6-privacy.ipv6Privacy.troubleshooting.title'), + issues: + $tRaw('pages/ipv6-privacy.ipv6Privacy.troubleshooting.issues') || + ([] as Array<{ + issue: string; + symptoms: string[]; + diagnosis: string; + solutions: string[]; + }>), + }, + + securityConsiderations: { + title: $t('pages/ipv6-privacy.ipv6Privacy.securityConsiderations.title'), + aspects: + $tRaw('pages/ipv6-privacy.ipv6Privacy.securityConsiderations.aspects') || + ([] as Array<{ + aspect: string; + benefits?: string[]; + limitations?: string[]; + challenges?: string[]; + }>), + }, + + bestPractices: { + title: $t('pages/ipv6-privacy.ipv6Privacy.bestPractices.title'), + practices: $tRaw('pages/ipv6-privacy.ipv6Privacy.bestPractices.practices') || [], + }, + + whenToUse: { + title: $t('pages/ipv6-privacy.ipv6Privacy.whenToUse.title'), + scenarios: + $tRaw('pages/ipv6-privacy.ipv6Privacy.whenToUse.scenarios') || + ([] as Array<{ + scenario: string; + recommendation: string; + reasoning: string; + configuration: string; + }>), + }, + + commonMistakes: { + title: $t('pages/ipv6-privacy.ipv6Privacy.commonMistakes.title'), + mistakes: $tRaw('pages/ipv6-privacy.ipv6Privacy.commonMistakes.mistakes') || [], + }, + + quickReference: { + title: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.title'), + addressTypesTitle: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.addressTypesTitle'), + identificationTitle: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.identificationTitle'), + configurationTitle: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.configurationTitle'), + troubleshootingTitle: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.troubleshootingTitle'), + keyRuleTitle: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.keyRuleTitle'), + keyRule: $t('pages/ipv6-privacy.ipv6Privacy.quickReference.keyRule'), + addressTypes: $tRaw('pages/ipv6-privacy.ipv6Privacy.quickReference.addressTypes') || [], + identification: $tRaw('pages/ipv6-privacy.ipv6Privacy.quickReference.identification') || [], + configuration: $tRaw('pages/ipv6-privacy.ipv6Privacy.quickReference.configuration') || [], + troubleshooting: $tRaw('pages/ipv6-privacy.ipv6Privacy.quickReference.troubleshooting') || [], + }, + + tools: { + title: $t('pages/ipv6-privacy.ipv6Privacy.testingTools.title'), + tools: + $tRaw('pages/ipv6-privacy.ipv6Privacy.testingTools.tools') || + ([] as Array<{ + tool: string; + purpose: string; + }>), + }, + }; }; diff --git a/src/lib/content/reverse-zones.ts b/src/lib/content/reverse-zones.ts index fa9684f6..ebd6d00e 100644 --- a/src/lib/content/reverse-zones.ts +++ b/src/lib/content/reverse-zones.ts @@ -1,286 +1,132 @@ -export const reverseZonesContent = { - title: 'Reverse Zones for CIDR Delegation', - description: - 'Minimal reverse DNS zones needed to properly delegate IPv4 and IPv6 CIDR blocks with practical examples.', +import { get } from 'svelte/store'; +import { t, tRaw } from '$lib/stores/language'; - sections: { - overview: { - title: 'What are Reverse Zones?', - content: `Reverse DNS zones map IP addresses back to domain names using special domains (.in-addr.arpa for IPv4, .ip6.arpa for IPv6). When you're delegated a CIDR block, you need to create the corresponding reverse zones for proper DNS operation. +export const getReverseZonesContent = () => { + const $t = get(t); + const $tRaw = get(tRaw); -Reverse zones are essential for mail servers, logging, security tools, and network troubleshooting.`, - }, - - delegation: { - title: 'How Reverse Delegation Works', - content: `Your ISP or RIR delegates reverse DNS authority for your IP blocks to your DNS servers. You then create the reverse zones and populate them with PTR records that map IP addresses to hostnames. - -The delegation happens at specific boundaries that align with IP addressing hierarchy.`, - }, - }, - - ipv4Zones: { - title: 'IPv4 Reverse Zones (in-addr.arpa)', - - classfullBoundaries: [ - { - cidr: '/8', - example: '10.0.0.0/8', - reverseZone: '10.in-addr.arpa', - description: 'Entire Class A network', - delegation: 'Usually handled by RIRs, not end users', - }, - { - cidr: '/16', - example: '172.16.0.0/16', - reverseZone: '16.172.in-addr.arpa', - description: 'Class B network', - delegation: 'Large organizations or ISPs', - }, - { - cidr: '/24', - example: '192.168.1.0/24', - reverseZone: '1.168.192.in-addr.arpa', - description: 'Class C network - most common delegation', - delegation: 'Standard small business / organization', - }, - ], + return { + title: $t('pages/reverse-zones.reverseZones.title'), + description: $t('pages/reverse-zones.reverseZones.description'), - classlessDelegation: [ - { - cidr: '/25', - example: '203.0.113.0/25', - addresses: '128 addresses', - problem: "Doesn't align with octet boundaries", - solution: 'Use CNAME delegation with bit notation', - zones: ['0-25.113.0.203.in-addr.arpa', '1-25.113.0.203.in-addr.arpa'], - }, - { - cidr: '/26', - example: '203.0.113.64/26', - addresses: '64 addresses', - problem: "Quarter of /24, doesn't align with octets", - solution: 'CNAME delegation for 64-127 range', - zones: ['64-26.113.0.203.in-addr.arpa'], - }, - { - cidr: '/27', - example: '203.0.113.128/27', - addresses: '32 addresses', - problem: 'Eighth of /24, complex delegation', - solution: 'CNAME delegation with range notation', - zones: ['128-27.113.0.203.in-addr.arpa'], - }, - ], - - practicalExamples: [ - { - scenario: 'Small Business with /24', - network: '192.0.2.0/24', - reverseZone: '2.0.192.in-addr.arpa', - ptrRecords: [ - '1.2.0.192.in-addr.arpa. IN PTR mail.example.com.', - '10.2.0.192.in-addr.arpa. IN PTR web.example.com.', - '50.2.0.192.in-addr.arpa. IN PTR server1.example.com.', - ], - delegation: 'ISP delegates entire /24 reverse zone to customer DNS', - }, - { - scenario: 'Medium Business with /23', - network: '198.51.100.0/23', - reverseZones: ['100.51.198.in-addr.arpa', '101.51.198.in-addr.arpa'], - description: 'Two /24 reverse zones needed', - delegation: 'ISP delegates both zones or uses automation', - }, - ], - }, - - ipv6Zones: { - title: 'IPv6 Reverse Zones (ip6.arpa)', - - nibbleBoundaries: [ - { - cidr: '/32', - example: '2001:db8::/32', - reverseZone: '8.b.d.0.1.0.0.2.ip6.arpa', - description: 'Typical RIR allocation to ISP', - delegation: 'RIR delegates to ISP', - }, - { - cidr: '/48', - example: '2001:db8:1234::/48', - reverseZone: '4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa', - description: 'Typical site allocation', - delegation: 'ISP delegates to organization', + sections: { + overview: { + title: $t('pages/reverse-zones.reverseZones.sections.overview.title'), + content: $t('pages/reverse-zones.reverseZones.sections.overview.content'), }, - { - cidr: '/56', - example: '2001:db8:1234:ab00::/56', - reverseZone: '0.0.b.a.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa', - description: 'Large home or small business', - delegation: 'Common residential allocation', + delegation: { + title: $t('pages/reverse-zones.reverseZones.sections.delegation.title'), + content: $t('pages/reverse-zones.reverseZones.sections.delegation.content'), }, - { - cidr: '/64', - example: '2001:db8:1234:5678::/64', - reverseZone: '8.7.6.5.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa', - description: 'Single subnet', - delegation: 'Individual subnet reverse zone', - }, - ], - - practicalExamples: [ - { - scenario: 'Enterprise with /48', - network: '2001:db8:1234::/48', - reverseZone: '4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa', - subZones: [ - '0.0.0.0.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)', - '1.0.0.0.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)', - 'a.b.c.d.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)', - ], - management: 'Create master zone, delegate individual /64s as needed', - }, - ], - }, - - zoneCreation: { - title: 'Creating Reverse Zones', - - ipv4Example: { - network: '192.0.2.0/24', - zoneName: '2.0.192.in-addr.arpa', - zoneFile: `$TTL 86400 -2.0.192.in-addr.arpa. IN SOA ns1.example.com. hostmaster.example.com. ( - 2024010101 ; serial - 3600 ; refresh - 1800 ; retry - 1209600 ; expire - 86400 ) ; minimum - - IN NS ns1.example.com. - IN NS ns2.example.com. - -1 IN PTR mail.example.com. -10 IN PTR web.example.com. -50 IN PTR server1.example.com. -100 IN PTR workstation.example.com.`, - - explanation: [ - 'Zone name is network reversed + in-addr.arpa', - 'SOA record defines zone authority and parameters', - 'NS records point to authoritative name servers', - 'PTR records map IP to hostname (just last octet for /24)', - ], }, - ipv6Example: { - network: '2001:db8:1234::/48', - zoneName: '4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa', - zoneFile: `$TTL 86400 -4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa. IN SOA ns1.example.com. hostmaster.example.com. ( - 2024010101 ; serial - 3600 ; refresh - 1800 ; retry - 1209600 ; expire - 86400 ) ; minimum - - IN NS ns1.example.com. - IN NS ns2.example.com. - -; Delegate /64 subnets -0.0.0.0 IN NS ns1.example.com. -0.0.0.0 IN NS ns2.example.com. - -1.0.0.0 IN NS ns1.example.com. -1.0.0.0 IN NS ns2.example.com.`, - - explanation: [ - 'Zone name is full prefix in nibble format + ip6.arpa', - 'Each hex digit becomes separate label in reverse', - 'Can delegate individual /64 subnets within /48', - 'Much longer zone names than IPv4', - ], + ipv4Zones: { + title: $t('pages/reverse-zones.reverseZones.ipv4Zones.title'), + classfullBoundaries: + $tRaw('pages/reverse-zones.reverseZones.ipv4Zones.classfullBoundaries') || + ([] as Array<{ + cidr: string; + example: string; + reverseZone: string; + description: string; + delegation: string; + }>), + classlessDelegation: + $tRaw('pages/reverse-zones.reverseZones.ipv4Zones.classlessDelegation') || + ([] as Array<{ + cidr: string; + example: string; + addresses: string; + problem: string; + solution: string; + zones: string[]; + }>), + practicalExamples: + $tRaw('pages/reverse-zones.reverseZones.ipv4Zones.practicalExamples') || + ([] as Array<{ + scenario: string; + network: string; + reverseZone?: string; + reverseZones?: string[]; + ptrRecords?: string[]; + description?: string; + delegation: string; + }>), }, - }, - delegationScenarios: [ - { - scenario: 'ISP to Customer (/24)', - delegation: "ISP adds NS records for customer's DNS servers in their reverse zone", - customerActions: [ - 'Set up DNS servers with reverse zone', - 'Create PTR records for important hosts', - 'Test reverse lookups work correctly', - ], - ispActions: [ - 'Add NS delegation in parent zone', - 'Update WHOIS records if required', - 'Verify customer DNS servers are working', - ], + ipv6Zones: { + title: $t('pages/reverse-zones.reverseZones.ipv6Zones.title'), + nibbleBoundaries: + $tRaw('pages/reverse-zones.reverseZones.ipv6Zones.nibbleBoundaries') || + ([] as Array<{ + cidr: string; + example: string; + reverseZone: string; + description: string; + delegation: string; + }>), + practicalExamples: + $tRaw('pages/reverse-zones.reverseZones.ipv6Zones.practicalExamples') || + ([] as Array<{ + scenario: string; + network: string; + reverseZone: string; + subZones: string[]; + management: string; + }>), }, - { - scenario: 'Organization Internal (/16 split)', - delegation: 'Large organization splits /16 into /24s for different departments', - process: [ - 'Create master zone for entire /16', - 'Delegate individual /24s to department DNS servers', - 'Each department manages their own PTR records', - ], - }, - ], - - bestPractices: [ - 'Always create reverse zones for your allocated IP blocks', - 'Ensure PTR records match forward DNS (A/AAAA records)', - 'Use consistent naming conventions for reverse records', - 'Monitor reverse DNS resolution for important services', - 'Automate PTR record creation/updates where possible', - 'Test reverse lookups from multiple external locations', - 'Keep reverse zone serial numbers updated when making changes', - ], - troubleshooting: [ - { - issue: 'Reverse lookups not working', - causes: ['Zone not delegated', 'DNS server not responding', 'PTR records missing'], - diagnosis: 'Use dig -x [ip] to test reverse resolution', - solution: 'Check delegation, verify DNS server config, add PTR records', + zoneCreation: { + title: $t('pages/reverse-zones.reverseZones.zoneCreation.title'), + ipv4Example: + $tRaw('pages/reverse-zones.reverseZones.zoneCreation.ipv4Example') || + ({} as { + network: string; + zoneName: string; + zoneFile: string; + explanation: string[]; + }), + ipv6Example: + $tRaw('pages/reverse-zones.reverseZones.zoneCreation.ipv6Example') || + ({} as { + network: string; + zoneName: string; + zoneFile: string; + explanation: string[]; + }), }, - { - issue: 'Mail servers rejecting email', - causes: ['Missing PTR record for mail server IP', "PTR doesn't match HELO name"], - diagnosis: 'Check mail server logs, test PTR record', - solution: 'Create PTR record that matches mail server hostname', - }, - { - issue: 'IPv6 reverse lookups failing', - causes: ['Complex nibble format errors', 'Zone delegation issues'], - diagnosis: 'Verify zone name format, test with dig -x', - solution: 'Double-check nibble format, verify IPv6 DNS configuration', - }, - ], - quickReference: { - zoneFormulas: [ - 'IPv4 /24: [third].[second].[first].in-addr.arpa', - 'IPv4 /16: [second].[first].in-addr.arpa', - 'IPv6 /48: [nibbles-reversed].ip6.arpa', - 'IPv6 /64: [more-nibbles-reversed].ip6.arpa', - ], - - essentialRecords: [ - 'SOA record (required for all zones)', - 'NS records (delegation to authoritative servers)', - 'PTR records (actual IP to name mappings)', - 'Match PTR with forward A/AAAA records', - ], - }, + delegationScenarios: + $tRaw('pages/reverse-zones.reverseZones.delegationScenarios.scenarios') || + ([] as Array<{ + scenario: string; + delegation: string; + customerActions?: string[]; + ispActions?: string[]; + process?: string[]; + }>), + + bestPractices: $tRaw('pages/reverse-zones.reverseZones.bestPractices.practices') || [], + + troubleshooting: + $tRaw('pages/reverse-zones.reverseZones.troubleshooting.issues') || + ([] as Array<{ + issue: string; + causes: string[]; + diagnosis: string; + solution: string; + }>), + + quickReference: { + zoneFormulas: $tRaw('pages/reverse-zones.reverseZones.quickReference.zoneFormulas') || [], + essentialRecords: $tRaw('pages/reverse-zones.reverseZones.quickReference.essentialRecords') || [], + }, - tools: [ - { tool: 'dig -x [ip]', purpose: 'Test reverse DNS lookup' }, - { tool: 'nslookup [ip]', purpose: 'Basic reverse lookup test' }, - { tool: 'host [ip]', purpose: 'Simple reverse resolution check' }, - { tool: 'online reverse DNS tools', purpose: 'Test from external perspective' }, - ], + tools: + $tRaw('pages/reverse-zones.reverseZones.testingTools.tools') || + ([] as Array<{ + tool: string; + purpose: string; + }>), + }; }; diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts new file mode 100644 index 00000000..35739ac9 --- /dev/null +++ b/src/lib/i18n/index.ts @@ -0,0 +1,222 @@ +/** + * Lightweight i18n System + * Handles translation loading, interpolation, and fallback + */ + +import { DEFAULT_LANGUAGE } from './supported-languages'; + +type TranslationObject = Record<string, any>; +type InterpolationParams = Record<string, string | number>; + +/** + * Simple string interpolation + * "Hello {name}" + { name: "World" } β†’ "Hello World" + */ +function interpolate(template: string, params: InterpolationParams): string { + if (!params) return template; + + return template.replace(/\{(\w+)\}/g, (match, key) => { + return params[key] !== undefined ? String(params[key]) : match; + }); +} + +/** + * Deep get value from nested object by dot-notation key + * get({ foo: { bar: 'baz' } }, 'foo.bar') β†’ 'baz' + * Protected against prototype pollution + */ +function getNestedValue(obj: TranslationObject, key: string): any { + const keys = key.split('.'); + let result: any = obj; + + for (const k of keys) { + // Prevent prototype pollution + if (k === '__proto__' || k === 'constructor' || k === 'prototype') { + return undefined; + } + + if (result && typeof result === 'object' && Object.prototype.hasOwnProperty.call(result, k)) { + // Safe property access - already validated against prototype pollution above + // and using hasOwnProperty to ensure property exists on object itself + result = Object.getOwnPropertyDescriptor(result, k)?.value; + } else { + return undefined; + } + } + + return result; +} + +/** + * Handle pluralization + * Expects translation object like: + * { "zero": "no items", "one": "1 item", "other": "{count} items" } + */ +function pluralize(translation: any, count: number): string { + if (typeof translation !== 'object') { + return String(translation); + } + + if (count === 0 && translation.zero) { + return translation.zero; + } + if (count === 1 && translation.one) { + return translation.one; + } + if (translation.other) { + return translation.other; + } + + // Fallback to 'other' or first available + return translation.other || Object.values(translation)[0] || ''; +} + +/** + * Core translation class + */ +export class I18n { + private locale: string; + private fallbackLocale: string; + private translations: Record<string, TranslationObject> = {}; + + constructor(locale: string = DEFAULT_LANGUAGE, fallbackLocale: string = DEFAULT_LANGUAGE) { + this.locale = locale; + this.fallbackLocale = fallbackLocale; + } + + /** + * Set current locale + */ + setLocale(locale: string): void { + this.locale = locale; + } + + /** + * Get current locale + */ + getLocale(): string { + return this.locale; + } + + /** + * Add translations for a locale + */ + addTranslations(locale: string, translations: TranslationObject): void { + if (!this.translations[locale]) { + this.translations[locale] = {}; + } + this.translations[locale] = { ...this.translations[locale], ...translations }; + } + + /** + * Add translations for a specific namespace + */ + addNamespace(locale: string, namespace: string, translations: TranslationObject): void { + if (!this.translations[locale]) { + this.translations[locale] = {}; + } + this.translations[locale][namespace] = translations; + } + + /** + * Check if a translation key exists + */ + has(key: string, locale?: string): boolean { + const lang = locale || this.locale; + const translations = this.translations[lang]; + if (!translations) return false; + + const value = getNestedValue(translations, key); + return value !== undefined; + } + + /** + * Get translation by key with fallback and interpolation + */ + t(key: string, params?: InterpolationParams): string { + // Try current locale + let translation = this.getTranslation(this.locale, key); + + // Fallback to default locale + if (translation === undefined && this.locale !== this.fallbackLocale) { + translation = this.getTranslation(this.fallbackLocale, key); + } + + // Fallback to key itself + if (translation === undefined) { + console.warn(`[i18n] Missing translation for key: ${key}`); + return key; + } + + // Handle pluralization if count param provided + if (params && 'count' in params && typeof translation === 'object') { + translation = pluralize(translation, Number(params.count)); + } + + // Convert to string and interpolate + const stringValue = String(translation); + return params ? interpolate(stringValue, params) : stringValue; + } + + /** + * Get raw translation value from locale + */ + private getTranslation(locale: string, key: string): any { + const translations = this.translations[locale]; + if (!translations) return undefined; + + return getNestedValue(translations, key); + } + + /** + * Get raw translation value (without string conversion) with fallback + */ + getRaw(key: string): any { + // Try current locale + let translation = this.getTranslation(this.locale, key); + + // Fallback to default locale + if (translation === undefined && this.locale !== this.fallbackLocale) { + translation = this.getTranslation(this.fallbackLocale, key); + } + + // Return undefined if not found (don't return the key) + if (translation === undefined) { + console.warn(`[i18n] Missing translation for key: ${key}`); + return undefined; + } + + return translation; + } + + /** + * Get all translations for current locale (for debugging) + */ + getAll(): TranslationObject { + return this.translations[this.locale] || {}; + } +} + +/** + * Create singleton instance + */ +export const i18n = new I18n(); + +/** + * Convenience function for translation + */ +export function t(key: string, params?: InterpolationParams): string { + return i18n.t(key, params); +} + +/** + * Convenience function for raw translation values + */ +export function getRaw(key: string): any { + return i18n.getRaw(key); +} + +/** + * Export types + */ +export type { TranslationObject, InterpolationParams }; diff --git a/src/lib/i18n/lang-detector.ts b/src/lib/i18n/lang-detector.ts new file mode 100644 index 00000000..dbf8caf0 --- /dev/null +++ b/src/lib/i18n/lang-detector.ts @@ -0,0 +1,119 @@ +/** + * Language Detection + * Determines user's preferred language with fallback chain: + * 1. localStorage preference + * 2. URL path (/de/...) + * 3. Browser navigator.language + * 4. Default (en) + */ + +import { browser } from '$app/environment'; +import { DEFAULT_LANGUAGE, isSupported, getBrowserLanguage } from './supported-languages'; + +const STORAGE_KEY = 'ntb-language'; + +/** + * Detect language from URL path + * /de/settings β†’ 'de' + * /settings β†’ null + */ +export function detectLanguageFromURL(pathname: string): string | null { + const segments = pathname.split('/').filter(Boolean); + if (segments.length > 0) { + const firstSegment = segments[0]; + if (firstSegment && isSupported(firstSegment)) { + return firstSegment; + } + } + return null; +} + +/** + * Get language from localStorage + */ +export function getStoredLanguage(): string | null { + if (!browser) return null; + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored && isSupported(stored)) { + return stored; + } + } catch (error) { + console.warn('Failed to read language from localStorage:', error); + } + return null; +} + +/** + * Save language to localStorage + */ +export function setStoredLanguage(lang: string): void { + if (!browser) return; + try { + if (isSupported(lang)) { + localStorage.setItem(STORAGE_KEY, lang); + } + } catch (error) { + console.warn('Failed to save language to localStorage:', error); + } +} + +/** + * Detect user's preferred language + * Priority: localStorage > URL > navigator > default + */ +export function detectLanguage(pathname?: string): string { + // 1. Check localStorage + const stored = getStoredLanguage(); + if (stored) { + return stored; + } + + // 2. Check URL path + if (pathname) { + const urlLang = detectLanguageFromURL(pathname); + if (urlLang) { + return urlLang; + } + } + + // 3. Check browser language + const browserLang = getBrowserLanguage(); + if (browserLang !== DEFAULT_LANGUAGE) { + return browserLang; + } + + // 4. Default fallback + return DEFAULT_LANGUAGE; +} + +/** + * Get the base path (without language prefix) + * /de/settings β†’ /settings + * /settings β†’ /settings + */ +export function getBasePath(pathname: string): string { + const segments = pathname.split('/').filter(Boolean); + if (segments.length > 0 && segments[0] && isSupported(segments[0])) { + return '/' + segments.slice(1).join('/'); + } + return pathname; +} + +/** + * Build path with language prefix + * ('de', '/settings') β†’ '/de/settings' + * ('en', '/settings') β†’ '/settings' (English has no prefix) + */ +export function buildLocalizedPath(lang: string, path: string): string { + // English doesn't use prefix + if (lang === DEFAULT_LANGUAGE) { + return getBasePath(path); + } + + // Remove existing language prefix if present + const basePath = getBasePath(path); + + // Add language prefix + return `/${lang}${basePath}`; +} diff --git a/src/lib/i18n/page-translations.ts b/src/lib/i18n/page-translations.ts new file mode 100644 index 00000000..c9128272 --- /dev/null +++ b/src/lib/i18n/page-translations.ts @@ -0,0 +1,33 @@ +/** + * Static translations loader for page-specific content + * This ensures translations are available at build time for SSR + */ + +import { i18n } from './index'; + +// Import page translations statically +import ipv6NotationEn from './translations/en/pages/ipv6-notation.json'; + +/** + * Load page translations for a given locale + * This function is designed to be called during SSR/SSG + */ +export function loadPageTranslations(locale: string = 'en') { + // For now, we only support English, but this can be extended + if (locale === 'en' || !locale) { + i18n.addNamespace('en', 'ipv6Notation', ipv6NotationEn); + } + + // Set the locale + i18n.setLocale(locale); +} + +/** + * Ensure translations are loaded + */ +export function ensurePageTranslations(locale: string = 'en') { + // Check if translations are already loaded + if (!i18n.has('ipv6Notation.title', locale)) { + loadPageTranslations(locale); + } +} diff --git a/src/lib/i18n/supported-languages.ts b/src/lib/i18n/supported-languages.ts new file mode 100644 index 00000000..c0d78f30 --- /dev/null +++ b/src/lib/i18n/supported-languages.ts @@ -0,0 +1,80 @@ +/** + * Supported Languages Configuration + * Lists all languages available in the application + */ + +export interface Language { + code: string; // ISO 639-1 code + name: string; // Native language name + englishName: string; // English name for reference + flag: string; // Unicode flag emoji + rtl?: boolean; // Right-to-left script +} + +export const SUPPORTED_LANGUAGES: Language[] = [ + { + code: 'en', + name: 'English', + englishName: 'English', + flag: 'πŸ‡¬πŸ‡§', + }, + { + code: 'de', + name: 'Deutsch', + englishName: 'German', + flag: 'πŸ‡©πŸ‡ͺ', + }, + { + code: 'es', + name: 'EspaΓ±ol', + englishName: 'Spanish', + flag: 'πŸ‡ͺπŸ‡Έ', + }, + { + code: 'fr', + name: 'FranΓ§ais', + englishName: 'French', + flag: 'πŸ‡«πŸ‡·', + }, +]; + +export const DEFAULT_LANGUAGE = 'en'; + +export const LANGUAGE_CODES = SUPPORTED_LANGUAGES.map((lang) => lang.code); + +/** + * Check if a language code is supported + */ +export function isSupported(code: string): boolean { + return LANGUAGE_CODES.includes(code); +} + +/** + * Get language config by code + */ +export function getLanguage(code: string): Language | undefined { + return SUPPORTED_LANGUAGES.find((lang) => lang.code === code); +} + +/** + * Get browser's preferred language from supported list + */ +export function getBrowserLanguage(): string { + if (typeof navigator === 'undefined') return DEFAULT_LANGUAGE; + + // Check exact match first + const browserLang = navigator.language.toLowerCase(); + const exactMatch = LANGUAGE_CODES.find((code) => browserLang.startsWith(code)); + if (exactMatch) return exactMatch; + + // Check language codes from browser languages + const browserLanguages = navigator.languages || [navigator.language]; + for (const lang of browserLanguages) { + const code = lang.split('-')[0]?.toLowerCase(); + if (code && isSupported(code)) { + return code; + } + } + + return DEFAULT_LANGUAGE; +} diff --git a/src/lib/i18n/translations/de/common.json b/src/lib/i18n/translations/de/common.json new file mode 100644 index 00000000..e60d1e75 --- /dev/null +++ b/src/lib/i18n/translations/de/common.json @@ -0,0 +1,120 @@ +{ + "actions": { + "save": "Speichern", + "cancel": "Abbrechen", + "delete": "LΓΆschen", + "edit": "Bearbeiten", + "copy": "Kopieren", + "copied": "Kopiert!", + "close": "Schließen", + "back": "ZurΓΌck", + "next": "Weiter", + "previous": "Vorherige", + "search": "Suchen", + "clear": "LΓΆschen", + "reset": "ZurΓΌcksetzen", + "export": "Exportieren", + "import": "Importieren", + "download": "Herunterladen", + "upload": "Hochladen", + "refresh": "Aktualisieren", + "apply": "Anwenden", + "submit": "Absenden", + "confirm": "BestΓ€tigen" + }, + + "states": { + "loading": "LΓ€dt...", + "calculating": "Berechne...", + "error": "Fehler", + "success": "Erfolg", + "warning": "Warnung", + "info": "Info", + "processing": "Verarbeite...", + "validating": "Validiere...", + "saving": "Speichere...", + "empty": "Keine Daten verfΓΌgbar", + "notFound": "Nicht gefunden" + }, + + "labels": { + "name": "Name", + "description": "Beschreibung", + "example": "Beispiel", + "examples": "Beispiele", + "value": "Wert", + "type": "Typ", + "options": "Optionen", + "settings": "Einstellungen", + "language": "Sprache", + "theme": "Design", + "help": "Hilfe", + "documentation": "Dokumentation", + "about": "Über", + "version": "Version", + "status": "Status" + }, + + "navigation": { + "home": "Startseite", + "settings": "Einstellungen", + "bookmarks": "Lesezeichen", + "search": "Suche", + "sitemap": "Sitemap", + "about": "Über", + "legal": "Rechtliches", + "github": "GitHub" + }, + + "validation": { + "required": "Dieses Feld ist erforderlich", + "invalid": "UngΓΌltiger Wert", + "invalidFormat": "UngΓΌltiges Format", + "invalidIp": "UngΓΌltige IP-Adresse", + "invalidCidr": "UngΓΌltige CIDR-Notation", + "outOfRange": "Wert außerhalb des Bereichs", + "mustBePositive": "Wert muss positiv sein", + "tooShort": "Wert ist zu kurz", + "tooLong": "Wert ist zu lang" + }, + + "time": { + "seconds": { + "zero": "0 Sekunden", + "one": "1 Sekunde", + "other": "{count} Sekunden" + }, + "minutes": { + "zero": "0 Minuten", + "one": "1 Minute", + "other": "{count} Minuten" + }, + "hours": { + "zero": "0 Stunden", + "one": "1 Stunde", + "other": "{count} Stunden" + }, + "days": { + "zero": "0 Tage", + "one": "1 Tag", + "other": "{count} Tage" + } + }, + + "clipboard": { + "copy": "In Zwischenablage kopieren", + "copySuccess": "In Zwischenablage kopiert!", + "copyError": "Fehler beim Kopieren in die Zwischenablage" + }, + + "errors": { + "generic": "Ein Fehler ist aufgetreten", + "networkError": "Netzwerkfehler aufgetreten", + "timeout": "Anfrage-Timeout", + "notFound": "Nicht gefunden", + "unauthorized": "Nicht autorisiert", + "forbidden": "Verboten", + "serverError": "Serverfehler", + "unknownError": "Unbekannter Fehler aufgetreten" + } +} diff --git a/src/lib/i18n/translations/de/nav.json b/src/lib/i18n/translations/de/nav.json new file mode 100644 index 00000000..74cd5e06 --- /dev/null +++ b/src/lib/i18n/translations/de/nav.json @@ -0,0 +1,59 @@ +{ + "top": { + "subnetting": { + "label": "Subnetting", + "description": "Subnetz-Rechner fΓΌr IPv4, IPv6, VLSM, Supernetting" + }, + "cidr": { + "label": "CIDR", + "description": "CIDR-Notations-Tools, Konverter und Rechner" + }, + "ip_tools": { + "label": "IP-Tools", + "description": "IP-Adressen-Konverter, Validatoren und Generatoren" + }, + "dns_tools": { + "label": "DNS-Tools", + "description": "DNS-Datensatz-Generatoren, Validatoren und DNSSEC-Tools" + }, + "dhcp": { + "label": "DHCP", + "description": "DHCP-Options-Generatoren fΓΌr WLAN-Controller und Netzwerkkonfiguration" + }, + "diagnostics": { + "label": "Diagnose", + "description": "Netzwerkdiagnose und KonnektivitΓ€tstests" + }, + "reference": { + "label": "Referenz", + "description": "Netzwerk-ReferenzhandbΓΌcher und Nachschlagetabellen" + } + }, + + "standalone": { + "bookmarks": { + "label": "Lesezeichen", + "description": "Netzwerkberechnungen und Tool-Ergebnisse speichern und organisieren" + }, + "search": { + "label": "Suche", + "description": "Tools und Referenzinhalte schnell finden" + }, + "settings": { + "label": "Einstellungen", + "description": "Designs, Layouts und Barrierefreiheitsoptionen anpassen" + } + }, + + "common_tools": { + "subnet_calculator": "Subnetz-Rechner", + "ipv6_subnet_calculator": "IPv6-Subnetz-Rechner", + "vlsm_calculator": "VLSM-Rechner", + "supernet_calculator": "Supernetz-Rechner", + "cidr_summarizer": "CIDR-Zusammenfasser", + "cidr_splitter": "CIDR-Teiler", + "ip_validator": "IP-Validator", + "dns_lookup": "DNS-Abfrage", + "tls_certificate": "TLS-Zertifikat-Analysator" + } +} diff --git a/src/lib/i18n/translations/de/settings.json b/src/lib/i18n/translations/de/settings.json new file mode 100644 index 00000000..314ee627 --- /dev/null +++ b/src/lib/i18n/translations/de/settings.json @@ -0,0 +1,94 @@ +{ + "title": "Einstellungen", + "description": "Passen Sie Ihr Erlebnis mit Designs, Layouts und Barrierefreiheitsoptionen an.", + + "disabled": { + "title": "Einstellungen deaktiviert", + "message": "Die Einstellungen fΓΌr diese Instanz wurden von Ihrem Administrator deaktiviert." + }, + + "language": { + "title": "Sprache" + }, + + "theme": { + "title": "Design", + "show_more": "Mehr Designs anzeigen", + "show_less": "Weniger anzeigen" + }, + + "font_scale": { + "title": "Schriftgrâße" + }, + + "homepage_layout": { + "title": "Startseiten-Layout" + }, + + "top_navigation": { + "title": "Obere Navigation" + }, + + "accessibility": { + "title": "Barrierefreiheit", + "show_all": "Alle Barrierefreiheitsoptionen anzeigen", + "show_less": "Weniger anzeigen" + }, + + "site_branding": { + "title": "Website-Branding", + "description": "Passen Sie den Website-Titel, die Beschreibung und das Symbol an.", + "site_title": "Website-Titel", + "site_description": "Beschreibung", + "site_icon_url": "Symbol-URL", + "apply": "Anwenden", + "reset": "ZurΓΌcksetzen" + }, + + "primary_color": { + "title": "PrimΓ€rfarbe", + "description": "WΓ€hlen Sie eine PrimΓ€rfarbe fΓΌr die BenutzeroberflΓ€che.", + "custom_color": "Benutzerdefinierte Farbe verwenden", + "color_picker": "FarbwΓ€hler", + "hex_code": "Hex-Code", + "reset": "ZurΓΌcksetzen" + }, + + "custom_css": { + "title": "Benutzerdefiniertes CSS", + "description": "FΓΌgen Sie Ihr eigenes CSS hinzu, um das Aussehen global anzupassen.", + "placeholder": "/* Geben Sie hier Ihr benutzerdefiniertes CSS ein */", + "characters": "Zeichen", + "apply": "Anwenden", + "clear": "LΓΆschen" + }, + + "more_info": { + "title": "Nicht gefunden, wonach Sie gesucht haben?", + "line1": "Gute Nachrichten! Der Code ist Open Source und einfach zu verwenden.", + "content": "Forken Sie einfach das Repository, folgen Sie unseren Entwicklungseinrichtungsanweisungen, nehmen Sie alle gewΓΌnschten Γ„nderungen und Anpassungen vor und stellen Sie dann Ihre eigene Instanz bereit.", + "support": "Wir bieten auch Unternehmens-Supportdienste an, bei denen wir benutzerdefinierte Γ„nderungen fΓΌr Sie vornehmen kΓΆnnen." + }, + + "delete_data": { + "title": "Daten lΓΆschen", + "caution": "Vorsicht: Dies setzt alle lokalen Daten zurΓΌck.", + "button": "Alle Daten lΓΆschen" + }, + + "sync": { + "title": "Einstellungen synchronisieren und Sichern/Wiederherstellen", + "line1": "Ihre Einstellungen werden im lokalen Speicher Ihres Browsers gespeichert und bleiben auch nach dem Beenden der App erhalten.", + "content": "Da wir keine Anmeldung/Registrierung fΓΌr die Nutzung der App benΓΆtigen, gibt es derzeit keine MΓΆglichkeit, Ihre Einstellungen automatisch ΓΌber GerΓ€te hinweg zu synchronisieren. Wenn Sie jedoch Networking Toolbox selbst hosten, kΓΆnnen Sie Einstellungen in Ihrer Konfiguration anwenden, indem Sie die folgenden Umgebungsvariablen einbeziehen. Auf diese Weise werden Ihre Einstellungen auf alle Benutzer auf allen GerΓ€ten angewendet.", + "export_settings": "Einstellungen exportieren", + "export_styles": "Stile exportieren", + "env_vars_title": "Umgebungsvariablen", + "env_vars_description": "Aktuelle Umgebungsvariablenwerte. Kopieren Sie diese in Ihre Konfigurationsdatei fΓΌr selbst gehostete Instanzen.", + "copy_clipboard": "In Zwischenablage kopieren", + "copied": "Kopiert!", + "custom_css_title": "Benutzerdefiniertes CSS", + "custom_css_description": "Wenden Sie Ihr benutzerdefiniertes CSS auf Ihre selbst gehostete Instanz an, indem Sie eine CSS-Datei mounten." + }, + + "more_settings": "Weitere Einstellungen" +} diff --git a/src/lib/i18n/translations/de/tools.json b/src/lib/i18n/translations/de/tools.json new file mode 100644 index 00000000..ecb86c49 --- /dev/null +++ b/src/lib/i18n/translations/de/tools.json @@ -0,0 +1,75 @@ +{ + "subnet_calculator": { + "title": "Subnetz-Rechner", + "description": "Berechnen Sie Netzwerk-, Broadcast- und Host-Informationen fΓΌr jedes Subnetz.", + + "input": { + "cidr_label": "Netzwerkadresse (CIDR)", + "cidr_placeholder": "192.168.1.0/24" + }, + + "sections": { + "network_info": "Netzwerkinformationen", + "host_info": "Host-Informationen", + "binary_representation": "BinΓ€re Darstellung" + }, + + "fields": { + "network_address": "Netzwerkadresse", + "broadcast_address": "Broadcast-Adresse", + "subnet_mask": "Subnetzmaske", + "wildcard_mask": "Wildcard-Maske", + "total_hosts": "Gesamt-Hosts", + "usable_hosts": "Nutzbare Hosts", + "first_host": "Erster Host", + "last_host": "Letzter Host", + "network_binary": "Netzwerk:", + "mask_binary": "Maske:", + "broadcast_binary": "Broadcast:" + }, + + "tooltips": { + "network_address": "Erste IP im Subnetz - identifiziert das Netzwerk", + "broadcast_address": "Letzte IP im Subnetz - sendet an alle Hosts", + "subnet_mask": "Definiert Netzwerk- vs. Host-Teil der IP", + "wildcard_mask": "Inverse der Subnetzmaske - wird in ACLs verwendet", + "total_hosts": "Alle IP-Adressen in diesem Subnetz", + "usable_hosts": "FΓΌr GerΓ€te verfΓΌgbare IPs (ohne Netzwerk/Broadcast)", + "first_host": "Erste fΓΌr GerΓ€te verfΓΌgbare IP-Adresse", + "last_host": "Letzte fΓΌr GerΓ€te verfΓΌgbare IP-Adresse", + "network_binary": "Netzwerkadresse im BinΓ€rformat", + "mask_binary": "Subnetzmaske im BinΓ€rformat", + "broadcast_binary": "Broadcast-Adresse im BinΓ€rformat" + }, + + "explainer": { + "title": "Subnetzberechnungen verstehen", + + "network_address_title": "Netzwerkadresse", + "network_address_content": "Die erste IP-Adresse in einem Subnetz, die zur Identifizierung des Netzwerks selbst verwendet wird. Hosts kΓΆnnen diese Adresse nicht zugewiesen bekommen, da sie das gesamte Netzwerksegment reprΓ€sentiert.", + + "broadcast_address_title": "Broadcast-Adresse", + "broadcast_address_content": "Die letzte IP-Adresse in einem Subnetz, die zum Senden von Nachrichten an alle GerΓ€te im Netzwerk verwendet wird. Wenn ein Paket an diese Adresse gesendet wird, erreicht es jeden Host im Subnetz.", + + "subnet_mask_title": "Subnetzmaske", + "subnet_mask_content": "Definiert, welcher Teil einer IP-Adresse das Netzwerk und welcher den Host reprΓ€sentiert. Eine Maske von /24 bedeutet, dass die ersten 24 Bits das Netzwerk identifizieren.", + + "wildcard_mask_title": "Wildcard-Maske", + "wildcard_mask_content": "Die Inverse einer Subnetzmaske, die in Zugriffskontrolllisten verwendet wird. Wo die Subnetzmaske Einsen hat, hat die Wildcard-Maske Nullen und umgekehrt.", + + "usable_hosts_title": "Nutzbare Hosts", + "usable_hosts_content": "Die Anzahl der fΓΌr GerΓ€te verfΓΌgbaren IP-Adressen. Immer 2 weniger als die Gesamtadressen, da Netzwerk- und Broadcast-Adressen reserviert sind.", + + "cidr_notation_title": "CIDR-Notation", + "cidr_notation_content": "Classless Inter-Domain Routing-Notation (z.B. /24) gibt an, wie viele Bits fΓΌr den Netzwerkteil verwendet werden. HΓΆhere Zahlen bedeuten kleinere Subnetze mit weniger Hosts." + }, + + "tips": { + "title": "Profi-Tipps", + "plan_growth": "Wachstum einplanen: WΓ€hlen Sie Subnetzgrâßen, die zukΓΌnftige Erweiterungen berΓΌcksichtigen", + "binary_understanding": "BinΓ€r verstehen: Das Erlernen von BinΓ€r hilft beim VerstΓ€ndnis der Subnetzbildung", + "common_sizes": "HΓ€ufige Grâßen: /24 (254 Hosts), /25 (126 Hosts), /26 (62 Hosts), /30 (2 Hosts fΓΌr Punkt-zu-Punkt)", + "private_networks": "Private Netzwerke: Verwenden Sie RFC 1918-Adressen (10.x.x.x, 172.16-31.x.x, 192.168.x.x) fΓΌr interne Netzwerke" + } + } +} diff --git a/src/lib/i18n/translations/en/common.json b/src/lib/i18n/translations/en/common.json new file mode 100644 index 00000000..063d1749 --- /dev/null +++ b/src/lib/i18n/translations/en/common.json @@ -0,0 +1,369 @@ +{ + "common": { + "notAvailable": "Not available", + "unknown": "Unknown", + "none": "None", + "all": "All", + "any": "Any", + "default": "Default", + "custom": "Custom", + "auto": "Auto", + "manual": "Manual", + "enabled": "Enabled", + "disabled": "Disabled", + "active": "Active", + "inactive": "Inactive", + "online": "Online", + "offline": "Offline", + "connected": "Connected", + "disconnected": "Disconnected", + "secure": "Secure", + "insecure": "Insecure", + "valid": "Valid", + "invalid": "Invalid", + "supported": "Supported", + "unsupported": "Unsupported", + "yes": "Yes", + "no": "No", + "on": "On", + "off": "Off", + "high": "High", + "medium": "Medium", + "low": "Low", + "good": "Good", + "poor": "Poor", + "excellent": "Excellent" + }, + + "buttons": { + "copy": "Copy" + }, + + "actions": { + "save": "Save", + "cancel": "Cancel", + "delete": "Delete", + "edit": "Edit", + "copy": "Copy", + "copied": "Copied!", + "copyResults": "Copy Results", + "close": "Close", + "back": "Back", + "next": "Next", + "previous": "Previous", + "search": "Search", + "clear": "Clear", + "reset": "Reset", + "export": "Export", + "import": "Import", + "download": "Download", + "upload": "Upload", + "refresh": "Refresh", + "apply": "Apply", + "submit": "Submit", + "confirm": "Confirm", + "generate": "Generate", + "convert": "Convert", + "validate": "Validate", + "check": "Check", + "lookup": "Lookup", + "calculate": "Calculate", + "analyze": "Analyze", + "build": "Build", + "create": "Create", + "test": "Test", + "compare": "Compare", + "merge": "Merge", + "split": "Split", + "format": "Format", + "normalize": "Normalize", + "decode": "Decode", + "encode": "Encode", + "downloaded": "Downloaded!", + "generated": "Generated!", + "converted": "Converted!", + "validated": "Validated!", + "calculated": "Calculated!" + }, + + "states": { + "loading": "Loading...", + "calculating": "Calculating...", + "error": "Error", + "success": "Success", + "warning": "Warning", + "info": "Info", + "processing": "Processing...", + "validating": "Validating...", + "saving": "Saving...", + "generating": "Generating...", + "converting": "Converting...", + "analyzing": "Analyzing...", + "checking": "Checking...", + "calculating": "Calculating...", + "building": "Building...", + "testing": "Testing...", + "formatting": "Formatting...", + "normalizing": "Normalizing...", + "comparing": "Comparing...", + "empty": "No data available", + "notFound": "Not found", + "completed": "Completed", + "failed": "Failed", + "ready": "Ready", + "pending": "Pending" + }, + + "labels": { + "name": "Name", + "description": "Description", + "example": "Example", + "examples": "Examples", + "value": "Value", + "type": "Type", + "usage": "Usage", + "options": "Options", + "settings": "Settings", + "language": "Language", + "theme": "Theme", + "help": "Help", + "documentation": "Documentation", + "about": "About", + "version": "Version", + "status": "Status", + "configuration": "Configuration", + "parameters": "Parameters", + "results": "Results", + "output": "Output", + "input": "Input", + "format": "Format", + "protocol": "Protocol", + "domain": "Domain", + "port": "Port", + "method": "Method", + "algorithm": "Algorithm", + "priority": "Priority", + "weight": "Weight", + "target": "Target", + "service": "Service", + "ttl": "TTL", + "record": "Record", + "records": "Records", + "hostname": "Hostname", + "address": "Address", + "network": "Network", + "subnet": "Subnet", + "mask": "Mask", + "prefix": "Prefix", + "range": "Range", + "count": "Count", + "size": "Size", + "length": "Length", + "timeout": "Timeout", + "interval": "Interval", + "retries": "Retries" + }, + + "navigation": { + "home": "Home", + "settings": "Settings", + "bookmarks": "Bookmarks", + "search": "Search", + "sitemap": "Sitemap", + "about": "About", + "legal": "Legal", + "github": "GitHub" + }, + + "validation": { + "required": "This field is required", + "invalid": "Invalid value", + "invalidFormat": "Invalid format", + "invalidIp": "Invalid IP address", + "invalidCidr": "Invalid CIDR notation", + "invalidDomain": "Invalid domain name", + "invalidPort": "Invalid port number", + "invalidEmail": "Invalid email address", + "invalidUrl": "Invalid URL", + "invalidHostname": "Invalid hostname", + "outOfRange": "Value out of range", + "mustBePositive": "Value must be positive", + "mustBeNumeric": "Value must be numeric", + "tooShort": "Value is too short", + "tooLong": "Value is too long", + "fieldRequired": "{field} is required", + "fieldInvalid": "Invalid {field}", + "formatError": "Invalid format for {field}" + }, + + "time": { + "seconds": { + "zero": "0 seconds", + "one": "1 second", + "other": "{count} seconds" + }, + "minutes": { + "zero": "0 minutes", + "one": "1 minute", + "other": "{count} minutes" + }, + "hours": { + "zero": "0 hours", + "one": "1 hour", + "other": "{count} hours" + }, + "days": { + "zero": "0 days", + "one": "1 day", + "other": "{count} days" + } + }, + + "clipboard": { + "copy": "Copy to clipboard", + "copied": "Copied!", + "copySuccess": "Copied to clipboard!", + "copyError": "Failed to copy to clipboard", + "copyNetworkAddress": "Copy network address to clipboard", + "copyBroadcastAddress": "Copy broadcast address to clipboard", + "copyBinaryFormat": "Copy binary format to clipboard", + "copyDecimalFormat": "Copy decimal format to clipboard", + "copyHexFormat": "Copy hexadecimal format to clipboard", + "copyOctalFormat": "Copy octal format to clipboard" + }, + + "errors": { + "generic": "An error occurred", + "networkError": "Network error occurred", + "timeout": "Request timed out", + "notFound": "Not found", + "unauthorized": "Unauthorized", + "forbidden": "Forbidden", + "serverError": "Server error", + "unknownError": "Unknown error occurred", + "parseError": "Failed to parse data", + "validationError": "Validation failed", + "connectionError": "Connection failed", + "configurationError": "Configuration error" + }, + + "placeholders": { + "domain": "example.com", + "email": "admin@example.com", + "hostname": "mail.example.com", + "port": "443", + "ip": "192.168.1.1", + "ipv6": "2001:db8::1", + "cidr": "192.168.0.0/24", + "asn": "AS15169", + "url": "https://example.com", + "path": "/path/to/resource", + "query": "SELECT * FROM table", + "command": "ping example.com", + "certificate": "-----BEGIN CERTIFICATE-----", + "key": "-----BEGIN PRIVATE KEY-----", + "token": "abcd1234efgh5678" + }, + + "sections": { + "overview": "Overview", + "details": "Details", + "summary": "Summary", + "advanced": "Advanced", + "basic": "Basic", + "optional": "Optional", + "required": "Required", + "recommended": "Recommended", + "troubleshooting": "Troubleshooting", + "tips": "Tips", + "warnings": "Warnings", + "notes": "Notes", + "related": "Related", + "reference": "Reference", + "resources": "Resources" + }, + + "units": { + "bytes": "bytes", + "kb": "KB", + "mb": "MB", + "gb": "GB", + "ms": "ms", + "sec": "sec", + "min": "min", + "hour": "hour", + "day": "day", + "percent": "%", + "bits": "bits", + "packets": "packets", + "requests": "requests" + }, + + "meta": { + "titleSuffix": "Networking Toolbox", + "titleSeparator": " | ", + "defaultDescription": "All-in-one networking toolbox with 100+ tools for network analysis, IP calculations, DNS lookups, and system diagnostics", + "keywords": "networking tools, IP calculator, DNS lookup, subnet calculator, CIDR, IPv6, network diagnostics" + }, + + "content": { + "keyConceptsTitle": "Key Concepts", + "aboutTitle": "About {toolName}", + "examplesTitle": "Examples", + "usageTipsTitle": "Usage Tips", + "quickExamplesTitle": "Quick Examples", + "commonUseCasesTitle": "Common Use Cases", + "bestPracticesTitle": "Best Practices", + "relatedToolsTitle": "Related Tools", + "technicalDetailsTitle": "Technical Details", + "implementationNotesTitle": "Implementation Notes", + "learningResourcesTitle": "Learning Resources", + "troubleshootingTitle": "Troubleshooting", + "faqTitle": "Frequently Asked Questions" + }, + + "network": { + "networkAddress": "Network Address", + "broadcastAddress": "Broadcast Address", + "subnetMask": "Subnet Mask", + "wildcardMask": "Wildcard Mask", + "defaultGateway": "Default Gateway", + "dnsServer": "DNS Server", + "dhcpServer": "DHCP Server", + "ipAddress": "IP Address", + "ipv4Address": "IPv4 Address", + "ipv6Address": "IPv6 Address", + "macAddress": "MAC Address", + "hostsAvailable": "Hosts Available", + "usableHosts": "Usable Hosts", + "totalHosts": "Total Hosts", + "cidrNotation": "CIDR Notation", + "prefixLength": "Prefix Length", + "subnetRange": "Subnet Range", + "addressSpace": "Address Space", + "privateNetwork": "Private Network", + "publicNetwork": "Public Network", + "multicast": "Multicast", + "anycast": "Anycast", + "unicast": "Unicast", + "linkLocal": "Link Local", + "loopback": "Loopback" + }, + + "form": { + "placeholder": "Enter value...", + "selectOption": "Select an option", + "enterValue": "Enter a value", + "chooseFile": "Choose file", + "dropFileHere": "Drop file here", + "optional": "Optional", + "required": "Required", + "recommended": "Recommended", + "advanced": "Advanced options", + "basic": "Basic options", + "moreOptions": "More options", + "lessOptions": "Fewer options", + "showAdvanced": "Show advanced options", + "hideAdvanced": "Hide advanced options" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics.json b/src/lib/i18n/translations/en/diagnostics.json new file mode 100644 index 00000000..58e95496 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics.json @@ -0,0 +1,918 @@ +{ + "dns-dmarc-check": { + "title": "DMARC Policy Checker", + "subtitle": "Analyze DMARC (Domain-based Message Authentication, Reporting & Conformance) policies", + "examples": { + "title": "DMARC Examples", + "items": { + "google": { + "description": "Google's comprehensive DMARC policy with strict enforcement" + }, + "github": { + "description": "GitHub's enterprise DMARC configuration for developer platform security" + }, + "microsoft": { + "description": "Microsoft's DMARC setup for Office 365 and enterprise email" + }, + "paypal": { + "description": "PayPal's strict DMARC policy for financial service protection" + }, + "amazon": { + "description": "Amazon's DMARC implementation for e-commerce platform security" + }, + "salesforce": { + "description": "Salesforce's DMARC configuration for CRM and business email" + } + } + }, + "form": { + "title": "DMARC Policy Check", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "checkButton": "Check DMARC Policy" + }, + "educational": { + "title": "Understanding DMARC", + "policies": { + "title": "DMARC Policies", + "none": "Monitor mode - collect data but take no action on failures", + "quarantine": "Mark suspicious messages, often sent to spam folder", + "reject": "Reject non-compliant messages outright (strongest security)" + }, + "alignmentModes": { + "title": "Alignment Modes", + "relaxed": "Allows organizational domain matching (default)", + "strict": "Requires exact domain matching (more secure)" + }, + "reportingTypes": { + "title": "Reporting Types", + "aggregate": "Daily summary reports of DMARC activity", + "forensic": "Real-time failure reports with message samples" + }, + "bestPractices": { + "title": "Best Practices", + "items": { + "startMonitoring": "Start with p=none to monitor before enforcement", + "gradualEnforcement": "Gradually increase to p=quarantine then p=reject", + "setupReporting": "Set up aggregate reporting to monitor DMARC activity", + "strictAlignment": "Use strict alignment for enhanced security when possible", + "subdomainPolicy": "Consider subdomain policy for comprehensive coverage" + } + } + } + }, + + "httpPerf": { + "examples": { + "google": "Google homepage performance test", + "delay": "Delayed response simulation (2s)", + "github": "GitHub API response time analysis", + "cloudflare": "Cloudflare CDN performance measurement" + } + }, + + "email-spf-check": { + "title": "Email SPF Policy Checker", + "description": "Check SPF (Sender Policy Framework) records for email authentication and deliverability", + "examples": { + "title": "SPF Examples", + "gmail": "Google Gmail SPF policy analysis", + "outlook": "Microsoft Outlook SPF configuration", + "salesforce": "Salesforce SPF setup for CRM emails", + "mailchimp": "MailChimp email service SPF record", + "github": "GitHub enterprise SPF policy check", + "sendgrid": "SendGrid email platform SPF analysis" + }, + "form": { + "title": "SPF Policy Check", + "domain": { + "label": "Domain Name", + "placeholder": "example.com" + }, + "check": "Check SPF Policy" + } + }, + + "tls-versions": { + "title": "TLS Versions Probe", + "description": "Test which TLS protocol versions a server supports by attempting connections with different TLS version constraints. Identify deprecated versions and assess overall TLS security posture.", + "examples": { + "title": "TLS Version Examples", + "google": "Google TLS version support", + "github": "GitHub TLS versions", + "cloudflare": "Cloudflare TLS support", + "microsoft": "Microsoft TLS versions", + "amazon": "Amazon TLS configuration", + "facebook": "Facebook TLS versions" + }, + "form": { + "title": "TLS Version Test", + "host": { + "label": "Host", + "placeholder": "google.com:443", + "tooltip": "Enter hostname with optional port (defaults to 443)", + "error": "Invalid host:port format" + }, + "servername": { + "label": "Custom Server Name", + "placeholder": "example.com", + "tooltip": "Override SNI server name for virtual hosting", + "checkbox": "Use custom SNI servername" + }, + "probe": "Probe TLS Versions", + "probing": "Probing TLS versions..." + }, + "results": { + "title": "TLS Version Support Results", + "security": { + "title": "Overall Security Assessment", + "levels": { + "excellent": "Excellent", + "good": "Good", + "warning": "Needs Attention", + "poor": "Poor", + "critical": "Critical", + "unknown": "Unknown" + }, + "descriptions": { + "excellent": "Only modern TLS versions supported", + "good": "Secure TLS versions only", + "warning": "Deprecated versions still supported", + "poor": "Only deprecated versions supported", + "critical": "No TLS versions detected", + "unknown": "No results available" + } + }, + "versions": { + "title": "Version Support Details", + "supported": "Supported", + "supportedDeprecated": "Supported (Deprecated)", + "notSupported": "Not Supported" + }, + "summary": { + "title": "Connection Summary", + "minVersion": "Minimum supported version", + "maxVersion": "Maximum supported version", + "preferredCipher": "Preferred cipher", + "certificateIssuer": "Certificate issuer", + "outOfTested": "Out of {count} tested" + } + }, + "versions": { + "tls10": "TLS 1.0", + "tls11": "TLS 1.1", + "tls12": "TLS 1.2", + "tls13": "TLS 1.3" + }, + "educational": { + "title": "Understanding TLS Versions", + "versionSecurity": { + "title": "TLS Version Security", + "tls13": "TLS 1.3: Latest version with improved security and performance", + "tls12": "TLS 1.2: Widely supported, secure when properly configured", + "tls11": "TLS 1.1: Deprecated, should not be used", + "tls10": "TLS 1.0: Deprecated, contains security vulnerabilities" + }, + "bestPractices": { + "title": "Best Practices", + "enableModern": "Enable TLS 1.2 and 1.3 only", + "disableDeprecated": "Disable deprecated versions (TLS 1.0, 1.1)", + "updateRegularly": "Regularly update TLS implementations", + "strongCiphers": "Use strong cipher suites" + }, + "compliance": { + "title": "Compliance Requirements", + "description": "Many compliance standards (PCI DSS, HIPAA) require disabling deprecated TLS versions. Check your specific requirements." + } + }, + "warnings": { + "deprecated": "This version is deprecated and should not be used" + } + }, + + "http-ping": { + "title": "HTTP Ping", + "description": "Test HTTP response times and connectivity by sending multiple requests to a URL. Analyze latency, response times, and success rates for web services and APIs.", + "examples": { + "google": "Google homepage", + "github": "GitHub homepage", + "githubApi": "GitHub API", + "delay": "Simulated 1s delay", + "cloudflare": "Cloudflare CDN", + "stackoverflow": "Stack Overflow" + }, + "form": { + "title": "HTTP Ping Configuration", + "url": { + "label": "Target URL", + "placeholder": "https://example.com", + "tooltip": "Enter the URL to test" + }, + "method": { + "label": "HTTP Method", + "options": { + "head": "HEAD - Headers only, fastest", + "get": "GET - Full response, more realistic" + } + }, + "count": { + "label": "Ping Count", + "tooltip": "Number of requests to send" + }, + "timeout": { + "label": "Timeout (ms)", + "tooltip": "Maximum time to wait for each request" + }, + "start": "Start HTTP Ping", + "starting": "Starting HTTP ping...", + "stop": "Stop Ping", + "pinging": "Pinging..." + }, + "results": { + "title": "HTTP Ping Results", + "summary": { + "title": "Statistics Summary", + "requests": "Requests", + "successful": "Successful", + "failed": "Failed", + "successRate": "Success Rate", + "avgResponseTime": "Average Response Time", + "minResponseTime": "Minimum Response Time", + "maxResponseTime": "Maximum Response Time", + "totalTime": "Total Time" + }, + "details": { + "title": "Request Details", + "request": "Request", + "status": "Status", + "responseTime": "Response Time", + "error": "Error" + }, + "status": { + "success": "Success", + "failed": "Failed", + "timeout": "Timeout" + } + }, + "educational": { + "title": "Understanding HTTP Ping", + "whatIs": { + "title": "What is HTTP Ping?", + "description": "HTTP Ping tests web service availability and performance by sending HTTP requests and measuring response times. Unlike ICMP ping, it tests the actual web service functionality." + }, + "methods": { + "title": "HTTP Methods", + "head": "HEAD: Retrieves headers only, minimal bandwidth usage", + "get": "GET: Downloads full response, more realistic test" + }, + "useCase": { + "title": "Common Use Cases", + "monitoring": "Web service monitoring and health checks", + "performance": "Performance testing and optimization", + "troubleshooting": "Network and application troubleshooting", + "api": "API endpoint testing and validation" + } + } + }, + + "mx-health": { + "title": "Email MX Health Checker", + "description": "Check mail server (MX) health including DNS resolution and optional SMTP port connectivity testing. Verify your email infrastructure is properly configured and reachable.", + "examples": { + "title": "MX Health Examples", + "gmail": "Google Gmail MX infrastructure", + "outlook": "Microsoft Outlook mail servers", + "yahoo": "Yahoo Mail MX configuration", + "protonmail": "ProtonMail secure email setup", + "fastmail": "FastMail professional hosting", + "github": "GitHub enterprise email setup" + }, + "form": { + "title": "MX Health Check", + "domain": { + "label": "Domain Name", + "placeholder": "example.com" + }, + "checkPorts": { + "label": "Check SMTP port connectivity (25, 587, 465)" + }, + "check": "Check MX Health", + "checking": "Checking MX Health..." + }, + "results": { + "title": "MX Health Results", + "summary": { + "healthy": "Mail Infrastructure Healthy", + "issues": "Mail Infrastructure Issues", + "resolvedSuccessfully": "{healthy} of {total} MX records resolved successfully", + "reachableViaSmtp": "β€’ {reachable} reachable via SMTP" + }, + "stats": { + "mxRecords": "MX Records", + "healthy": "Healthy", + "reachable": "Reachable" + } + }, + "ports": { + "25": "SMTP (Standard)", + "587": "Submission (TLS)", + "465": "SMTPS (SSL)", + "generic": "Port {port}" + }, + "copy": { + "title": "MX Health Check for {domain}", + "summary": "Summary:", + "totalMx": "Total MX records: {count}", + "healthyMx": "Healthy MX records: {count}", + "reachableMx": "Reachable MX records: {count}", + "overallHealth": "Overall health: {status}", + "redundancy": "Redundancy: {status}", + "mxRecordsByPriority": "MX Records (by priority):", + "priority": "(Priority: {priority})", + "error": "Error: {error}", + "ipv4": "IPv4: {addresses}", + "ipv6": "IPv6: {addresses}", + "none": "None", + "portChecks": "Port checks:", + "open": "Open", + "closed": "Closed", + "healthy": "Healthy", + "issuesDetected": "Issues detected", + "yes": "Yes", + "no": "No" + } + }, + + "ocsp-stapling": { + "title": "OCSP Stapling Check", + "description": "Report if server staples OCSP and basic status info", + "examples": { + "cloudflare": "Cloudflare - OCSP stapling enabled", + "digicert": "DigiCert - OCSP stapling enabled", + "github": "GitHub - OCSP stapling disabled" + }, + "form": { + "title": "OCSP Stapling Configuration", + "hostname": { + "label": "Hostname and Port", + "placeholder": "example.com", + "tooltip": "Enter hostname to test OCSP stapling", + "error": "Please enter a hostname" + }, + "port": { + "label": "Port", + "placeholder": "443", + "tooltip": "TLS port to test (default: 443)" + }, + "check": "Check", + "checking": "Checking..." + }, + "results": { + "title": "OCSP Stapling Results", + "status": { + "title": "OCSP Stapling Status", + "enabled": { + "title": "OCSP Stapling Enabled", + "description": "This server provides OCSP responses with the TLS handshake" + }, + "disabled": { + "title": "OCSP Stapling Not Enabled", + "description": "This server does not staple OCSP responses" + } + }, + "details": { + "title": "OCSP Response Details", + "certificateStatus": "Certificate Status", + "responseStatus": "Response Status", + "thisUpdate": "This Update", + "nextUpdate": "Next Update", + "producedAt": "Produced At", + "responderUrl": "Responder URL" + }, + "validity": { + "title": "Response Validity", + "validFor": "Valid For", + "expiresIn": "Expires In", + "progressLabel": "Validity Period Progress" + }, + "certificate": { + "title": "Certificate Information", + "subject": "Subject", + "issuer": "Issuer", + "ocspUrls": "OCSP URLs" + }, + "recommendations": { + "title": "Recommendations" + } + }, + "loading": { + "title": "Checking OCSP Stapling", + "description": "Connecting to server and analyzing OCSP response stapling..." + }, + "educational": { + "title": "Understanding OCSP Stapling", + "whatIs": { + "title": "What is OCSP Stapling?", + "description": "OCSP Stapling is a security feature where the server includes a certificate status response during the TLS handshake. This eliminates the need for clients to contact the Certificate Authority directly to check if a certificate has been revoked." + }, + "whyImportant": { + "title": "Why is it Important?", + "privacy": "Privacy: Prevents CA from tracking user browsing", + "performance": "Performance: Faster connections, no extra DNS lookups", + "reliability": "Reliability: Works even if OCSP responder is down", + "security": "Security: Real-time certificate validation" + }, + "howItWorks": { + "title": "How It Works", + "description": "The server periodically queries the OCSP responder and caches the response. During TLS handshake, the server \"staples\" this cached response to the certificate, proving its validity without requiring the client to make additional network requests." + }, + "checkingStatus": { + "title": "Checking Status", + "description": "This tool connects to servers with OCSP stapling enabled and analyzes the stapled response. It checks certificate status, response validity, timing information, and provides recommendations for servers without stapling enabled." + } + }, + "errors": { + "checkFailed": "OCSP Check Failed", + "failedToCheck": "Failed to check OCSP stapling" + } + }, + + "dnssec-adflag": { + "title": "DNSSEC AD Flag Checker", + "description": "Query DNS records via DoH and report if the AD (Authenticated Data) bit is set. The AD bit indicates whether the DNS response has been cryptographically verified through DNSSEC validation.", + + "recordTypes": { + "a": "IPv4 address records", + "aaaa": "IPv6 address records", + "cname": "Canonical name records", + "mx": "Mail exchange records", + "txt": "Text records", + "ns": "Name server records", + "soa": "Start of authority records" + }, + + "examples": { + "title": "Example DNSSEC Tests", + "cloudflare": "DNSSEC-signed domain", + "dnssecFailed": "DNSSEC validation failure test", + "example": "Unsigned domain example", + "google": "Popular signed domain", + "iana": "Internet registry domain" + }, + + "form": { + "title": "DNSSEC Query Configuration", + "domain": { + "label": "Domain Name", + "tooltip": "Enter a domain name to check DNSSEC validation status" + }, + "recordType": { + "label": "Record Type", + "tooltip": "Select the DNS record type to query" + }, + "resolver": { + "label": "DoH Resolver", + "tooltip": "Choose a DNS-over-HTTPS resolver for the query" + }, + "button": { + "check": "Check DNSSEC", + "checking": "Checking DNSSEC..." + } + }, + + "results": { + "title": "DNSSEC Status for {{domain}}", + "copyRawJson": "Copy Raw JSON", + "query": { + "label": "Query", + "tooltip": "The domain and record type that was queried" + }, + "resolver": { + "label": "DoH Resolver", + "tooltip": "DNS-over-HTTPS resolver used for the query" + }, + "validation": { + "title": "DNSSEC Validation Status", + "adFlag": { + "title": "AD (Authenticated Data) Flag", + "set": "SET - Response is DNSSEC validated", + "notSet": "NOT SET - Response is not validated" + }, + "cdFlag": { + "title": "CD (Checking Disabled) Flag", + "message": "SET - DNSSEC validation was disabled for this query" + }, + "responseCode": { + "title": "Response Code" + } + }, + "explanation": { + "title": "Explanation" + } + }, + + "error": { + "title": "DNSSEC Check Failed", + "troubleshooting": { + "title": "Troubleshooting Tips", + "domain": "Ensure the domain name is valid and exists", + "recordType": "Try a different record type if the current one doesn't exist", + "resolver": "Switch to a different DoH resolver", + "records": "Some domains may not have the requested record type" + } + }, + + "education": { + "title": "About DNSSEC and the AD Flag", + "dnssec": { + "title": "What is DNSSEC?", + "description": "DNS Security Extensions (DNSSEC) adds cryptographic authentication to DNS responses, protecting against DNS spoofing and cache poisoning attacks by ensuring response integrity." + }, + "adFlag": { + "title": "The AD (Authenticated Data) Flag", + "description": "The AD bit in DNS responses indicates that the resolver has successfully validated the response using DNSSEC. When set, you can trust the response hasn't been tampered with." + }, + "doh": { + "title": "Why Use DoH for DNSSEC?", + "description": "DNS-over-HTTPS preserves DNSSEC validation status in the AD flag, while traditional DNS queries may not expose this information clearly to clients." + }, + "interpreting": { + "title": "Interpreting Results", + "adSet": { + "title": "AD Set", + "description": "Response is cryptographically verified" + }, + "adNotSet": { + "title": "AD Not Set", + "description": "Domain unsigned, validation failed, or resolver doesn't validate" + }, + "cdSet": { + "title": "CD Set", + "description": "Validation was disabled for this query" + }, + "servfail": { + "title": "SERVFAIL", + "description": "May indicate DNSSEC validation failure" + } + } + } + }, + + "http-ping": { + "title": "HTTP Ping", + "description": "Measure HTTP/HTTPS response latency by sending repeated requests and analyzing timing statistics. Alternative to ICMP ping for web services and APIs.", + + "examples": { + "title": "HTTP Ping Examples", + "google": "Search engine response test", + "github": "Developer platform connectivity", + "githubApi": "API endpoint performance", + "delay": "Simulated delay testing", + "cloudflare": "CDN response analysis", + "stackoverflow": "Community site performance" + }, + + "form": { + "title": "HTTP Ping Configuration", + "url": { + "label": "Target URL", + "tooltip": "Enter a complete HTTP or HTTPS URL", + "error": "Must be a valid HTTP or HTTPS URL" + }, + "method": { + "label": "HTTP Method", + "options": { + "head": "Headers only, fastest and most efficient", + "get": "Full response, more realistic timing", + "options": "Preflight requests" + } + }, + "count": { + "label": "Request Count", + "tooltip": "Number of requests to send (1-20)" + }, + "timeout": { + "label": "Timeout (ms)", + "tooltip": "Timeout per request in milliseconds" + }, + "button": { + "start": "Start HTTP Ping", + "pinging": "Pinging..." + } + }, + + "results": { + "title": "HTTP Ping Results", + "copy": "Copy Results", + "latency": { + "excellent": "Excellent", + "good": "Good", + "fair": "Fair", + "poor": "Poor" + }, + "summary": { + "successful": "Successful requests", + "failed": "Failed requests" + }, + "statistics": { + "title": "Latency Statistics", + "minimum": "Minimum", + "maximum": "Maximum", + "average": "Average", + "median": "Median", + "p95": "95th Percentile", + "range": "Range" + }, + "individual": { + "title": "Individual Request Results" + }, + "errors": { + "title": "Request Errors ({{count}})" + }, + "info": { + "title": "Request Information", + "url": "URL", + "method": "Method", + "successRate": "Success Rate" + } + }, + + "error": { + "title": "HTTP Ping Failed" + }, + + "education": { + "title": "Understanding HTTP Ping", + "httpVsIcmp": { + "title": "HTTP vs ICMP Ping", + "http": { + "title": "HTTP", + "description": "Tests application layer connectivity" + }, + "icmp": { + "title": "ICMP", + "description": "Tests network layer connectivity" + }, + "userExperience": "HTTP ping better reflects real user experience", + "firewalls": "Works through firewalls that block ICMP" + }, + "methods": { + "title": "Request Methods", + "head": { + "title": "HEAD", + "description": "Headers only, fastest and most efficient" + }, + "get": { + "title": "GET", + "description": "Full response, more realistic timing" + }, + "options": { + "title": "OPTIONS", + "description": "Check allowed methods and CORS" + } + }, + "latencyGuide": { + "title": "Latency Guidelines" + } + } + }, + + "mx-health": { + "title": "Email MX Health Checker", + "description": "Check mail server (MX) health including DNS resolution and optional SMTP port connectivity testing. Verify your email infrastructure is properly configured and reachable.", + + "examples": { + "title": "MX Health Examples", + "tooltip": "Check MX health for {{domain}}", + "gmail": "Google's email infrastructure", + "outlook": "Microsoft's email service", + "yahoo": "Yahoo's mail system", + "protonmail": "ProtonMail secure email", + "fastmail": "FastMail email service", + "github": "GitHub's email system" + }, + + "form": { + "title": "MX Health Check", + "domain": { + "label": "Domain Name", + "placeholder": "example.com" + }, + "checkPorts": { + "label": "Check SMTP port connectivity (25, 587, 465)" + }, + "button": { + "check": "Check MX Health", + "checking": "Checking MX Health..." + }, + "check": "Check MX Health" + }, + + "results": { + "title": "MX Health Results", + "copy": "Copy Results", + "healthy": "Healthy", + "issuesDetected": "Issues detected", + "summary": { + "healthy": "Mail Infrastructure Healthy", + "issues": "Mail Infrastructure Issues" + }, + "stats": { + "mxRecords": "MX Records", + "healthy": "Healthy", + "reachable": "Reachable", + "redundancy": "Redundancy" + }, + "mxRecords": { + "title": "MX Records (by priority)", + "ipv4": "IPv4 Addresses", + "ipv6": "IPv6 Addresses", + "portConnectivity": "SMTP Port Connectivity", + "open": "Open", + "closed": "Closed" + } + }, + + "ports": { + "smtp": "SMTP (Standard)", + "submission": "Submission (TLS)", + "smtps": "SMTPS (SSL)", + "custom": "Port {{port}}" + }, + + "error": { + "title": "MX Health Check Failed", + "unknown": "Unknown error occurred" + }, + + "education": { + "title": "Understanding MX Records", + "basics": { + "title": "MX Record Basics", + "priority": { + "title": "Priority", + "description": "Lower numbers have higher priority" + }, + "exchange": { + "title": "Exchange", + "description": "The mail server hostname" + }, + "redundancy": { + "title": "Redundancy", + "description": "Multiple MX records provide failover" + }, + "loadBalancing": { + "title": "Load balancing", + "description": "Equal priorities distribute load" + } + }, + "smtpPorts": { + "title": "SMTP Ports", + "port25": { + "title": "Port 25", + "description": "Standard SMTP (server-to-server)" + }, + "port587": { + "title": "Port 587", + "description": "Mail submission (client-to-server, TLS)" + }, + "port465": { + "title": "Port 465", + "description": "SMTPS (deprecated but still used)" + } + }, + "healthIndicators": { + "title": "Health Indicators", + "allResolve": "All MX records should resolve to IP addresses", + "oneReachable": "At least one SMTP port should be reachable", + "multipleRedundancy": "Multiple MX records provide redundancy", + "lowerPriority": "Lower priority servers should be reachable" + }, + "commonIssues": { + "title": "Common Issues", + "nonExistentHosts": "MX pointing to non-existent hosts", + "portsBlocked": "All SMTP ports blocked by firewall", + "singlePoint": "Single point of failure (one MX record)", + "incorrectPriority": "Incorrect priority configuration" + } + } + }, + "alpn": { + "title": "TLS ALPN Negotiation", + "description": "Test Application-Layer Protocol Negotiation (ALPN) to see which protocol a server selects from your offered list. Commonly used to negotiate HTTP/2, HTTP/3, or other application protocols during TLS handshake.", + "examples": { + "title": "ALPN Examples", + "google": "Test Google's HTTP/2 support", + "github": "Check GitHub's modern protocol support", + "cloudflare": "Test Cloudflare's HTTP/3 implementation", + "wikipedia": "Check Wikipedia's protocol negotiation", + "cdn": "Test CDN protocol optimization", + "api": "Check API endpoint protocol support", + "tooltip": "Test ALPN for {{host}} ({{description}})" + }, + "protocols": { + "standard": "Most common combination for modern web", + "experimental": "Includes experimental HTTP/3 support", + "http2Only": "HTTP/2 only, no fallback to HTTP/1.1", + "fallback": "HTTP/1.1 fallback for legacy servers" + }, + "protocolInfo": { + "h3": { + "name": "HTTP/3", + "description": "Latest HTTP version over QUIC" + }, + "h2": { + "name": "HTTP/2", + "description": "Binary, multiplexed HTTP protocol" + }, + "http11": { + "name": "HTTP/1.1", + "description": "Traditional HTTP protocol" + }, + "http10": { + "name": "HTTP/1.0", + "description": "Legacy HTTP protocol" + }, + "unknown": { + "description": "Custom or unknown protocol" + } + }, + "status": { + "unknown": "Unknown", + "successful": "Successful", + "failed": "Failed", + "noSelection": "No Selection", + "noResults": "No results available", + "serverSelected": "Server selected {{protocol}}", + "noNegotiation": "No protocol was negotiated", + "noProtocolSelected": "Server did not select any protocol" + }, + "form": { + "title": "ALPN Negotiation Configuration", + "hostLabel": "Host:Port", + "hostTooltip": "Enter hostname:port (e.g., google.com:443)", + "invalidHost": "Invalid host:port format", + "protocolsLabel": "ALPN Protocols", + "protocolsTooltip": "Comma-separated list of protocols to offer (e.g., h2,http/1.1)", + "quickSelect": "Quick select:", + "customSni": "Use custom SNI servername", + "sniTooltip": "Custom servername for SNI (Server Name Indication)", + "testing": "Testing ALPN...", + "testButton": "Test ALPN Negotiation" + }, + "results": { + "title": "ALPN Negotiation Results", + "copied": "Copied!", + "copyButton": "Copy Results", + "negotiationStatus": "Negotiation: {{status}}", + "tlsVersion": "TLS Version: {{version}}", + "connectionEstablished": "Connection established successfully", + "protocolDetails": "Protocol Negotiation Details", + "requestedProtocols": "Requested Protocols", + "priority": "Priority {{priority}}", + "selectedProtocol": "Selected Protocol", + "noProtocolSelected": "No protocol was selected by the server", + "connectionInfo": "Connection Information", + "serverName": "Server Name:", + "tlsVersionLabel": "TLS Version:" + }, + "copy": { + "header": "ALPN Negotiation Results for {{host}}", + "generatedAt": "Generated at: {{timestamp}}", + "requestedProtocols": "Requested Protocols: {{protocols}}", + "negotiatedProtocol": "Negotiated Protocol: {{protocol}}", + "tlsVersion": "TLS Version: {{version}}", + "success": "Success: {{success}}", + "none": "None", + "unknown": "Unknown", + "yes": "Yes", + "no": "No", + "negotiationStatus": "Negotiation Status: {{status}}", + "description": "Description: {{description}}", + "selectedProtocolInfo": "Selected Protocol Info:", + "name": " Name: {{name}}", + "protocolDescription": " Description: {{description}}" + }, + "error": { + "title": "ALPN Negotiation Failed" + }, + "info": { + "title": "Understanding ALPN", + "whatIsAlpn": { + "title": "What is ALPN?", + "description": "Application-Layer Protocol Negotiation (ALPN) is a TLS extension that allows the client and server to negotiate which application protocol to use during the TLS handshake." + }, + "commonProtocols": { + "title": "Common Protocols", + "h2": "HTTP/2 - Binary, multiplexed protocol", + "h3": "HTTP/3 - Latest HTTP over QUIC", + "http11": "Traditional HTTP/1.1", + "spdy": "Legacy SPDY protocol" + }, + "priority": { + "title": "Protocol Priority", + "description": "Protocols are offered in preference order. The server selects the first protocol from your list that it supports. Order matters!" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-axfr-tester.json b/src/lib/i18n/translations/en/diagnostics/dns-axfr-tester.json new file mode 100644 index 00000000..ad1a2919 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-axfr-tester.json @@ -0,0 +1,29 @@ +{ + "title": "DNS Zone Transfer (AXFR) Tester", + "description": "Test if DNS zone transfers are allowed (potential security vulnerability)", + + "form": { + "title": "AXFR Test", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "nameserver": { + "label": "Nameserver (optional)", + "placeholder": "ns1.example.com" + }, + "test": "Test Zone Transfer", + "testing": "Testing...", + "error": "Please enter a domain" + }, + + "results": { + "title": "Zone Transfer Results", + "allowed": "Zone Transfer Allowed", + "denied": "Zone Transfer Denied", + "vulnerable": "Vulnerable", + "secure": "Secure", + "records_found": "{count} records retrieved", + "security_risk": "Security Risk" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-blacklist-checker.json b/src/lib/i18n/translations/en/diagnostics/dns-blacklist-checker.json new file mode 100644 index 00000000..a66d91ca --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-blacklist-checker.json @@ -0,0 +1,24 @@ +{ + "title": "DNS Blacklist Checker", + "description": "Check if an IP address is listed on DNS-based blacklists (RBLs/DNSBLs)", + + "form": { + "title": "Blacklist Check", + "ip": { + "label": "IP Address", + "placeholder": "8.8.8.8" + }, + "check": "Check Blacklists", + "checking": "Checking...", + "error": "Please enter a valid IP address" + }, + + "results": { + "title": "Blacklist Check Results", + "listed": "Listed", + "not_listed": "Not Listed", + "blacklists_checked": "{count} blacklists checked", + "clean": "Clean", + "issues_found": "{count} issues found" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-caa-effective.json b/src/lib/i18n/translations/en/diagnostics/dns-caa-effective.json new file mode 100644 index 00000000..22f2c29a --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-caa-effective.json @@ -0,0 +1,148 @@ +{ + "title": "CAA Effective Policy Checker", + "subtitle": "Check effective CAA (Certificate Authority Authorization) policies by walking up the domain label chain. Determine which Certificate Authorities are authorized to issue certificates for a domain.", + "examples": { + "title": "CAA Examples", + "items": { + "github": { + "domain": "github.com", + "description": "GitHub CAA configuration", + "tooltip": "Check CAA policy for github.com" + }, + "google": { + "domain": "www.google.com", + "description": "Google subdomain CAA inheritance", + "tooltip": "Check CAA policy for www.google.com" + }, + "stripe": { + "domain": "api.stripe.com", + "description": "Stripe API subdomain CAA", + "tooltip": "Check CAA policy for api.stripe.com" + }, + "cloudflare": { + "domain": "blog.cloudflare.com", + "description": "Cloudflare blog CAA setup", + "tooltip": "Check CAA policy for blog.cloudflare.com" + }, + "microsoft": { + "domain": "microsoft.com", + "description": "Microsoft CAA settings", + "tooltip": "Check CAA policy for microsoft.com" + }, + "amazon": { + "domain": "amazon.com", + "description": "Amazon CAA implementation", + "tooltip": "Check CAA policy for amazon.com" + } + } + }, + "form": { + "title": "CAA Policy Check", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter the domain name to check effective CAA policy for", + "invalidFormat": "Invalid domain name format", + "checkingButton": "Checking CAA...", + "checkButton": "Check CAA Policy" + }, + "error": { + "title": "CAA Check Failed", + "domainRequired": "Domain name is required", + "invalidDomain": "Invalid domain name format. Use valid domain names like \"example.com\"", + "checkFailed": "CAA check failed ({status})", + "invalidRequest": "Invalid request. Please check your domain name.", + "serviceUnavailable": "CAA check service temporarily unavailable. Please try again." + }, + "results": { + "title": "CAA Policy Results", + "copyButton": "Copy Results", + "copied": "Copied!", + "effectivePolicy": { + "title": "Effective CAA Policy", + "policyFoundAt": "Policy found at:", + "hasRecords": "This domain has CAA records that control certificate issuance", + "noPolicyFound": "No CAA policy found", + "noPolicyMessage": "No CAA records found in the domain chain for", + "implication": "This means any Certificate Authority can issue certificates for this domain." + }, + "singleLevel": { + "title": "Top-level CAA Policy", + "message": "CAA records found directly on {domain} - no domain tree traversal required." + }, + "chain": { + "title": "CAA Lookup Chain", + "description": "CAA records are looked up by walking up the domain tree until a CAA record is found or the root is reached.", + "levelUp": "level", + "levelsUp": "levels", + "up": "up", + "effectiveBadge": "Effective", + "noCAABadge": "No CAA" + }, + "tags": { + "issue": { + "description": "Authorized to issue certificates" + }, + "issuewild": { + "description": "Authorized to issue wildcard certificates" + }, + "iodef": { + "description": "Incident reporting contact" + }, + "unknown": { + "description": "Unknown CAA tag" + } + }, + "flags": { + "critical": "Critical flag set - unknown properties must be ignored", + "standard": "Standard flag", + "flagLabel": "Flag: {flag}" + } + }, + "copy": { + "header": "CAA Check for {domain}", + "generatedAt": "Generated at: {timestamp}", + "effectivePolicy": "Effective CAA Policy:", + "domainLabel": "Domain: {domain}", + "recordsLabel": "Records:", + "noRecords": "No CAA records found in the domain chain.", + "chainHeader": "CAA Chain (walked up from {domain}):" + }, + "education": { + "title": "Understanding CAA Records", + "format": { + "title": "CAA Record Format", + "example": "flag tag \"value\"", + "flagDescription": "Flag: 0 (non-critical) or 128 (critical)", + "tagDescription": "Tag: issue, issuewild, or iodef", + "valueDescription": "Value: CA domain or contact information" + }, + "tags": { + "title": "CAA Tags", + "issue": "issue: Authorizes a CA to issue certificates for the domain", + "issuewild": "issuewild: Authorizes a CA to issue wildcard certificates", + "iodef": "iodef: Specifies a URL/email for incident reporting" + }, + "lookupProcess": { + "title": "CAA Lookup Process", + "step1": "Check for CAA records at the requested domain", + "step2": "If none found, check the parent domain", + "step3": "Continue up the tree until CAA records are found", + "step4": "If no CAA records exist, any CA can issue certificates" + }, + "examples": { + "title": "Common CAA Examples", + "letsencrypt": { + "code": "0 issue \"letsencrypt.org\"", + "description": "Allow Let's Encrypt to issue certificates" + }, + "prohibitWildcard": { + "code": "0 issuewild \";\"", + "description": "Prohibit wildcard certificate issuance" + }, + "iodef": { + "code": "0 iodef \"mailto:security@example.com\"", + "description": "Report policy violations to security team" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-dmarc-check.json b/src/lib/i18n/translations/en/diagnostics/dns-dmarc-check.json new file mode 100644 index 00000000..f4ea41cf --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-dmarc-check.json @@ -0,0 +1,190 @@ +{ + "title": "DMARC Policy Checker", + "subtitle": "Analyze DMARC (Domain-based Message Authentication, Reporting & Conformance) policies. Check policy configuration, alignment settings, and identify potential security issues.", + + "examples": { + "title": "DMARC Examples", + "items": { + "google": { + "domain": "google.com", + "description": "Google DMARC policy", + "tooltip": "Check DMARC policy for google.com (Google DMARC policy)" + }, + "github": { + "domain": "github.com", + "description": "GitHub enterprise DMARC", + "tooltip": "Check DMARC policy for github.com (GitHub enterprise DMARC)" + }, + "microsoft": { + "domain": "microsoft.com", + "description": "Microsoft DMARC configuration", + "tooltip": "Check DMARC policy for microsoft.com (Microsoft DMARC configuration)" + }, + "paypal": { + "domain": "paypal.com", + "description": "PayPal strict DMARC policy", + "tooltip": "Check DMARC policy for paypal.com (PayPal strict DMARC policy)" + }, + "amazon": { + "domain": "amazon.com", + "description": "Amazon DMARC implementation", + "tooltip": "Check DMARC policy for amazon.com (Amazon DMARC implementation)" + }, + "salesforce": { + "domain": "salesforce.com", + "description": "Salesforce DMARC setup", + "tooltip": "Check DMARC policy for salesforce.com (Salesforce DMARC setup)" + } + } + }, + + "form": { + "title": "DMARC Policy Check", + "domainLabel": "Domain Name", + "domainTooltip": "Enter the domain to check DMARC policy for", + "domainPlaceholder": "example.com", + "checkButton": "Check DMARC Policy", + "checking": "Checking DMARC..." + }, + + "results": { + "title": "DMARC Policy Analysis", + "copy": "Copy Results", + "copied": "Copied!", + + "status": { + "secure": "DMARC Configuration Secure", + "issuesFound": "DMARC Issues Found", + "needsImprovement": "DMARC Needs Improvement", + "noCriticalIssues": "No critical issues detected", + "issuesIdentified": "{count} issue{plural} identified" + }, + + "recordSection": { + "title": "DMARC Record", + "location": "_dmarc.{domain}" + }, + + "policyConfiguration": { + "title": "Policy Configuration", + "mainPolicy": "Main Policy", + "subdomainPolicy": "Subdomain Policy", + "coverage": "Coverage", + "dkimAlignment": "DKIM Alignment", + "spfAlignment": "SPF Alignment", + "failureOptions": "Failure Options", + + "policies": { + "reject": "Reject non-compliant messages", + "quarantine": "Quarantine suspicious messages", + "none": "Monitor only, no action", + "unknown": "Unknown policy" + }, + + "alignment": { + "strict": "Strict", + "relaxed": "Relaxed", + "strictDescription": "Exact domain match", + "relaxedDescription": "Organizational domain match" + }, + + "coverageDescription": "of messages affected", + + "failureOptionsDescriptions": { + "both": "DKIM and SPF failure", + "any": "Any alignment failure", + "dkim": "DKIM failure only", + "spf": "SPF failure only", + "custom": "Custom configuration" + } + }, + + "reporting": { + "title": "Reporting Configuration", + "aggregateReports": "Aggregate Reports (RUA)", + "forensicReports": "Forensic Reports (RUF)", + "notConfigured": "Not configured" + }, + + "issues": { + "title": "Issues & Recommendations", + "messages": { + "policyNone": "Policy is set to \"none\" - no action taken on DMARC failures", + "relaxedAlignment": "Both DKIM and SPF alignment are relaxed - consider strict alignment", + "noAggregateReporting": "No aggregate reporting address (rua) specified", + "noForensicReporting": "No forensic reporting address (ruf) specified", + "partialCoverage": "Only {percentage}% of messages are subject to DMARC policy" + }, + "severityLevels": { + "high": "HIGH", + "medium": "MEDIUM", + "low": "LOW" + } + } + }, + + "noRecord": { + "title": "No DMARC Record Found", + "message": "Domain {domain} does not have a DMARC policy configured at {dmarcDomain}.", + "helpText": "This means the domain is not protected by DMARC. Consider implementing a DMARC policy to prevent email spoofing." + }, + + "error": { + "title": "DMARC Check Failed", + "lookupFailed": "DMARC check failed: {status}", + "unknownError": "Unknown error occurred" + }, + + "copy": { + "header": "DMARC Check for {domain}", + "generatedAt": "Generated at: {timestamp}", + "recordLabel": "DMARC Record:", + "parsedPolicyLabel": "Parsed Policy:", + "mainPolicyLabel": "Main Policy:", + "subdomainPolicyLabel": "Subdomain Policy:", + "dkimAlignmentLabel": "DKIM Alignment:", + "spfAlignmentLabel": "SPF Alignment:", + "percentageLabel": "Percentage:", + "aggregateReportsLabel": "Aggregate Reports:", + "forensicReportsLabel": "Forensic Reports:", + "failureOptionsLabel": "Failure Options:", + "issuesFoundLabel": "Issues Found:", + "noIssuesFound": "No issues found - DMARC configuration looks good!", + "alignmentStrict": "strict", + "alignmentRelaxed": "relaxed" + }, + + "educational": { + "title": "Understanding DMARC", + + "policies": { + "title": "DMARC Policies", + "none": "Monitor mode - collect data but take no action on failures", + "quarantine": "Mark suspicious messages, often sent to spam folder", + "reject": "Reject non-compliant messages outright (strongest security)" + }, + + "alignmentModes": { + "title": "Alignment Modes", + "relaxed": "Allows organizational domain matching (default)", + "strict": "Requires exact domain matching (more secure)" + }, + + "reportingTypes": { + "title": "Reporting Types", + "aggregate": "Daily summary reports of DMARC activity", + "forensic": "Real-time failure reports with message samples" + }, + + "bestPractices": { + "title": "Best Practices", + "items": { + "startMonitoring": "Start with p=none to monitor before enforcement", + "gradualEnforcement": "Gradually increase to p=quarantine then p=reject", + "setupReporting": "Set up aggregate reporting to monitor DMARC activity", + "strictAlignment": "Use strict alignment for enhanced security when possible", + "subdomainPolicy": "Consider subdomain policy for comprehensive coverage" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-dnssec-adflag.json b/src/lib/i18n/translations/en/diagnostics/dns-dnssec-adflag.json new file mode 100644 index 00000000..f5f63116 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-dnssec-adflag.json @@ -0,0 +1,24 @@ +{ + "title": "DNSSEC AD Flag Checker", + "description": "Check if DNS resolvers set the Authenticated Data (AD) flag for DNSSEC validation", + + "form": { + "title": "DNSSEC AD Flag Check", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "check": "Check AD Flag", + "checking": "Checking...", + "error": "Please enter a domain" + }, + + "results": { + "title": "AD Flag Results", + "ad_flag_set": "AD Flag Set", + "ad_flag_not_set": "AD Flag Not Set", + "dnssec_validated": "DNSSEC Validated", + "resolver": "Resolver", + "authenticated": "Authenticated" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-dnssec-validation-chain.json b/src/lib/i18n/translations/en/diagnostics/dns-dnssec-validation-chain.json new file mode 100644 index 00000000..b607a4cd --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-dnssec-validation-chain.json @@ -0,0 +1,124 @@ +{ + "title": "DNSSEC Validation Chain Checker", + "subtitle": "Validate DNSSEC chain from root to domain, verify DS/DNSKEY matching and RRSIG signatures", + + "examples": { + "title": "Quick Examples", + "items": { + "govuk": { + "domain": "gov.uk", + "description": "UK Government (DNSSEC Enabled)", + "tooltip": "Check DNSSEC for gov.uk" + }, + "cloudflare": { + "domain": "cloudflare.com", + "description": "Cloudflare (DNSSEC Enabled)", + "tooltip": "Check DNSSEC for cloudflare.com" + }, + "failed": { + "domain": "dnssec-failed.org", + "description": "DNSSEC Failed Test", + "tooltip": "Check DNSSEC for dnssec-failed.org" + }, + "google": { + "domain": "google.com", + "description": "Google (DNSSEC Enabled)", + "tooltip": "Check DNSSEC for google.com" + }, + "isc": { + "domain": "isc.org", + "description": "ISC (DNSSEC Pioneer)", + "tooltip": "Check DNSSEC for isc.org" + }, + "ietf": { + "domain": "ietf.org", + "description": "IETF (DNSSEC Enabled)", + "tooltip": "Check DNSSEC for ietf.org" + } + } + }, + + "form": { + "title": "Domain Name", + "domainLabel": "Domain to Validate", + "domainPlaceholder": "example.com", + "validateButton": "Validate DNSSEC Chain", + "validating": "Validating..." + }, + + "loading": { + "title": "Validating DNSSEC Chain", + "message": "Querying DNS records from root to {domain}..." + }, + + "error": { + "title": "Validation Failed", + "invalidDomain": "Please enter a valid domain name", + "validationFailed": "DNSSEC validation failed", + "unexpectedError": "An unexpected error occurred" + }, + + "results": { + "title": "DNSSEC Validation Results", + "summary": { + "valid": "DNSSEC Valid", + "invalid": "DNSSEC Invalid", + "domain": "Domain:", + "chainLinks": "Chain Links:", + "validated": "Validated:", + "status": "Status:", + "secure": "Secure", + "brokenChain": "Broken Chain", + "errors": "Validation Errors:" + }, + "chain": { + "title": "DNSSEC Chain", + "level": "Level {level}", + "ds": { + "title": "DS Records", + "tooltip": "Delegation Signer records link this zone to the parent zone", + "keyTag": "Key Tag:", + "keyTagTooltip": "Identifier for the DNSKEY this DS record refers to", + "algorithm": "Algorithm:", + "algorithmTooltip": "Cryptographic algorithm used for signing", + "digestType": "Digest Type:", + "digestTypeTooltip": "Hash algorithm used to create the digest", + "hash": "Hash:", + "hashTooltip": "Hash of the DNSKEY record" + }, + "dnskey": { + "title": "DNSKEY Records", + "tooltip": "Public keys used to verify DNSSEC signatures", + "keyTag": "Key Tag:", + "keyTagTooltip": "Identifier for this DNSKEY", + "flags": "Flags:", + "flagsTooltip": "Key properties: 256=ZSK, 257=KSK", + "algorithm": "Algorithm:", + "algorithmTooltip": "Cryptographic algorithm used for signing", + "matchedDS": "Matched DS", + "matchedDSTooltip": "This DNSKEY matches a DS record in the parent zone", + "ksk": "KSK", + "kskTooltip": "Key Signing Key - signs other DNSKEYs", + "zsk": "ZSK", + "zskTooltip": "Zone Signing Key - signs zone data" + }, + "rrsig": { + "title": "RRSIG Records", + "tooltip": "Digital signatures created with DNSKEY to authenticate DNS data", + "typeCovered": "Type Covered:", + "typeCoveredTooltip": "DNS record type this signature covers", + "keyTag": "Key Tag:", + "keyTagTooltip": "Identifies which DNSKEY created this signature", + "signer": "Signer:", + "signerTooltip": "Zone that created this signature", + "valid": "Valid", + "validTooltip": "Signature is valid and within time window", + "invalid": "Invalid", + "invalidTooltip": "Signature is expired or not yet valid" + }, + "errors": { + "title": "Errors" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-glue-check.json b/src/lib/i18n/translations/en/diagnostics/dns-glue-check.json new file mode 100644 index 00000000..514ad5df --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-glue-check.json @@ -0,0 +1,24 @@ +{ + "title": "DNS Glue Record Checker", + "description": "Check for proper glue records configuration for name servers", + + "form": { + "title": "Glue Record Check", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "check": "Check Glue Records", + "checking": "Checking...", + "error": "Please enter a domain" + }, + + "results": { + "title": "Glue Record Results", + "glue_required": "Glue Required", + "glue_found": "Glue Found", + "glue_missing": "Glue Missing", + "nameservers": "Nameservers", + "glue_records": "Glue Records" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-lookup.json b/src/lib/i18n/translations/en/diagnostics/dns-lookup.json new file mode 100644 index 00000000..fdca7450 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-lookup.json @@ -0,0 +1,130 @@ +{ + "title": "DNS Lookup Tool", + "subtitle": "Resolve DNS records for any domain using various public resolvers or custom DNS servers. Supports all common record types with detailed response information.", + + "recordTypes": { + "A": { + "label": "A", + "description": "IPv4 address records" + }, + "AAAA": { + "label": "AAAA", + "description": "IPv6 address records" + }, + "CNAME": { + "label": "CNAME", + "description": "Canonical name records" + }, + "MX": { + "label": "MX", + "description": "Mail exchange records" + }, + "TXT": { + "label": "TXT", + "description": "Text records" + }, + "NS": { + "label": "NS", + "description": "Name server records" + }, + "SOA": { + "label": "SOA", + "description": "Start of authority records" + }, + "CAA": { + "label": "CAA", + "description": "Certificate authority authorization" + }, + "PTR": { + "label": "PTR", + "description": "Pointer records" + }, + "SRV": { + "label": "SRV", + "description": "Service records" + } + }, + + "resolvers": { + "cloudflare": "Cloudflare (1.1.1.1)", + "google": "Google (8.8.8.8)", + "quad9": "Quad9 (9.9.9.9)", + "opendns": "OpenDNS (208.67.222.222)" + }, + + "examples": { + "items": { + "exampleA": { + "domain": "example.com", + "type": "A", + "description": "Basic A record lookup", + "tooltip": "Query A records for example.com" + }, + "googleMX": { + "domain": "google.com", + "type": "MX", + "description": "Mail server records", + "tooltip": "Query MX records for google.com" + }, + "cloudflareAAAA": { + "domain": "cloudflare.com", + "type": "AAAA", + "description": "IPv6 addresses", + "tooltip": "Query AAAA records for cloudflare.com" + }, + "githubDMARC": { + "domain": "_dmarc.github.com", + "type": "TXT", + "description": "DMARC policy record", + "tooltip": "Query TXT records for _dmarc.github.com" + }, + "microsoftTXT": { + "domain": "microsoft.com", + "type": "TXT", + "description": "Multiple TXT records (SPF, verification)", + "tooltip": "Query TXT records for microsoft.com" + }, + "netflixNS": { + "domain": "netflix.com", + "type": "NS", + "description": "Name server records", + "tooltip": "Query NS records for netflix.com" + } + } + }, + + "form": { + "title": "Lookup Configuration", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter the domain name to query", + "recordTypeLabel": "Record Type", + "recordTypeTooltip": "Select the DNS record type to query", + "dnsResolverLabel": "DNS Resolver", + "dnsResolverTooltip": "Choose a DNS resolver to use for the query", + "customResolverPlaceholder": "8.8.8.8 or custom IP", + "useCustomResolver": "Use custom resolver", + "performing": "Performing Lookup...", + "lookupButton": "Lookup DNS Records" + }, + + "error": { + "title": "Lookup Failed", + "invalidInput": "Invalid input", + "invalidRequest": "Invalid request. Please check your input values.", + "serviceUnavailable": "DNS lookup service temporarily unavailable. Please try again.", + "lookupFailed": "Lookup failed ({status})" + }, + + "noRecords": { + "title": "No Records Found", + "usingResolver": "Using resolver: {resolver}" + }, + + "results": { + "title": "DNS Records Found", + "copyButton": "Copy Results", + "ttlTooltip": "Time To Live - how long this record can be cached", + "noRecordsMessage": "No records found for {domain} ({type})" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-ns-soa-check.json b/src/lib/i18n/translations/en/diagnostics/dns-ns-soa-check.json new file mode 100644 index 00000000..8a1f9e57 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-ns-soa-check.json @@ -0,0 +1,135 @@ +{ + "title": "NS/SOA Consistency Checker", + "subtitle": "Verify DNS nameserver and SOA (Start of Authority) record consistency. Check that all listed nameservers resolve correctly and analyze SOA parameters for proper DNS configuration.", + + "examples": { + "title": "NS/SOA Examples", + "items": { + "google": { + "domain": "google.com", + "description": "Google DNS infrastructure check", + "tooltip": "Check NS/SOA consistency for google.com" + }, + "github": { + "domain": "github.com", + "description": "GitHub nameserver configuration", + "tooltip": "Check NS/SOA consistency for github.com" + }, + "cloudflare": { + "domain": "cloudflare.com", + "description": "Cloudflare NS/SOA setup", + "tooltip": "Check NS/SOA consistency for cloudflare.com" + }, + "stackoverflow": { + "domain": "stackoverflow.com", + "description": "Stack Overflow DNS consistency", + "tooltip": "Check NS/SOA consistency for stackoverflow.com" + }, + "microsoft": { + "domain": "microsoft.com", + "description": "Microsoft nameserver analysis", + "tooltip": "Check NS/SOA consistency for microsoft.com" + }, + "aws": { + "domain": "aws.amazon.com", + "description": "AWS subdomain NS/SOA check", + "tooltip": "Check NS/SOA consistency for aws.amazon.com" + } + } + }, + + "form": { + "title": "NS/SOA Check", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter the domain to check nameserver and SOA consistency for", + "checking": "Checking NS/SOA...", + "checkButton": "Check NS/SOA Records" + }, + + "results": { + "title": "NS/SOA Analysis Results", + "copyButton": "Copy Results", + "copied": "Copied!", + "status": { + "healthy": "DNS Configuration Healthy", + "issues": "DNS Issues Detected", + "problems": "DNS Configuration Problems" + }, + "nameservers": { + "title": "Nameservers ({count})", + "failedToResolve": "Failed to resolve" + }, + "soa": { + "title": "SOA (Start of Authority) Record", + "rawTitle": "Raw SOA Record", + "parsedTitle": "Parsed SOA Parameters", + "primaryNS": "Primary NS", + "primaryNSTooltip": "The primary nameserver for this zone", + "administrator": "Administrator", + "administratorTooltip": "Email address of the zone administrator (@ replaced with .)", + "serial": "Serial", + "serialTooltip": "Zone serial number - used to track zone changes", + "refresh": "Refresh", + "refreshTooltip": "How often secondary servers should check for updates", + "retry": "Retry", + "retryTooltip": "How long to wait before retrying a failed zone transfer", + "expire": "Expire", + "expireTooltip": "When secondary servers should stop answering queries if they can't contact the primary", + "minimumTTL": "Minimum TTL", + "minimumTTLTooltip": "Default TTL for negative responses (NXDOMAIN)" + }, + "analysis": { + "title": "Configuration Analysis", + "serialYYYYMMDDNN": "Serial number appears to use YYYYMMDDNN format (recommended)", + "serialSuggestion": "Consider using YYYYMMDDNN format for serial numbers for easier tracking", + "refreshGood": "Refresh interval ({time}) is within recommended range", + "refreshTooFrequent": "Refresh interval ({time}) is quite frequent - consider increasing", + "refreshTooLong": "Refresh interval ({time}) is quite long - consider reducing", + "retryGood": "Retry interval is properly configured", + "retryTooLong": "Retry interval should be less than refresh interval", + "retryTooShort": "Retry interval ({time}) might be too short", + "expireGood": "Expire time ({time}) provides good resilience", + "expireShort": "Expire time ({time}) is quite short - consider at least 1 week" + } + }, + + "error": { + "title": "NS/SOA Check Failed" + }, + + "education": { + "title": "Understanding NS and SOA Records", + "nsRecords": { + "title": "NS (Name Server) Records", + "description": "NS records specify which name servers are authoritative for a domain. All listed nameservers should:", + "resolveToValidIP": "Resolve to valid IP addresses", + "beReachable": "Be reachable and responsive", + "serveConsistent": "Serve consistent zone data", + "beDistributed": "Be geographically distributed for redundancy" + }, + "soaRecord": { + "title": "SOA (Start of Authority)", + "description": "The SOA record contains administrative information about the zone:", + "serial": "Serial: Version number for zone changes", + "refresh": "Refresh: How often secondaries check for updates", + "retry": "Retry: Wait time before retrying failed transfers", + "expire": "Expire: When to stop serving stale data", + "minimum": "Minimum: Default negative response TTL" + }, + "recommended": { + "title": "Recommended Values", + "refresh": "Refresh: 1-24 hours (3600-86400s)", + "retry": "Retry: 10-60 minutes (600-3600s)", + "expire": "Expire: 1-4 weeks (604800-2419200s)", + "minimum": "Minimum: 5 minutes to 1 hour (300-3600s)" + }, + "commonIssues": { + "title": "Common Issues", + "unreachable": "Unreachable nameservers: Can cause resolution failures", + "inconsistent": "Inconsistent data: Different responses from different NS", + "wrongValues": "Wrong SOA values: Too aggressive or too conservative timing", + "serialIssues": "Serial number issues: Outdated or incorrect format" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-propagation.json b/src/lib/i18n/translations/en/diagnostics/dns-propagation.json new file mode 100644 index 00000000..fed08974 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-propagation.json @@ -0,0 +1,140 @@ +{ + "title": "DNS Propagation Checker", + "subtitle": "Check DNS record propagation across multiple public DNS resolvers. Compare responses from Cloudflare, Google, Quad9, and OpenDNS to verify consistent DNS propagation worldwide.", + + "recordTypes": { + "A": { + "label": "A", + "description": "IPv4 address records" + }, + "AAAA": { + "label": "AAAA", + "description": "IPv6 address records" + }, + "CNAME": { + "label": "CNAME", + "description": "Canonical name records" + }, + "MX": { + "label": "MX", + "description": "Mail exchange records" + }, + "TXT": { + "label": "TXT", + "description": "Text records" + }, + "NS": { + "label": "NS", + "description": "Name server records" + } + }, + + "resolvers": { + "cloudflare": { + "name": "Cloudflare", + "ip": "1.1.1.1", + "location": "Global" + }, + "google": { + "name": "Google", + "ip": "8.8.8.8", + "location": "Global" + }, + "quad9": { + "name": "Quad9", + "ip": "9.9.9.9", + "location": "Global" + }, + "opendns": { + "name": "OpenDNS", + "ip": "208.67.222.222", + "location": "Global" + } + }, + + "examples": { + "title": "Propagation Examples", + "items": { + "googleA": { + "domain": "google.com", + "type": "A", + "description": "Check A record propagation", + "tooltip": "Check A record propagation for google.com" + }, + "githubAAAA": { + "domain": "github.com", + "type": "AAAA", + "description": "IPv6 propagation check", + "tooltip": "Check AAAA record propagation for github.com" + }, + "gmailMX": { + "domain": "gmail.com", + "type": "MX", + "description": "Mail server propagation", + "tooltip": "Check MX record propagation for gmail.com" + }, + "dmarcTXT": { + "domain": "_dmarc.google.com", + "type": "TXT", + "description": "DMARC policy propagation", + "tooltip": "Check TXT record propagation for _dmarc.google.com" + } + } + }, + + "form": { + "title": "Propagation Check Configuration", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter the domain name to check propagation for", + "recordTypeLabel": "Record Type", + "recordTypeTooltip": "Select the DNS record type to check", + "checking": "Checking Propagation...", + "checkButton": "Check DNS Propagation" + }, + + "error": { + "title": "Propagation Check Failed", + "propagationFailed": "Propagation check failed: {status}", + "unknownError": "Unknown error occurred" + }, + + "results": { + "title": "Propagation Results", + "fullyPropagated": "Fully Propagated", + "inconsistentResults": "Inconsistent Results", + "copyAll": "Copy All Results", + "copied": "Copied!", + "errorMessage": "Error: {error}", + "noRecordsFound": "No records found", + "ttlTooltip": "Time To Live", + "lastChecked": "Last checked: {domain} ({type}) at {time}", + "copyHeader": "DNS Propagation Check for {domain} ({type})", + "copyCheckedAt": "Checked at: {time}", + "copyNoRecords": "No records found" + }, + + "education": { + "title": "Understanding DNS Propagation", + "whatIsPropagation": { + "title": "What is DNS Propagation?", + "description": "DNS propagation refers to the time it takes for DNS changes to spread across the internet. Different resolvers may cache records for different periods, leading to temporary inconsistencies." + }, + "factors": { + "title": "Factors Affecting Propagation", + "ttl": "TTL Values: Lower TTL means faster propagation", + "caching": "Resolver Caching: Each resolver has its own cache policies", + "geography": "Geographic Location: Physical distance affects update speed", + "infrastructure": "DNS Infrastructure: Authoritative server response time" + }, + "interpreting": { + "title": "Interpreting Results", + "fullyPropagated": "Fully Propagated: All resolvers return identical results", + "inconsistent": "Inconsistent: Different resolvers return different results", + "error": "Error: Resolver failed to respond or returned an error" + }, + "resolversTested": { + "title": "DNS Resolvers Tested" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-query-performance.json b/src/lib/i18n/translations/en/diagnostics/dns-query-performance.json new file mode 100644 index 00000000..f75164d6 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-query-performance.json @@ -0,0 +1,28 @@ +{ + "title": "DNS Query Performance", + "description": "Measure DNS query performance and response times for a domain", + + "form": { + "title": "DNS Performance Test", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "record_type": { + "label": "Record Type" + }, + "test": "Test DNS Performance", + "testing": "Testing...", + "error": "Please enter a domain" + }, + + "results": { + "title": "DNS Performance Results", + "query_time": "Query Time", + "avg_time": "Average Time", + "min_time": "Min Time", + "max_time": "Max Time", + "nameserver": "Nameserver", + "ttl": "TTL" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-reverse-lookup.json b/src/lib/i18n/translations/en/diagnostics/dns-reverse-lookup.json new file mode 100644 index 00000000..839a302f --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-reverse-lookup.json @@ -0,0 +1,91 @@ +{ + "title": "Reverse DNS Lookup", + "subtitle": "Perform reverse DNS lookups (PTR records) to find hostnames associated with IP addresses. Automatically handles both IPv4 and IPv6 addresses with proper .in-addr.arpa and .ip6.arpa zone formatting.", + "resolvers": { + "cloudflare": "Cloudflare (1.1.1.1)", + "google": "Google (8.8.8.8)", + "quad9": "Quad9 (9.9.9.9)", + "opendns": "OpenDNS (208.67.222.222)" + }, + "examples": { + "title": "Common IP Examples", + "items": { + "googleDNS": { + "ip": "8.8.8.8", + "description": "Google DNS server", + "tooltip": "Perform reverse lookup for 8.8.8.8 (Google DNS server)" + }, + "cloudflareDNS": { + "ip": "1.1.1.1", + "description": "Cloudflare DNS server", + "tooltip": "Perform reverse lookup for 1.1.1.1 (Cloudflare DNS server)" + }, + "googleIPv6": { + "ip": "2001:4860:4860::8888", + "description": "Google IPv6 DNS", + "tooltip": "Perform reverse lookup for 2001:4860:4860::8888 (Google IPv6 DNS)" + }, + "cloudflareIPv6": { + "ip": "2606:4700:4700::1111", + "description": "Cloudflare IPv6 DNS", + "tooltip": "Perform reverse lookup for 2606:4700:4700::1111 (Cloudflare IPv6 DNS)" + } + } + }, + "form": { + "title": "Reverse Lookup Configuration", + "ipLabel": "IP Address", + "ipPlaceholder": "8.8.8.8 or 2001:db8::1", + "ipTooltip": "Enter an IPv4 or IPv6 address to perform reverse lookup", + "dnsResolverLabel": "DNS Resolver", + "dnsResolverTooltip": "Choose a DNS resolver to use for the query", + "customResolverPlaceholder": "8.8.8.8 or custom IP", + "useCustomResolver": "Use custom resolver", + "invalidFormat": "Invalid IP address format", + "performingLookup": "Performing Reverse Lookup...", + "lookupButton": "Reverse Lookup IP" + }, + "error": { + "title": "Reverse Lookup Failed", + "invalidIPFormat": "Invalid IP address format", + "lookupFailed": "Reverse lookup failed: {status}" + }, + "results": { + "title": "Reverse DNS Results", + "copyButton": "Copy Results", + "copied": "Copied!", + "ipAddressLabel": "IP Address:", + "ipAddressTooltip": "The IP address that was queried", + "reverseZoneLabel": "Reverse Zone:", + "reverseZoneTooltip": "The reverse DNS zone that was queried (automatically generated)", + "ptrRecordsFound": "PTR Records Found:", + "ttlTooltip": "Time To Live - how long this record can be cached", + "noRecords": { + "title": "No PTR records found for", + "helpText": "This IP address may not have a reverse DNS entry configured." + } + }, + "education": { + "title": "About Reverse DNS Lookups", + "howItWorks": { + "title": "How it Works", + "description": "Reverse DNS converts IP addresses to hostnames using PTR records. IPv4 addresses use .in-addr.arpa zones, while IPv6 addresses use .ip6.arpa zones with each nibble reversed." + }, + "useCases": { + "title": "Common Use Cases", + "items": { + "emailVerification": "Email server verification", + "securityAnalysis": "Security analysis and logging", + "troubleshooting": "Network troubleshooting", + "ownership": "Identifying server ownership" + } + }, + "zoneFormat": { + "title": "Zone Format Examples", + "ipv4Label": "IPv4:", + "ipv4Example": "8.8.8.8 β†’ 8.8.8.8.in-addr.arpa", + "ipv6Label": "IPv6:", + "ipv6Example": "2001:db8::1 β†’ 1.0.0.0...b.d.0.1.0.0.2.ip6.arpa" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-soa-serial.json b/src/lib/i18n/translations/en/diagnostics/dns-soa-serial.json new file mode 100644 index 00000000..6366b5c0 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-soa-serial.json @@ -0,0 +1,156 @@ +{ + "title": "SOA Serial Analyzer", + "subtitle": "Analyze Start of Authority (SOA) records to interpret serial number formats and examine DNS zone timing parameters. SOA records contain critical zone metadata including serial numbers for change tracking and timing values for zone transfers.", + + "resolvers": { + "cloudflare": "Cloudflare (1.1.1.1)", + "google": "Google (8.8.8.8)", + "quad9": "Quad9 (9.9.9.9)", + "opendns": "OpenDNS (208.67.222.222)" + }, + + "examples": { + "title": "Domain Examples", + "items": { + "google": { + "domain": "google.com", + "description": "High-traffic domain with frequent updates", + "tooltip": "Analyze SOA record for google.com (High-traffic domain with frequent updates)" + }, + "github": { + "domain": "github.com", + "description": "Tech company with modern DNS management", + "tooltip": "Analyze SOA record for github.com (Tech company with modern DNS management)" + }, + "cloudflare": { + "domain": "cloudflare.com", + "description": "DNS provider with optimal configurations", + "tooltip": "Analyze SOA record for cloudflare.com (DNS provider with optimal configurations)" + }, + "iana": { + "domain": "iana.org", + "description": "Internet standards organization", + "tooltip": "Analyze SOA record for iana.org (Internet standards organization)" + }, + "rfcEditor": { + "domain": "rfc-editor.org", + "description": "Official RFC publication site", + "tooltip": "Analyze SOA record for rfc-editor.org (Official RFC publication site)" + }, + "example": { + "domain": "example.com", + "description": "Reserved example domain (RFC 2606)", + "tooltip": "Analyze SOA record for example.com (Reserved example domain (RFC 2606))" + } + } + }, + + "form": { + "title": "SOA Analysis Configuration", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter a domain name to analyze its SOA record", + "resolverLabel": "DoH Resolver", + "resolverTooltip": "Choose a DNS-over-HTTPS resolver for the query", + "analyzing": "Analyzing SOA Record...", + "analyzeButton": "Analyze SOA" + }, + + "results": { + "title": "SOA Analysis for {name}", + "copyButton": "Copy Raw JSON", + "copied": "Copied!", + "domainLabel": "Domain:", + "domainTooltip": "The domain that was queried", + "resolverLabel": "DoH Resolver:", + "resolverTooltip": "DNS-over-HTTPS resolver used for the query", + + "serialAnalysis": { + "title": "Serial Number Analysis", + "notAvailable": "Not available", + "unknown": "Unknown", + "formatLabel": "Format:", + "formatUnknown": "Unknown", + "formatExplanation": "No analysis available", + "parsedDateLabel": "Parsed Date:", + "parsedYear": "Year: {year}", + "parsedMonth": "Month: {month}", + "parsedDay": "Day: {day}", + "parsedRevision": "Revision: {revision}", + "validityLabel": "Validity:", + "valid": "Valid format", + "invalid": "Invalid or unusual format" + }, + + "soaDetails": { + "title": "SOA Record Details", + "primaryServer": "Primary Server:", + "contactEmail": "Contact Email:", + "ttl": "TTL:", + "notAvailable": "Not available" + }, + + "timing": { + "title": "Zone Timing Parameters", + "refresh": { + "title": "Refresh", + "description": "How often secondary servers check for updates" + }, + "retry": { + "title": "Retry", + "description": "Retry interval after failed refresh attempts" + }, + "expire": { + "title": "Expire", + "description": "When secondary servers stop serving the zone" + }, + "minimum": { + "title": "Minimum", + "description": "Minimum TTL for negative responses" + } + }, + + "assessment": { + "title": "Configuration Assessment" + } + }, + + "error": { + "title": "SOA Analysis Failed", + "troubleshooting": "Troubleshooting Tips:", + "tips": { + "validDomain": "Ensure the domain name is valid and has a SOA record", + "tryDifferent": "Try a different DoH resolver if the current one fails", + "someResolvers": "Some domains may not respond to certain resolvers", + "checkDomain": "Check if the domain exists and is properly configured" + } + }, + + "education": { + "title": "About SOA Records and Serial Numbers", + "whatIsSOA": { + "title": "What is a SOA Record?", + "description": "Start of Authority records contain administrative information about a DNS zone, including the primary server, contact email, and timing parameters that control zone transfers and caching behavior." + }, + "serialFormats": { + "title": "Serial Number Formats", + "yyyymmddnn": "YYYYMMDDNN: Date-based format (e.g., 2024031501 = March 15, 2024, revision 01)", + "unixTimestamp": "Unix Timestamp: Seconds since epoch (e.g., 1710518400)", + "sequential": "Sequential: Simple incrementing numbers (e.g., 1, 2, 3...)" + }, + "timingParams": { + "title": "Timing Parameters", + "refresh": "Refresh: How often secondaries check for updates", + "retry": "Retry: Retry interval after failed transfers", + "expire": "Expire: When to stop serving if updates fail", + "minimum": "Minimum: TTL for negative (NXDOMAIN) responses" + }, + "bestPractices": { + "title": "Best Practices", + "useYYYYMMDDNN": "Use YYYYMMDDNN format for predictable versioning", + "setRefresh": "Set refresh to 3600-7200s for most zones", + "retryShorter": "Retry should be shorter than refresh (1800-3600s)", + "expireLonger": "Expire should be much longer (604800-1209600s)" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-spf-evaluator.json b/src/lib/i18n/translations/en/diagnostics/dns-spf-evaluator.json new file mode 100644 index 00000000..4bd88dcc --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-spf-evaluator.json @@ -0,0 +1,152 @@ +{ + "title": "SPF Record Evaluator", + "description": "Analyze SPF (Sender Policy Framework) records with recursive expansion of includes and redirects. Check DNS lookup limits and identify potential policy issues.", + + "examples": [ + { + "domain": "google.com", + "description": "Google SPF with multiple includes" + }, + { + "domain": "github.com", + "description": "GitHub SPF record structure" + }, + { + "domain": "mailchimp.com", + "description": "MailChimp complex SPF policy" + }, + { + "domain": "salesforce.com", + "description": "Salesforce enterprise SPF" + }, + { + "domain": "microsoft.com", + "description": "Microsoft Office 365 SPF" + }, + { + "domain": "atlassian.com", + "description": "Atlassian SPF configuration" + } + ], + + "examplesSection": { + "title": "SPF Examples", + "tooltip": "Evaluate SPF record for {domain} ({description})" + }, + + "form": { + "title": "SPF Evaluation", + "domain": { + "label": "Domain Name", + "placeholder": "example.com", + "tooltip": "Enter the domain to evaluate SPF records for" + }, + "evaluate": "Evaluate SPF Record", + "evaluating": "Evaluating SPF...", + "errors": { + "unknownError": "Unknown error occurred" + } + }, + + "results": { + "title": "SPF Evaluation Results", + "copy": "Copy Results", + "copied": "Copied!", + + "status": { + "noEvaluation": "No evaluation performed", + "limitExceeded": "DNS lookup limit exceeded ({count}/10)", + "highCount": "High DNS lookup count ({count}/10)", + "lookupsUsed": "DNS lookups used: {count}/10" + }, + + "record": { + "title": "Original SPF Record" + }, + + "mechanisms": { + "title": "Direct Mechanisms", + "types": { + "version": "version", + "passAll": "pass all", + "failAll": "fail all", + "softFailAll": "soft fail all", + "neutralAll": "neutral all", + "ipv4": "IPv4", + "ipv6": "IPv6", + "aRecord": "A record", + "mxRecord": "MX record", + "existsCheck": "exists check", + "ptrRecord": "PTR record", + "other": "other" + } + }, + + "includes": { + "title": "Include Chain", + "types": { + "include": "include", + "redirect": "redirect" + }, + "redirectTo": "redirect to: {domain}" + }, + + "redirects": { + "title": "Redirects" + } + }, + + "error": { + "title": "SPF Evaluation Failed" + }, + + "copyTemplate": { + "header": "SPF Evaluation for {domain}", + "generated": "Generated at: {timestamp}", + "originalRecord": "Original SPF Record:\n{record}", + "mechanismsHeader": "Mechanisms:", + "mechanismItem": " {mechanism}", + "includesHeader": "Includes:", + "includeItem": "{indent}{type}: {domain}", + "includeError": " (Error: {error})", + "statusHeader": "\nStatus: {message}" + }, + + "education": { + "title": "Understanding SPF Records", + + "mechanisms": { + "title": "SPF Mechanisms", + "all": "Matches all addresses (use carefully)", + "ipAddresses": "Matches specific IP addresses or ranges", + "records": "Matches A or MX record addresses", + "include": "References another domain's SPF record", + "redirect": "Redirects to another domain's SPF record" + }, + + "qualifiers": { + "title": "SPF Qualifiers", + "pass": "(Pass): Explicitly allow", + "fail": "(Fail): Explicitly deny", + "softFail": "(Soft Fail): Mark as suspicious", + "neutral": "(Neutral): No explicit policy" + }, + + "dnsLimits": { + "title": "DNS Lookup Limits", + "description": "SPF has a limit of 10 DNS lookups to prevent infinite loops and reduce load. This includes:", + "includeMechanisms": "Each {mechanism} mechanism", + "recordMechanisms": "Each {mechanisms} mechanism", + "redirectLookups": "Lookups from {modifier} modifiers" + }, + + "bestPractices": { + "title": "Best Practices", + "keepUnderLimit": "Keep DNS lookups under the 10-lookup limit", + "endWithAll": "End with {failAll} or {softFailAll} for security", + "useIpAddresses": "Use IP addresses when possible to reduce lookups", + "avoidNesting": "Avoid excessive nesting of includes", + "regularAudit": "Regularly audit and update SPF records" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-spf-flatten.json b/src/lib/i18n/translations/en/diagnostics/dns-spf-flatten.json new file mode 100644 index 00000000..8b533234 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-spf-flatten.json @@ -0,0 +1,25 @@ +{ + "title": "SPF Record Flattener", + "description": "Flatten SPF records by resolving includes to reduce DNS lookups", + + "form": { + "title": "SPF Flattening", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "flatten": "Flatten SPF", + "flattening": "Flattening...", + "error": "Please enter a domain" + }, + + "results": { + "title": "Flattened SPF Record", + "original": "Original SPF", + "flattened": "Flattened SPF", + "lookup_count": "DNS Lookup Count", + "original_lookups": "Original Lookups", + "flattened_lookups": "Flattened Lookups", + "savings": "Lookup Savings" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/dns-trace.json b/src/lib/i18n/translations/en/diagnostics/dns-trace.json new file mode 100644 index 00000000..6c6b3b3a --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/dns-trace.json @@ -0,0 +1,131 @@ +{ + "title": "DNS Trace Tool", + "subtitle": "Iterative trace from root to authoritative nameservers via DNS over HTTPS", + + "examples": { + "title": "Quick Examples", + "items": { + "cloudflare": { + "domain": "www.cloudflare.com", + "description": "Cloudflare edge network", + "tooltip": "Trace DNS resolution path for www.cloudflare.com" + }, + "google": { + "domain": "www.google.com", + "description": "Popular service with CDN", + "tooltip": "Trace DNS resolution path for www.google.com" + }, + "github": { + "domain": "github.com", + "description": "GitHub platform trace", + "tooltip": "Trace DNS resolution path for github.com" + }, + "bbc": { + "domain": "bbc.co.uk", + "description": "Multi-level TLD (.co.uk)", + "tooltip": "Trace DNS resolution path for bbc.co.uk" + }, + "aws": { + "domain": "aws.amazon.com", + "description": "AWS subdomain delegation", + "tooltip": "Trace DNS resolution path for aws.amazon.com" + }, + "alicia": { + "domain": "aliciasykes.com", + "description": "Homepage hosted on Vercel", + "tooltip": "Trace DNS resolution path for aliciasykes.com" + } + } + }, + + "form": { + "title": "Trace Configuration", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "tracing": "Tracing...", + "traceButton": "Trace" + }, + + "loading": { + "title": "Performing DNS Trace", + "message": "Following the DNS resolution path from root servers to authoritative nameservers..." + }, + + "error": { + "title": "Trace Failed", + "emptyDomain": "Please enter a domain name", + "failed": "Failed to trace domain", + "general": "An error occurred" + }, + + "results": { + "pathTitle": "Trace Path", + "step": { + "query": "Query:", + "server": "Server:", + "response": "Response:", + "typeTooltip": "Type of DNS query operation", + "timingTooltip": "Time taken for this query", + "queryTooltip": "Domain name being queried", + "qtypeTooltip": "DNS record type requested", + "serverTooltip": "DNS server that responded to this query", + "responseTooltip": "Response received from the DNS server", + "referral": "Referral to {nameservers}", + "nodata": "No data for this record type", + "nxdomain": "Domain does not exist" + }, + "flags": { + "aa": "Authoritative Answer", + "ad": "Authenticated Data (DNSSEC)", + "rd": "Recursion Desired", + "ra": "Recursion Available" + }, + "summary": { + "title": "Trace Summary", + "totalTime": { + "label": "Total Time", + "tooltip": "Total time for the complete DNS trace" + }, + "dnsQueries": { + "label": "DNS Queries", + "tooltip": "Number of DNS queries performed during trace" + }, + "finalServer": { + "label": "Final Server", + "tooltip": "Final authoritative server that provided the answer" + }, + "recordType": { + "label": "Record Type", + "tooltip": "Type of DNS record that was traced" + }, + "totalHops": { + "label": "Total Hops", + "tooltip": "Number of DNS resolution hops from root to authoritative" + }, + "avgLatency": { + "label": "Avg Latency", + "tooltip": "Average response time per DNS query" + }, + "dnssecStatus": { + "label": "DNSSEC Status", + "tooltip": "DNSSEC validation status for the final response", + "valid": "Valid", + "notValidated": "Not Validated" + }, + "authoritative": { + "label": "Authoritative", + "tooltip": "Whether the final answer came from an authoritative server", + "yes": "Yes", + "no": "No" + }, + "resolutionPath": { + "label": "Resolution Path", + "tooltip": "The path taken through different DNS servers" + }, + "finalAnswer": { + "label": "Final Answer", + "tooltip": "The final answer received from the authoritative server" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/email-dmarc-check.json b/src/lib/i18n/translations/en/diagnostics/email-dmarc-check.json new file mode 100644 index 00000000..9b069c10 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/email-dmarc-check.json @@ -0,0 +1,139 @@ +{ + "title": "Email DMARC Policy Checker", + "subtitle": "Check DMARC policy for email authentication, analyze deliverability impact, and get implementation recommendations", + "examples": { + "title": "DMARC Examples", + "items": { + "gmail": { + "domain": "gmail.com", + "description": "Google Gmail DMARC policy", + "tooltip": "Check DMARC configuration for gmail.com" + }, + "outlook": { + "domain": "outlook.com", + "description": "Microsoft Outlook DMARC setup", + "tooltip": "Check DMARC configuration for outlook.com" + }, + "github": { + "domain": "github.com", + "description": "GitHub enterprise DMARC", + "tooltip": "Check DMARC configuration for github.com" + }, + "paypal": { + "domain": "paypal.com", + "description": "PayPal strict DMARC policy", + "tooltip": "Check DMARC configuration for paypal.com" + }, + "amazon": { + "domain": "amazon.com", + "description": "Amazon DMARC implementation", + "tooltip": "Check DMARC configuration for amazon.com" + }, + "salesforce": { + "domain": "salesforce.com", + "description": "Salesforce DMARC configuration", + "tooltip": "Check DMARC configuration for salesforce.com" + } + } + }, + "form": { + "title": "DMARC Policy Check", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "checkButton": "Check DMARC Policy", + "checking": "Checking DMARC..." + }, + "results": { + "title": "DMARC Policy Analysis", + "copyButton": "Copy Results", + "copied": "Copied!", + "deliverability": { + "title": "Email Deliverability Impact", + "recommendations": "Deliverability Recommendations" + }, + "record": { + "title": "DMARC Record" + }, + "policy": { + "title": "Policy Configuration", + "mainPolicy": "Main Policy", + "subdomainPolicy": "Subdomain Policy", + "coverage": "Coverage Percentage", + "coverageDescription": "Percentage of mail subject to policy", + "dkimAlignment": "DKIM Alignment", + "spfAlignment": "SPF Alignment", + "failureOptions": "Failure Reporting Options", + "values": { + "reject": "Block all unauthorized emails", + "quarantine": "Mark as spam/junk", + "none": "Monitor only, no action", + "unknown": "Unknown policy", + "strict": "Strict", + "relaxed": "Relaxed", + "strictDescription": "Exact domain match required", + "relaxedDescription": "Organizational domain match allowed" + }, + "failureOptionsValues": { + "0": "Generate reports if all mechanisms fail", + "1": "Generate reports if any mechanism fails", + "d": "Generate reports if DKIM check fails", + "s": "Generate reports if SPF check fails", + "custom": "Custom failure reporting configuration" + } + }, + "reporting": { + "title": "Email Reporting Configuration", + "aggregateTitle": "Aggregate Reports (RUA)", + "aggregateDescription": "Daily summaries of DMARC activity", + "aggregateNotConfigured": "Not configured", + "aggregateNotConfiguredHint": "Missing aggregate reporting - consider adding rua=", + "forensicTitle": "Forensic Reports (RUF)", + "forensicDescription": "Real-time failure reports with message samples", + "forensicNotConfigured": "Not configured", + "forensicNotConfiguredHint": "Optional - provides detailed failure analysis" + } + }, + "noRecord": { + "title": "No DMARC Record Found", + "message": "No DMARC record was found for {domain}", + "impactTitle": "Email Deliverability Impact:", + "impacts": { + "noProtection": "No protection against email spoofing", + "reputation": "May affect email reputation with major providers", + "visibility": "Missing visibility into email authentication failures", + "recommendation": "Consider implementing DMARC starting with p=none for monitoring" + } + }, + "error": { + "title": "DMARC Check Failed" + }, + "education": { + "title": "Understanding DMARC for Email Delivery", + "policiesTitle": "DMARC Policies & Email Impact", + "policies": { + "none": "none: Monitoring mode - no action taken on failures, only reporting", + "quarantine": "quarantine: Failed emails are marked as spam or moved to quarantine", + "reject": "reject: Failed emails are rejected outright, strongest protection" + }, + "bestPracticesTitle": "Email Delivery Best Practices", + "bestPractices": { + "startNone": "Start with p=none to monitor without affecting email delivery", + "gradual": "Gradually move to p=quarantine, then p=reject as you fix issues", + "reporting": "Set up aggregate reporting (rua=) to track authentication results", + "testAlignment": "Test DKIM and SPF alignment before enforcing strict policies", + "subdomain": "Consider subdomain policy (sp=) if subdomains send email" + }, + "alignmentTitle": "Alignment Modes", + "alignment": { + "relaxed": "Relaxed (r): Allows organizational domain matches (subdomain.example.com matches example.com)", + "strict": "Strict (s): Requires exact domain match for DKIM/SPF alignment" + }, + "issuesTitle": "Common Email Delivery Issues", + "issues": { + "thirdParty": "Third-party senders may fail if not properly configured in SPF/DKIM", + "forwarding": "Email forwarding can break SPF alignment", + "mailingLists": "Mailing lists may modify messages, breaking DKIM signatures", + "percentage": "Use pct= to gradually roll out enforcement (default 100%)" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/email-greylist-tester.json b/src/lib/i18n/translations/en/diagnostics/email-greylist-tester.json new file mode 100644 index 00000000..aebeae7b --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/email-greylist-tester.json @@ -0,0 +1,220 @@ +{ + "title": "Email Greylisting Tester", + "description": "Test if a mail server implements greylisting by performing multiple connection attempts and analyzing rejection patterns", + + "examples": { + "title": "Quick Examples", + "protonmail": "ProtonMail (privacy-focused)", + "tutanota": "Tutanota (encrypted email)", + "icloud": "iCloud Mail", + "zoho": "Zoho Mail (business)", + "google": "Google Workspace MX", + "runbox": "Runbox (Exim-based)", + "port_label": "Port {port}" + }, + + "form": { + "title": "Test Mail Server Greylisting", + "domain": { + "label": "Domain", + "placeholder": "smtp.example.com" + }, + "port": { + "label": "Port", + "placeholder": "25" + }, + "attempts": { + "label": "Connection Attempts" + }, + "delay": { + "label": "Delay Between Attempts (seconds)" + }, + "test": "Test Greylisting", + "testing": "Testing...", + "error": "Please enter a domain name" + }, + + "loading": { + "title": "Testing Greylisting", + "description": "Testing {attempts} connection{plural} to {domain}:{port} with {delay}s delay...", + "note": "This will take approximately {duration} seconds to complete" + }, + + "results": { + "title": "Test Results for {domain}:{port}", + + "status": { + "detected": { + "title": "Greylisting Detected", + "description": "Server implements greylisting (Confidence: {confidence})" + }, + "not_detected": { + "title": "No Greylisting Detected", + "description": "Server does not appear to implement greylisting" + }, + "typical_delay": { + "title": "Typical Delay", + "description": "{delay} seconds between rejection and acceptance" + } + }, + + "attempts": { + "title": "Connection Attempts", + "failed": "Failed", + "response": "Response:", + "duration": "Duration:", + "error": "Error:" + }, + + "analysis": { + "title": "Analysis", + "server": "Server:", + "total_attempts": "Total Attempts:", + "successful_connections": "Successful Connections:", + "test_duration": "Test Duration:", + "initial_connection": "Initial Connection:", + "temporarily_rejected": "Temporarily rejected", + "accepted_immediately": "Accepted immediately", + "subsequent_attempts": "Subsequent Attempts:", + "accepted_after_delay": "Accepted after delay", + "still_rejected": "Still rejected", + "delay_duration": "Delay Duration:", + "delay_seconds": "{delay} seconds", + "confidence_level": "Confidence Level:" + } + }, + + "info": { + "title": "Understanding Greylisting", + + "what_is": { + "title": "What is Greylisting?", + "content": "Greylisting is an anti-spam technique where a mail server temporarily rejects emails from unknown senders. The sending server is expected to retry delivery after a delay (typically 1-15 minutes). Legitimate mail servers will retry, while many spam sources will not, effectively reducing spam without blocking legitimate mail." + }, + + "how_it_works": { + "title": "How Greylisting Works", + "steps": { + "initial": { + "title": "Initial Connection", + "description": "First email delivery attempt from unknown sender (triplet: sender IP, sender address, recipient address)" + }, + "rejection": { + "title": "Temporary Rejection", + "description": "Server responds with 4xx temporary error code (usually 450 or 451) asking sender to try again later" + }, + "retry": { + "title": "Retry Period", + "description": "Legitimate mail servers wait a specified period (usually 1-15 minutes) before retrying" + }, + "acceptance": { + "title": "Acceptance", + "description": "After retry delay, server accepts the email and adds triplet to whitelist for future deliveries" + } + } + }, + + "smtp_codes": { + "title": "SMTP Response Codes", + "220": { + "name": "Service Ready", + "description": "Server is ready to accept mail - no greylisting active" + }, + "421": { + "name": "Service Not Available", + "description": "Server is temporarily unavailable - may indicate greylisting" + }, + "450": { + "name": "Mailbox Unavailable", + "description": "Temporary failure - common greylisting response code" + }, + "451": { + "name": "Local Error", + "description": "Temporary error processing request - another greylisting indicator" + } + }, + + "confidence": { + "title": "Confidence Levels", + "high": { + "title": "High", + "description": "Explicit greylisting keywords in response + subsequent acceptance after delay" + }, + "medium": { + "title": "Medium", + "description": "Temporary rejection codes (450/451) + subsequent acceptance" + }, + "low": { + "title": "Low", + "description": "Inconsistent behavior or unclear rejection pattern" + }, + "none": { + "title": "None", + "description": "No greylisting detected - consistent acceptance or rejection" + } + }, + + "benefits": { + "title": "Benefits of Greylisting", + "spam_reduction": { + "title": "Spam Reduction", + "description": "Blocks 50-90% of spam without false positives, as most spam sources do not retry" + }, + "resource_efficient": { + "title": "Resource Efficient", + "description": "Minimal server resources required compared to content filtering" + }, + "no_false_positives": { + "title": "No False Positives", + "description": "Legitimate mail is always delivered, just with a slight delay" + }, + "compliant": { + "title": "Compliant", + "description": "Works within SMTP RFC standards - temporary rejection is expected behavior" + } + }, + + "drawbacks": { + "title": "Drawbacks of Greylisting", + "delivery_delay": { + "title": "Delivery Delay", + "description": "Initial emails from new senders are delayed by 1-15 minutes" + }, + "time_sensitive": { + "title": "Time-Sensitive Issues", + "description": "Can cause problems with password resets, verification codes, and urgent communications" + }, + "legitimate_failures": { + "title": "Legitimate Failures", + "description": "Some legitimate mail servers or services may not retry properly" + }, + "resource_usage": { + "title": "Resource Usage", + "description": "Requires database to track triplets and manage whitelist" + } + }, + + "best_practices": { + "title": "Best Practices", + "practices": [ + "Use shorter retry delays (1-5 minutes) to minimize user impact", + "Implement automatic whitelisting of known good servers", + "Provide bypass mechanisms for time-sensitive emails", + "Combine with SPF, DKIM, and DMARC for better protection", + "Monitor false positive rates and adjust policies accordingly", + "Whitelist common email services (Gmail, Outlook, etc.) to reduce delays" + ] + }, + + "quick_tips": { + "title": "Quick Tips", + "tips": [ + "Greylisting typically delays first-time emails by 1-15 minutes", + "Look for SMTP codes 450 or 451 as indicators of greylisting", + "Most greylisting implementations whitelist senders after successful retry", + "Greylisting is most effective when combined with other anti-spam measures", + "Some mail servers use adaptive greylisting that adjusts based on sender reputation" + ] + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/email-mail-tls-check.json b/src/lib/i18n/translations/en/diagnostics/email-mail-tls-check.json new file mode 100644 index 00000000..eec18542 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/email-mail-tls-check.json @@ -0,0 +1,67 @@ +{ + "title": "SMTP TLS Checker", + "description": "Check if a mail server supports TLS encryption (STARTTLS and Direct TLS)", + + "examples": { + "title": "Quick Examples", + "gmail_starttls": "Google Mail STARTTLS", + "outlook_starttls": "Microsoft Outlook STARTTLS", + "gmail_direct": "Gmail Direct TLS" + }, + + "form": { + "title": "Check Mail Server TLS", + "domain_placeholder": "mail.example.com", + "port_placeholder": "Port", + "check": "Check TLS", + "checking": "Checking...", + "error": "Please enter a domain name" + }, + + "loading": { + "title": "Checking TLS Support", + "description": "Testing connection to {domain}:{port}..." + }, + + "results": { + "title": "TLS Check Results for {domain}:{port}", + + "status": { + "starttls_supported": { + "title": "STARTTLS Supported", + "description": "Server supports upgrading to TLS" + }, + "direct_tls_supported": { + "title": "Direct TLS Supported", + "description": "Server supports implicit TLS" + }, + "not_supported": { + "title": "TLS Not Supported", + "description": "Server does not support TLS encryption" + } + }, + + "connection": { + "title": "Connection Details", + "tls_version": "TLS Version", + "cipher_suite": "Cipher Suite" + }, + + "certificate": { + "title": "Certificate Information", + "common_name": "Common Name", + "issuer": "Issuer", + "valid_from": "Valid From", + "valid_to": "Valid To", + "expires_in": "Expires in {days} days", + "serial_number": "Serial Number", + "fingerprint": "Fingerprint", + "alt_names": "Alternative Names ({count})" + } + }, + + "info": { + "title": "About SMTP TLS", + "quick_tips": "Quick Tips" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/email-mx-health.json b/src/lib/i18n/translations/en/diagnostics/email-mx-health.json new file mode 100644 index 00000000..58c7cc6c --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/email-mx-health.json @@ -0,0 +1,54 @@ +{ + "title": "Email MX Health Checker", + "description": "Check mail server (MX) health including DNS resolution and optional SMTP port connectivity testing. Verify your email infrastructure is properly configured and reachable.", + + "examples": { + "title": "MX Health Examples", + "gmail": "Google Gmail MX infrastructure", + "outlook": "Microsoft Outlook mail servers", + "yahoo": "Yahoo Mail MX configuration", + "protonmail": "ProtonMail secure email setup", + "fastmail": "FastMail professional hosting", + "github": "GitHub enterprise email setup" + }, + + "form": { + "title": "MX Health Check", + "domain": { + "label": "Domain Name", + "placeholder": "example.com" + }, + "check_ports": "Check SMTP port connectivity (25, 587, 465)", + "check": "Check MX Health", + "checking": "Checking MX Health..." + }, + + "results": { + "title": "MX Health Results", + "copy": "Copy Results", + "copied": "Copied!", + + "summary": { + "healthy": "Mail Infrastructure Healthy", + "issues": "Mail Infrastructure Issues", + "resolved": "{healthy} of {total} MX records resolved successfully", + "reachable": "{reachable} reachable via SMTP" + }, + + "stats": { + "mx_records": "MX Records", + "healthy": "Healthy", + "reachable": "Reachable", + "redundancy": "Redundancy" + }, + + "ports": { + "25": "SMTP (Standard)", + "587": "Submission (TLS)", + "465": "SMTPS (SSL)", + "other": "Port {port}", + "open": "Open", + "closed": "Closed" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/email-spf-check.json b/src/lib/i18n/translations/en/diagnostics/email-spf-check.json new file mode 100644 index 00000000..d7ce0ea4 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/email-spf-check.json @@ -0,0 +1,153 @@ +{ + "title": "Email SPF Policy Checker", + "description": "Check SPF (Sender Policy Framework) records for email authentication and deliverability. Analyze which servers are authorized to send email for your domain and assess delivery risk.", + + "examples": { + "title": "SPF Examples", + "gmail": "Google Gmail SPF policy", + "outlook": "Microsoft Outlook SPF setup", + "salesforce": "Salesforce SPF configuration", + "mailchimp": "MailChimp email service SPF", + "github": "GitHub enterprise SPF policy", + "sendgrid": "SendGrid email platform SPF" + }, + + "form": { + "title": "SPF Policy Check", + "domain": { + "label": "Domain Name", + "tooltip": "Enter the domain to check SPF policy for", + "placeholder": "example.com" + }, + "check": "Check SPF Policy", + "checking": "Checking SPF..." + }, + + "results": { + "title": "SPF Policy Analysis", + "copy": "Copy Results", + "copied": "Copied!", + + "deliverability": { + "title": "Email Deliverability Risk", + "low": "Strong SPF policy with hard fail - excellent email security", + "medium": "Moderate SPF policy with soft fail - good but could be stronger", + "high": "Weak or missing SPF policy - high risk of email spoofing" + }, + + "details": { + "hardFail": { + "label": "Hard Fail (-all)", + "enabled": "Enabled", + "disabled": "Disabled", + "enabledDesc": "Unauthorized emails will be rejected", + "disabledDesc": "Consider upgrading to -all for better security" + }, + "softFail": { + "label": "Soft Fail (~all)", + "enabled": "Enabled", + "disabled": "Disabled", + "enabledDesc": "Unauthorized emails marked as suspicious", + "hardFailInstead": "Using stronger hard fail instead", + "noEnforcement": "No SPF enforcement configured" + }, + "allowsAll": { + "label": "Allows All (+all)", + "enabled": "Enabled", + "warning": "WARNING: Any server can send email for this domain" + } + }, + + "record": { + "title": "SPF Record", + "location": "TXT record for {domain}" + }, + + "breakdown": { + "title": "SPF Policy Breakdown", + "lookupLimitExceeded": { + "title": "DNS Lookup Limit Exceeded", + "message": "This SPF record requires {count} DNS lookups, which exceeds the RFC limit of 10. This may cause delivery failures." + }, + "highLookupCount": { + "title": "High DNS Lookup Count", + "message": "This SPF record requires {count} DNS lookups. Consider optimizing to stay well below the 10-lookup limit." + }, + "directMechanisms": "Direct Mechanisms", + "includedPolicies": "Included SPF Policies" + }, + + "mechanisms": { + "spfVersion": "SPF version identifier", + "ipv4Address": "IPv4 address or network: {address}", + "ipv6Address": "IPv6 address or network: {address}", + "aRecordSpecific": "A record lookup for: {domain}", + "aRecordDomain": "A record lookup for domain itself", + "mxRecordSpecific": "MX record lookup for: {domain}", + "mxRecordDomain": "MX record lookup for domain itself", + "existsLookup": "DNS lookup test: {domain}", + "hardFail": "Hard fail - reject unauthorized emails", + "softFail": "Soft fail - mark unauthorized emails as suspicious", + "passAll": "Pass all - allow any server (dangerous)", + "neutral": "Neutral - no policy decision" + }, + + "noRecord": { + "title": "No SPF Record Found", + "message": "Domain {domain} does not have an SPF record configured.", + "warning": "This means anyone can send email claiming to be from this domain, significantly increasing spoofing risk." + }, + + "error": { + "title": "SPF Check Failed" + } + }, + + "copy": { + "header": "SPF Check for {domain}", + "generatedAt": "Generated at: {timestamp}", + "recordLabel": "SPF Record:", + "emailAnalysisLabel": "Email Deliverability Analysis:", + "riskLevel": "Risk Level:", + "hardFailLabel": "Hard Fail (-all):", + "softFailLabel": "Soft Fail (~all):", + "allowsAllLabel": "Allows All (+all):", + "expandedAnalysisLabel": "Expanded SPF Analysis:", + "totalLookupsLabel": "Total DNS lookups:", + "mechanismsLabel": "Mechanisms:", + "includesLabel": "Includes:", + "yes": "Yes", + "no": "No" + }, + + "education": { + "title": "Understanding SPF for Email", + "mechanisms": { + "title": "SPF Mechanisms", + "ip": "Authorize specific IP addresses or networks", + "amx": "Authorize servers from A or MX records", + "include": "Include another domain's SPF policy", + "all": "Final policy decision (+pass, ~soft fail, -hard fail)" + }, + "deliverability": { + "title": "Email Deliverability", + "hardFail": "Best security, blocks unauthorized senders", + "softFail": "Marks suspicious, doesn't block delivery", + "noSpf": "High spoofing risk, may affect deliverability", + "tooManyLookups": "Can cause delivery failures" + }, + "bestPractices": { + "title": "Best Practices", + "useHardFail": "Use -all for hard fail when possible", + "limitLookups": "Keep DNS lookups under 10 (preferably under 5)", + "testChanges": "Test SPF changes before deployment", + "monitorDelivery": "Monitor email delivery after SPF changes" + }, + "examples": { + "title": "Common SPF Examples", + "googleWorkspace": "Use Google Workspace with soft fail", + "specificIp": "Only allow specific IP with hard fail", + "aMxRecords": "Allow A and MX record servers with hard fail" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/glue-check.json b/src/lib/i18n/translations/en/diagnostics/glue-check.json new file mode 100644 index 00000000..329bd460 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/glue-check.json @@ -0,0 +1,86 @@ +{ + "title": "DNS Glue Check Tool", + "description": "Check which NS names require glue records and whether A/AAAA records exist", + + "examples": { + "title": "Quick Examples", + "items": [ + { + "zone": "cloudflare.com", + "description": "Multiple NS with full IPv4+IPv6 glue records" + }, + { + "zone": "yahoo.com", + "description": "Mixed glue status with warning (missing IPv6 on one NS)" + }, + { + "zone": "bbc.co.uk", + "description": "Mixed delegation: internal and external nameservers" + }, + { + "zone": "github.com", + "description": "All external nameservers (NSOne + AWS Route53)" + }, + { + "zone": "twitch.tv", + "description": "Different TLD (.tv) with external AWS nameservers" + }, + { + "zone": "apple.com", + "description": "Clean 4-nameserver setup with complete glue records" + } + ] + }, + + "form": { + "zoneName": { + "label": "Zone Name", + "placeholder": "example.com" + }, + "checkButton": { + "checking": "Checking...", + "default": "Check Glue" + } + }, + + "errors": { + "emptyZone": "Please enter a zone name", + "fetchFailed": "Failed to check glue records", + "genericError": "An error occurred" + }, + + "loading": { + "title": "Checking Glue Records", + "description": "Analyzing nameservers and checking for required glue records..." + }, + + "results": { + "title": "Glue Check Results", + "summary": { + "title": "Glue Check Summary", + "totalNameservers": "Total Nameservers", + "requiringGlue": "Requiring Glue", + "withValidGlue": "With Valid Glue", + "missingGlue": "Missing Glue" + }, + "nameservers": { + "glueRequired": "Glue Required", + "external": "External", + "aRecords": "A Records", + "aaaaRecords": "AAAA Records", + "noARecordsFound": "No A records found", + "noAaaaRecordsFound": "No AAAA records found", + "externalNS": "External nameserver", + "externalNSExplanation": "No glue records required as this nameserver is outside the zone" + }, + "status": { + "error": "Critical: No glue records found", + "warning": "Warning: Incomplete glue records", + "success": "Healthy: All glue records present" + } + }, + + "issues": { + "title": "Issues Found" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-compression.json b/src/lib/i18n/translations/en/diagnostics/http-compression.json new file mode 100644 index 00000000..68b95167 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-compression.json @@ -0,0 +1,26 @@ +{ + "title": "HTTP Compression Checker", + "description": "Check HTTP compression support and analyze compression ratios", + + "form": { + "title": "Compression Check", + "url": { + "label": "URL", + "placeholder": "https://example.com" + }, + "check": "Check Compression", + "checking": "Checking...", + "error": "Please enter a valid URL" + }, + + "results": { + "title": "Compression Analysis", + "compression_enabled": "Compression Enabled", + "compression_disabled": "Compression Disabled", + "encoding": "Encoding", + "original_size": "Original Size", + "compressed_size": "Compressed Size", + "compression_ratio": "Compression Ratio", + "savings": "{percent}% savings" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-cookie-security.json b/src/lib/i18n/translations/en/diagnostics/http-cookie-security.json new file mode 100644 index 00000000..140637b4 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-cookie-security.json @@ -0,0 +1,83 @@ +{ + "title": "HTTP Cookie Security Inspector", + "description": "Analyze Set-Cookie headers for Secure, HttpOnly, SameSite, and other security attributes", + + "examples": { + "title": "Cookie Security Examples", + "github": "Excellent security (Score: 91)", + "cloudflare": "Strong security (Score: 90)", + "paypal": "Good security (Score: 80)", + "ebay": "Many cookies (7 cookies, Score: 67)", + "nytimes": "No HttpOnly flags (Score: 59)", + "apple": "Poor security (1 cookie, Score: 37)", + "linkedin": "Large set (7 cookies, Score: 69)", + "no_cookies": "No cookies found" + }, + + "form": { + "title": "URL to Inspect", + "url": { + "label": "URL", + "placeholder": "https://example.com", + "error": "Please enter a valid HTTP/HTTPS URL" + }, + "inspect": "Inspect Cookies", + "analyzing": "Analyzing..." + }, + + "loading": { + "title": "Analyzing Cookies", + "description": "Inspecting Set-Cookie headers for security attributes..." + }, + + "results": { + "title": "Cookie Security Analysis", + + "score": { + "title": "Security Score", + "no_cookies": "No cookies", + "overall": "Overall Assessment", + "total_cookies": "Total Cookies:", + "secure_cookies": "Secure Cookies:", + "httponly_cookies": "HttpOnly Cookies:" + }, + + "cookies": { + "title": "Cookie Details", + "value": "Value:", + "attributes": { + "secure": "Secure", + "httponly": "HttpOnly", + "samesite": "SameSite: {sameSite}" + }, + "metadata": { + "domain": "Domain:", + "path": "Path:", + "expires": "Expires:", + "max_age": "Max-Age:", + "max_age_suffix": "s" + }, + "issues": { + "title": "Security Issues:" + } + }, + + "none": { + "title": "No Cookies Found", + "message": "The server did not send any Set-Cookie headers in the response." + }, + + "recommendations": { + "title": "Security Recommendations" + } + }, + + "error": { + "title": "Cookie Inspection Failed", + "failed": "Failed to check cookie security", + "not_found": "Domain not found. Please check the URL and try again.", + "connection_refused": "Connection refused. The server may be down or unreachable.", + "timeout": "Request timed out. The server may be slow to respond.", + "unexpected": "An unexpected error occurred. Please try again." + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-cors-check.json b/src/lib/i18n/translations/en/diagnostics/http-cors-check.json new file mode 100644 index 00000000..8f99feea --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-cors-check.json @@ -0,0 +1,128 @@ +{ + "title": "CORS Policy Checker", + "description": "Test and analyze Cross-Origin Resource Sharing (CORS) policies for APIs and web services", + + "examples": { + "title": "CORS Examples", + "github": "GitHub API CORS policy", + "httpbin": "HTTPBin CORS test", + "weather": "Open weather API", + "placeholder": "JSON Placeholder API" + }, + + "form": { + "title": "CORS Test Configuration", + "url": { + "label": "Target URL", + "tooltip": "The API endpoint or resource you want to test", + "placeholder": "https://api.example.com", + "error": "Invalid URL format", + "required": "URL is required" + }, + "origin": { + "label": "Origin", + "tooltip": "Your website's origin (where the request would come from)", + "placeholder": "https://yoursite.com", + "error": "Invalid origin format", + "required": "Origin is required", + "invalid": "Invalid URL or Origin format" + }, + "method": { + "label": "HTTP Method", + "tooltip": "The HTTP method to test" + }, + "checkButton": "Check CORS", + "check": "Check CORS", + "checking": "Checking CORS..." + }, + + "results": { + "title": "CORS Policy Analysis", + "corsStatus": "CORS Status", + "preflightStatus": "Preflight Status", + "cacheMaxAge": "Cache Max Age", + "failed": "Failed", + + "status": { + "enabled": "CORS is enabled and allows this origin", + "blocked": "CORS blocked - origin not allowed", + "no_cors": "No CORS headers detected", + "error": "CORS check error" + }, + + "details": { + "title": "CORS Policy Details", + "corsEnabled": "CORS Enabled", + "originAccess": "Origin Access", + "credentialsSupport": "Credentials Support", + "yes": "Yes", + "no": "No", + "allowed": "Allowed", + "blocked": "Blocked" + }, + + "allowedMethods": { + "title": "Allowed Methods", + "none": "No methods specified" + }, + + "allowedHeaders": { + "title": "Allowed Headers", + "none": "No headers specified" + }, + + "corsHeaders": { + "title": "CORS Headers", + "none": "No CORS headers found", + "partial": "CORS enabled but no detailed headers available" + } + }, + + "errors": { + "title": "CORS Check Failed" + }, + + "error": { + "title": "CORS Check Failed", + "network": "Network error occurred", + "timeout": "Request timed out" + }, + + "about": { + "title": "About CORS", + "whatIs": { + "title": "What is CORS?", + "description": "Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers that restricts web pages from making requests to a different domain than the one serving the page." + }, + "preflight": { + "title": "Preflight Requests", + "description": "For certain cross-origin requests, browsers send a preflight OPTIONS request to check if the actual request is allowed." + }, + "headers": { + "title": "CORS Headers", + "allowOrigin": "Access-Control-Allow-Origin: Specifies which origins can access the resource", + "allowMethods": "Access-Control-Allow-Methods: Lists allowed HTTP methods", + "allowHeaders": "Access-Control-Allow-Headers: Specifies allowed request headers", + "allowCredentials": "Access-Control-Allow-Credentials: Indicates if credentials can be included" + } + }, + + "info": { + "title": "Understanding CORS", + "what": { + "heading": "What is CORS?", + "description": "Cross-Origin Resource Sharing (CORS) is a security feature implemented by web browsers that restricts web pages from making requests to a different domain than the one serving the page. This prevents malicious websites from accessing sensitive data." + }, + "how": { + "heading": "How CORS Works", + "description": "When your web application tries to access a resource from a different origin, the browser sends a preflight OPTIONS request to check if the server allows the cross-origin request. The server responds with CORS headers that specify which origins, methods, and headers are allowed." + }, + "common": { + "heading": "Common CORS Issues", + "missing": "Missing CORS headers - server doesn't include Access-Control-Allow-Origin", + "wrong_origin": "Wrong origin - server allows different origins than yours", + "credentials": "Credentials issues - trying to send cookies without proper headers", + "methods": "Method not allowed - the HTTP method you're using isn't permitted" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-headers.json b/src/lib/i18n/translations/en/diagnostics/http-headers.json new file mode 100644 index 00000000..d88446a1 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-headers.json @@ -0,0 +1,112 @@ +{ + "title": "HTTP Headers Analyzer", + "description": "Analyze HTTP response headers, status codes, and response metadata. Supports custom request methods and headers for comprehensive HTTP testing.", + + "examples": { + "title": "Header Examples", + "get": "Basic GET request headers", + "github": "GitHub API headers (HEAD)", + "cloudflare": "Cloudflare response headers", + "status_404": "404 status response", + "redirect": "Redirect chain headers", + "gzip": "Compressed response headers" + }, + + "form": { + "title": "Request Configuration", + "url": { + "label": "URL", + "tooltip": "Enter the URL to analyze", + "placeholder": "https://example.com", + "error": "Invalid URL format", + "required": "URL is required" + }, + "method": { + "label": "Method", + "tooltip": "HTTP method to use" + }, + "custom_headers": { + "label": "Custom Headers (Optional)", + "tooltip": "Custom headers (one per line: 'Name: Value')", + "placeholder": "User-Agent: My Custom Agent\nAuthorization: Bearer token123" + }, + "analyze": "Analyze Headers", + "analyzing": "Analyzing Headers..." + }, + + "results": { + "title": "HTTP Response Analysis", + + "status": { + "label": "HTTP Status", + "response_size": "Response Size", + "total_time": "Total Time" + }, + + "headers": { + "title": "Response Headers" + }, + + "timing": { + "title": "Performance Timing", + "dns": "DNS Resolution:", + "tcp": "TCP Connect:", + "tls": "TLS Handshake:", + "ttfb": "Time to First Byte:", + "help": "* Timing values are approximations when not isolated" + } + }, + + "error": { + "title": "Request Failed" + }, + + "info": { + "title": "About HTTP Headers", + + "response": { + "heading": "Response Headers", + "description": "HTTP headers provide metadata about the response, including content type, caching instructions, security policies, and server information." + }, + + "status_codes": { + "heading": "Status Codes", + "2xx": { + "name": "2xx:", + "description": "Success responses" + }, + "3xx": { + "name": "3xx:", + "description": "Redirection responses" + }, + "4xx": { + "name": "4xx:", + "description": "Client error responses" + }, + "5xx": { + "name": "5xx:", + "description": "Server error responses" + } + }, + + "common": { + "heading": "Common Headers", + "content_type": { + "name": "Content-Type:", + "description": "MIME type of content" + }, + "cache_control": { + "name": "Cache-Control:", + "description": "Caching directives" + }, + "set_cookie": { + "name": "Set-Cookie:", + "description": "Cookie instructions" + }, + "location": { + "name": "Location:", + "description": "Redirect target URL" + } + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-perf.json b/src/lib/i18n/translations/en/diagnostics/http-perf.json new file mode 100644 index 00000000..60c4c0b9 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-perf.json @@ -0,0 +1,151 @@ +{ + "title": "HTTP Performance Analyzer", + "description": "Measure HTTP request performance including DNS resolution, TCP connection, TLS handshake, and response times. Get detailed timing breakdowns and performance insights.", + + "examples": { + "title": "Performance Examples", + "google": "Google homepage performance", + "delay": "Delayed response (2s)", + "github": "GitHub API response time", + "cloudflare": "Cloudflare CDN performance" + }, + + "form": { + "title": "Performance Test Configuration", + "url": { + "label": "URL", + "tooltip": "Enter the URL to measure performance for", + "placeholder": "https://example.com", + "error": "Invalid URL format", + "required": "URL is required" + }, + "method": { + "label": "Method", + "tooltip": "HTTP method to use for the request" + }, + "measure": "Measure Performance", + "measuring": "Measuring Performance..." + }, + + "results": { + "title": "Performance Analysis", + "copy": "Copy Results", + "copied": "Copied!", + + "grade": { + "label": "Grade {grade}", + "total": "{total}ms Total" + }, + + "overview": { + "http_status": "HTTP Status", + "response_size": "Response Size", + "throughput": "Throughput", + "unknown": "Unknown" + }, + + "timing": { + "title": "Performance Timing Breakdown", + "dns": "DNS Resolution", + "tcp": "TCP Connect", + "tls": "TLS Handshake", + "ttfb": "Time to First Byte", + "total": "Total Time" + }, + + "features": { + "title": "Connection Features", + "https": { + "name": "HTTPS", + "secure": "Secure connection", + "unsecure": "Unencrypted connection" + }, + "compression": { + "name": "Compression", + "enabled": "Response is compressed", + "disabled": "No compression detected" + }, + "http_version": "HTTP Version", + "connection_reuse": "Connection Reuse" + } + }, + + "errors": { + "title": "Performance Test Failed" + }, + + "error": { + "title": "Performance Test Failed" + }, + + "about": { + "title": "About HTTP Performance", + "timingComponents": { + "title": "Timing Components", + "dns": "DNS Resolution: Time to resolve domain name to IP address", + "tcp": "TCP Connect: Time to establish TCP connection", + "tls": "TLS Handshake: Time for SSL/TLS negotiation (HTTPS only)", + "ttfb": "Time to First Byte: Server processing and response start time" + }, + "grades": { + "title": "Performance Grades", + "excellent": "A (≀200ms): Excellent performance", + "good": "B (≀500ms): Good performance", + "acceptable": "C (≀1000ms): Acceptable performance", + "poor": "D/F (>1000ms): Poor performance" + }, + "optimization": { + "title": "Optimization Tips", + "description": "Use CDN for faster response times, enable compression, implement HTTP/2, optimize DNS resolution, and consider connection keep-alive for multiple requests." + } + }, + + "info": { + "title": "About HTTP Performance", + + "timing": { + "heading": "Timing Components", + "dns": { + "name": "DNS:", + "description": "Domain name resolution time" + }, + "tcp": { + "name": "TCP:", + "description": "TCP connection establishment" + }, + "tls": { + "name": "TLS:", + "description": "SSL/TLS handshake (HTTPS only)" + }, + "ttfb": { + "name": "TTFB:", + "description": "Server processing and response start" + } + }, + + "grades": { + "heading": "Performance Grades", + "a": { + "label": "A (≀200ms):", + "description": "Excellent performance" + }, + "b": { + "label": "B (≀500ms):", + "description": "Good performance" + }, + "c": { + "label": "C (≀1000ms):", + "description": "Acceptable performance" + }, + "d": { + "label": "D/F (>1000ms):", + "description": "Poor performance" + } + }, + + "tips": { + "heading": "Optimization Tips", + "content": "Use CDN for faster response times, enable compression, implement HTTP/2, optimize DNS resolution, and consider connection keep-alive for multiple requests." + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-redirect-trace.json b/src/lib/i18n/translations/en/diagnostics/http-redirect-trace.json new file mode 100644 index 00000000..2132aa6c --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-redirect-trace.json @@ -0,0 +1,88 @@ +{ + "title": "HTTP Redirect Tracer", + "description": "Trace HTTP redirects and analyze redirect chains for a URL", + + "form": { + "title": "Redirect Trace Configuration", + "url": { + "label": "URL", + "placeholder": "https://example.com", + "tooltip": "Enter the URL to trace redirects for", + "required": "URL is required", + "invalidFormat": "Invalid URL format" + }, + "maxRedirects": { + "label": "Max Redirects", + "tooltip": "Maximum number of redirects to follow" + }, + "trace": "Trace Redirects", + "tracing": "Tracing...", + "error": "Please enter a valid URL" + }, + + "examples": { + "title": "Redirect Examples", + "types": { + "urlShortener": "URL shortener redirect", + "absolute": "Absolute redirects", + "external": "Redirect to external site", + "relative": "Relative path redirects" + } + }, + + "results": { + "title": "Redirect Chain Analysis", + "copyChain": "Copy Chain", + "copied": "Copied!", + + "stats": { + "totalRedirects": "Total Redirects", + "finalStatus": "Final Status", + "totalTime": "Total Time" + }, + + "chain": { + "title": "Redirect Chain", + "finalDestination": "Final Destination", + "noRedirects": "No redirects found - URL resolved directly", + "failed": "Redirect Trace Failed" + }, + + "security": { + "hstsPresent": "HSTS header present", + "httpsUpgrade": "HTTP to HTTPS upgrade" + }, + + "redirect_count": "{count} redirects", + "status_code": "Status Code", + "location": "Location", + "redirect_time": "Redirect Time", + "total_time": "Total Time", + "redirect_loop": "Redirect Loop Detected", + "too_many_redirects": "Too Many Redirects" + }, + + "education": { + "title": "About HTTP Redirects", + + "redirectTypes": { + "title": "Redirect Types" + }, + + "security": { + "title": "Security Considerations", + "hsts": "HSTS:", + "hstsDescription": "Prevents downgrade attacks", + "httpsUpgrade": "HTTP β†’ HTTPS:", + "httpsDescription": "Security upgrades", + "openRedirects": "Open Redirects:", + "openDescription": "Potential security risk", + "loops": "Redirect Loops:", + "loopsDescription": "Infinite chains" + }, + + "performance": { + "title": "Performance Impact" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/http-security.json b/src/lib/i18n/translations/en/diagnostics/http-security.json new file mode 100644 index 00000000..33d41c3b --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/http-security.json @@ -0,0 +1,110 @@ +{ + "title": "HTTP Security Headers Analyzer", + "description": "Analyze and evaluate security headers to identify potential vulnerabilities and security improvements. Check for HSTS, CSP, XSS protection, and other essential security headers.", + + "examples": { + "title": "Security Examples", + "github": "GitHub security headers", + "cloudflare": "Cloudflare security setup", + "hsts": "Example with HSTS", + "basic": "Basic site (minimal headers)" + }, + + "form": { + "title": "Security Analysis", + "url": { + "label": "URL", + "tooltip": "Enter the URL to analyze security headers for", + "placeholder": "https://example.com", + "error": "Invalid URL format", + "required": "URL is required" + }, + "analyze": "Analyze Security Headers", + "analyzing": "Analyzing Security..." + }, + + "results": { + "title": "Security Headers Analysis", + "copy": "Copy Analysis", + "copied": "Copied!", + + "score": { + "grade": "Grade {grade}", + "label": "Security Score: {score}%", + "headers_present": "Headers Present", + "headers_missing": "Headers Missing" + }, + + "analysis": { + "title": "Security Header Analysis" + }, + + "headers": { + "title": "Security Headers Found" + }, + + "none": { + "title": "No security headers found", + "message": "This site may be vulnerable to various attacks" + }, + + "status": { + "present": "present", + "weak": "weak", + "missing": "missing" + } + }, + + "error": { + "title": "Security Analysis Failed" + }, + + "info": { + "title": "About Security Headers", + + "critical": { + "heading": "Critical Headers", + "hsts": { + "name": "Strict-Transport-Security:", + "description": "Forces HTTPS connections" + }, + "csp": { + "name": "Content-Security-Policy:", + "description": "Prevents XSS and injection attacks" + }, + "xfo": { + "name": "X-Frame-Options:", + "description": "Prevents clickjacking attacks" + }, + "xcto": { + "name": "X-Content-Type-Options:", + "description": "Prevents MIME sniffing" + } + }, + + "additional": { + "heading": "Additional Protection", + "referrer": { + "name": "Referrer-Policy:", + "description": "Controls referrer information" + }, + "permissions": { + "name": "Permissions-Policy:", + "description": "Controls browser features" + }, + "cors": { + "name": "Cross-Origin-*:", + "description": "CORS and isolation policies" + }, + "xss": { + "name": "X-XSS-Protection:", + "description": "Legacy XSS protection" + } + }, + + "tips": { + "heading": "Implementation Tips", + "content": "Start with basic headers (HSTS, CSP, X-Frame-Options) and gradually add more. Test thoroughly as some headers may break functionality if misconfigured." + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/network-asn-geo-lookup.json b/src/lib/i18n/translations/en/diagnostics/network-asn-geo-lookup.json new file mode 100644 index 00000000..14042af0 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/network-asn-geo-lookup.json @@ -0,0 +1,95 @@ +{ + "title": "ASN & Geolocation Lookup", + "subtitle": "Look up IP address geolocation, ISP, and autonomous system information", + + "examples": { + "title": "Example Lookups", + "items": { + "googleDNS": { + "ip": "8.8.8.8", + "description": "Google Public DNS", + "tooltip": "Look up 8.8.8.8" + }, + "m247": { + "ip": "2.58.47.0", + "description": "M247 Proton", + "tooltip": "Look up 2.58.47.0" + }, + "cloudflareDNS": { + "ip": "1.1.1.1", + "description": "Cloudflare DNS", + "tooltip": "Look up 1.1.1.1" + }, + "github": { + "ip": "140.82.121.4", + "description": "GitHub", + "tooltip": "Look up 140.82.121.4" + }, + "fastly": { + "ip": "151.101.1.140", + "description": "Fastly CDN", + "tooltip": "Look up 151.101.1.140" + }, + "cloudflareIPv6": { + "ip": "2606:4700:4700::1111", + "description": "Cloudflare IPv6", + "tooltip": "Look up 2606:4700:4700::1111" + } + } + }, + + "form": { + "title": "IP Lookup", + "ipLabel": "IP Address", + "ipTooltip": "Enter an IPv4 or IPv6 address", + "ipPlaceholder": "8.8.8.8 or 2001:4860:4860::8888", + "lookingUp": "Looking up...", + "lookupButton": "Lookup" + }, + + "results": { + "title": "Results for {ip}", + "copyButton": "Copy Results", + "copied": "Copied!", + + "networkInfo": { + "title": "Network Information", + "asn": "ASN", + "organization": "Organization", + "isp": "ISP" + }, + + "geoLocation": { + "title": "Geographic Location", + "country": "Country", + "region": "Region", + "city": "City", + "timezone": "Timezone" + }, + + "coordinates": { + "title": "Coordinates", + "latitude": "Latitude:", + "longitude": "Longitude:", + "viewMap": "View full map" + }, + + "connectionType": { + "title": "Connection Type", + "mobile": "Mobile Network", + "proxy": "Proxy/VPN", + "hosting": "Hosting/Datacenter" + } + }, + + "error": { + "title": "Lookup Failed" + }, + + "info": { + "title": "About ASN & Geolocation", + "quickTips": { + "title": "Quick Tips" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/network-bgp-route-lookup.json b/src/lib/i18n/translations/en/diagnostics/network-bgp-route-lookup.json new file mode 100644 index 00000000..91d51305 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/network-bgp-route-lookup.json @@ -0,0 +1,26 @@ +{ + "title": "BGP Route Lookup", + "description": "Look up BGP routing information for an IP address or prefix", + + "form": { + "title": "BGP Route Lookup", + "ip": { + "label": "IP Address or Prefix", + "placeholder": "8.8.8.8 or 8.8.8.0/24" + }, + "lookup": "Lookup BGP Route", + "looking_up": "Looking up...", + "error": "Please enter a valid IP address or prefix" + }, + + "results": { + "title": "BGP Route Information", + "prefix": "Prefix", + "asn": "ASN", + "as_path": "AS Path", + "origin": "Origin", + "next_hop": "Next Hop", + "community": "Community", + "local_pref": "Local Preference" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/network-http-ping.json b/src/lib/i18n/translations/en/diagnostics/network-http-ping.json new file mode 100644 index 00000000..99b67e6f --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/network-http-ping.json @@ -0,0 +1,30 @@ +{ + "title": "HTTP Ping", + "description": "Measure HTTP response time and availability for a URL", + + "form": { + "title": "HTTP Ping Configuration", + "url": { + "label": "URL", + "placeholder": "https://example.com" + }, + "count": { + "label": "Ping Count", + "placeholder": "5" + }, + "ping": "Start HTTP Ping", + "pinging": "Pinging...", + "error": "Please enter a valid URL" + }, + + "results": { + "title": "HTTP Ping Results", + "status": "Status", + "response_time": "Response Time", + "avg_time": "Average Time", + "min_time": "Min Time", + "max_time": "Max Time", + "success_rate": "Success Rate", + "failed": "Failed" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/network-ipv6-connectivity-checker.json b/src/lib/i18n/translations/en/diagnostics/network-ipv6-connectivity-checker.json new file mode 100644 index 00000000..36955079 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/network-ipv6-connectivity-checker.json @@ -0,0 +1,27 @@ +{ + "title": "IPv6 Connectivity Checker", + "description": "Test IPv6 connectivity to a host and verify dual-stack support", + + "form": { + "title": "IPv6 Connectivity Test", + "hostname": { + "label": "Hostname", + "placeholder": "example.com" + }, + "check": "Check IPv6 Connectivity", + "checking": "Checking...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "IPv6 Connectivity Results", + "ipv6_supported": "IPv6 Supported", + "ipv6_not_supported": "IPv6 Not Supported", + "ipv4_supported": "IPv4 Supported", + "dual_stack": "Dual Stack", + "ipv6_addresses": "IPv6 Addresses", + "ipv4_addresses": "IPv4 Addresses", + "reachable": "Reachable", + "not_reachable": "Not Reachable" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/network-tcp-port-check.json b/src/lib/i18n/translations/en/diagnostics/network-tcp-port-check.json new file mode 100644 index 00000000..22a02dbd --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/network-tcp-port-check.json @@ -0,0 +1,149 @@ +{ + "title": "TCP Port Checker", + "subtitle": "Test TCP connectivity to one or more host:port combinations. Attempts direct TCP connections to check if ports are open and measures connection latency.", + + "examples": { + "title": "Port Check Examples", + "items": { + "https": { + "targets": "google.com:443\ngithub.com:443\nstackoverflow.com:443", + "description": "Common HTTPS ports", + "tooltip": "Test ports: google.com:443, github.com:443, stackoverflow.com:443" + }, + "smtp": { + "targets": "smtp.gmail.com:587\nsmtp.gmail.com:465\nsmtp.gmail.com:25", + "description": "Gmail SMTP ports", + "tooltip": "Test ports: smtp.gmail.com:587, smtp.gmail.com:465, smtp.gmail.com:25" + }, + "dns": { + "targets": "dns.google:53\n1.1.1.1:53\n8.8.8.8:53", + "description": "DNS server ports", + "tooltip": "Test ports: dns.google:53, 1.1.1.1:53, 8.8.8.8:53" + }, + "httpVsHttps": { + "targets": "reddit.com:80\nreddit.com:443\napi.reddit.com:443", + "description": "HTTP vs HTTPS ports", + "tooltip": "Test ports: reddit.com:80, reddit.com:443, api.reddit.com:443" + }, + "localDev": { + "targets": "localhost:22\nlocalhost:80\nlocalhost:443\nlocalhost:3306\nlocalhost:5432", + "description": "Local development ports", + "tooltip": "Test ports: localhost:22, localhost:80, localhost:443, localhost:3306, localhost:5432" + }, + "microsoft": { + "targets": "microsoft.com:443\noffice.com:443\noutlook.com:443", + "description": "Microsoft services", + "tooltip": "Test ports: microsoft.com:443, office.com:443, outlook.com:443" + } + } + }, + + "form": { + "title": "Port Check Configuration", + "targetsLabel": "Target Hosts & Ports", + "targetsTooltip": "Enter host:port combinations, one per line (max 50)", + "targetsPlaceholder": "google.com:443\ngithub.com:22\nexample.com:80", + "targetCount": "{count}/50 targets", + "invalidFormat": "Use format: hostname:port (one per line)", + "commonPortsTitle": "Common Ports", + "timeoutLabel": "Timeout (ms)", + "timeoutTooltip": "Connection timeout in milliseconds", + "checking": "Checking Ports...", + "checkButton": "Check Ports" + }, + + "commonPorts": { + "ssh": { + "port": "22", + "service": "SSH", + "description": "Secure Shell" + }, + "http": { + "port": "80", + "service": "HTTP", + "description": "Web traffic" + }, + "https": { + "port": "443", + "service": "HTTPS", + "description": "Secure web traffic" + }, + "smtp": { + "port": "25", + "service": "SMTP", + "description": "Email sending" + }, + "smtpSubmission": { + "port": "587", + "service": "SMTP", + "description": "Email submission" + }, + "imaps": { + "port": "993", + "service": "IMAPS", + "description": "Secure IMAP" + }, + "pop3s": { + "port": "995", + "service": "POP3S", + "description": "Secure POP3" + }, + "dns": { + "port": "53", + "service": "DNS", + "description": "Domain resolution" + } + }, + + "results": { + "title": "Port Check Results", + "copyButton": "Copy Results", + "copied": "Copied!", + "summaryOpenPorts": "{count} Open", + "summaryClosedPorts": "{count} Closed", + "summaryAvgLatency": "{latency}ms", + "openPortsDescription": "Ports accepting connections", + "closedPortsDescription": "Ports not responding", + "avgLatencyDescription": "Average latency", + "portStatusTitle": "Port Status ({count} targets)", + "statusOpen": "Open ({latency}ms)", + "statusClosed": "Closed" + }, + + "error": { + "title": "Port Check Failed" + }, + + "info": { + "title": "Understanding TCP Port Connectivity", + "portStates": { + "title": "Port States", + "open": "Open:", + "openDesc": "Port accepts connections and responds", + "closed": "Closed:", + "closedDesc": "Port actively refuses connections", + "filtered": "Filtered:", + "filteredDesc": "Port blocked by firewall (appears as timeout)", + "timeout": "Timeout:", + "timeoutDesc": "No response within timeout period" + }, + "commonPorts": { + "title": "Common Ports", + "ssh": "SSH (22):", + "sshDesc": "Secure remote access", + "http": "HTTP (80):", + "httpDesc": "Web traffic", + "https": "HTTPS (443):", + "httpsDesc": "Secure web traffic", + "smtp": "SMTP (25/587):", + "smtpDesc": "Email sending" + }, + "troubleshooting": { + "title": "Troubleshooting Tips", + "tip1": "Timeouts often indicate firewall blocking", + "tip2": "Connection refused means service is not running", + "tip3": "Check both client and server firewalls", + "tip4": "Verify service is listening on expected port" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/rdap-asn.json b/src/lib/i18n/translations/en/diagnostics/rdap-asn.json new file mode 100644 index 00000000..10f7beaf --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/rdap-asn.json @@ -0,0 +1,119 @@ +{ + "title": "ASN RDAP Lookup", + "subtitle": "Query Autonomous System Number allocation and registration data using RDAP through Regional Internet Registries. ASNs identify networks on the global Internet routing table and are essential for BGP routing operations.", + + "examples": { + "title": "Common ASN Examples", + "items": { + "google": { + "asn": "AS15169", + "description": "Google LLC - Major cloud provider", + "tooltip": "Perform RDAP lookup for AS15169 (Google LLC - Major cloud provider)" + }, + "cloudflare": { + "asn": "AS13335", + "description": "Cloudflare - CDN and security services", + "tooltip": "Perform RDAP lookup for AS13335 (Cloudflare - CDN and security services)" + }, + "amazon": { + "asn": "AS16509", + "description": "Amazon.com - AWS cloud infrastructure", + "tooltip": "Perform RDAP lookup for AS16509 (Amazon.com - AWS cloud infrastructure)" + }, + "microsoft": { + "asn": "AS8075", + "description": "Microsoft Corporation - Azure cloud", + "tooltip": "Perform RDAP lookup for AS8075 (Microsoft Corporation - Azure cloud)" + }, + "meta": { + "asn": "AS32934", + "description": "Meta Platforms - Facebook services", + "tooltip": "Perform RDAP lookup for AS32934 (Meta Platforms - Facebook services)" + }, + "googleCloud": { + "asn": "AS396982", + "description": "Google Cloud Platform - Additional ranges", + "tooltip": "Perform RDAP lookup for AS396982 (Google Cloud Platform - Additional ranges)" + } + } + }, + + "form": { + "title": "RDAP Lookup Configuration", + "asnLabel": "Autonomous System Number", + "asnPlaceholder": "AS15169 or 15169", + "asnTooltip": "Enter an Autonomous System Number to query allocation data via RDAP", + "asnHint": "Supports both formats: AS15169 or 15169 (AS prefix is optional)", + "lookupButton": "Lookup ASN", + "performing": "Performing RDAP Lookup..." + }, + + "results": { + "title": "RDAP Data for {asn}", + "copy": "Copy Raw JSON", + "copied": "Copied!", + "asn": "ASN", + "asnTooltip": "The ASN that was queried", + "rdapService": "RDAP Service", + "rdapServiceTooltip": "RDAP service used for the query", + "notAvailable": "Not available", + "asnInfo": { + "title": "ASN Information", + "organizationName": "Organization Name:", + "type": "Type:", + "country": "Country:", + "registry": "Registry:" + }, + "allocation": { + "title": "Allocation Details", + "status": "Status:", + "allocationDate": "Allocation Date:", + "lastChanged": "Last Changed:" + }, + "contacts": { + "title": "Contact Information", + "registrant": "Registrant", + "administrative": "Administrative", + "technical": "Technical", + "abuse": "Abuse", + "contact": "Contact", + "handle": "Handle: {handle}" + } + }, + + "error": { + "title": "RDAP Lookup Failed", + "lookupFailed": "ASN RDAP lookup failed: {status}", + "troubleshooting": { + "title": "Troubleshooting Tips:", + "validFormat": "Ensure the ASN format is valid (e.g., AS15169 or just 15169)", + "asnExists": "Check if the ASN exists and is currently allocated", + "privateASN": "Private ASNs (64512-65534, 4200000000-4294967294) may not have public RDAP data", + "rateLimiting": "Some RIRs may have rate limiting or access restrictions", + "tryAgain": "Try again in a few moments if the service is temporarily unavailable" + } + }, + + "educational": { + "title": "About ASN RDAP Lookups", + "whatIsASN": { + "title": "What is an ASN?", + "description": "Autonomous System Numbers identify networks on the global Internet routing table and are essential for BGP routing operations. Each ASN represents a collection of IP address blocks under unified administrative control." + }, + "asnRanges": { + "title": "ASN Ranges by Registry", + "arin": "ARIN: AS1 - AS23551, AS393216 - AS394239", + "ripe": "RIPE NCC: AS24576 - AS25599, AS34816 - AS35839", + "apnic": "APNIC: AS23552 - AS24575, AS37888 - AS38911", + "lacnic": "LACNIC: AS26592 - AS27647, AS61440 - AS61951", + "afrinic": "AFRINIC: AS36864 - AS37887, AS327680 - AS328703" + }, + "whatYouGet": { + "title": "What You'll Get", + "organization": "Organization name and type", + "country": "Country of registration", + "allocation": "Allocation date and status", + "contacts": "Contact information (admin, technical, abuse)" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/rdap-domain.json b/src/lib/i18n/translations/en/diagnostics/rdap-domain.json new file mode 100644 index 00000000..70c26ed3 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/rdap-domain.json @@ -0,0 +1,119 @@ +{ + "title": "Domain RDAP Lookup", + "subtitle": "Query domain registration data using RDAP (Registration Data Access Protocol). RDAP is the modern successor to WHOIS, providing structured JSON responses through IANA bootstrap registry routing.", + + "examples": { + "title": "Common Domain Examples", + "items": { + "example": { + "domain": "example.com", + "description": "Example domain for testing", + "tooltip": "Perform RDAP lookup for example.com (Example domain for testing)" + }, + "google": { + "domain": "google.com", + "description": "Popular domain with comprehensive records", + "tooltip": "Perform RDAP lookup for google.com (Popular domain with comprehensive records)" + }, + "github": { + "domain": "github.com", + "description": "Tech company domain", + "tooltip": "Perform RDAP lookup for github.com (Tech company domain)" + }, + "stackoverflow": { + "domain": "stackoverflow.com", + "description": "Community platform domain", + "tooltip": "Perform RDAP lookup for stackoverflow.com (Community platform domain)" + }, + "cloudflare": { + "domain": "cloudflare.com", + "description": "CDN provider domain", + "tooltip": "Perform RDAP lookup for cloudflare.com (CDN provider domain)" + }, + "iana": { + "domain": "iana.org", + "description": "Internet registry domain", + "tooltip": "Perform RDAP lookup for iana.org (Internet registry domain)" + } + } + }, + + "form": { + "title": "RDAP Lookup Configuration", + "domainLabel": "Domain Name", + "domainPlaceholder": "example.com", + "domainTooltip": "Enter a domain name to query registration data via RDAP", + "lookupButton": "Lookup Domain", + "performing": "Performing RDAP Lookup..." + }, + + "results": { + "title": "RDAP Data for {domain}", + "copy": "Copy Raw JSON", + "copied": "Copied!", + "domain": "Domain", + "domainTooltip": "The domain name that was queried", + "rdapService": "RDAP Service", + "rdapServiceTooltip": "RDAP service used for the query", + "notAvailable": "Not available", + "domainInfo": { + "title": "Domain Information", + "domainName": "Domain Name:", + "status": "Status:", + "registrar": "Registrar:" + }, + "dates": { + "title": "Important Dates", + "registration": "Registration Date:", + "lastUpdated": "Last Updated:", + "expiration": "Expiration Date:", + "expiresSoon": "Expires Soon!" + }, + "nameservers": { + "title": "Nameservers ({count})" + }, + "contacts": { + "title": "Contact Information", + "registrant": "Registrant", + "administrative": "Administrative", + "technical": "Technical", + "contact": "Contact", + "handle": "Handle: {handle}" + } + }, + + "error": { + "title": "RDAP Lookup Failed", + "unknownError": "Unknown error occurred", + "lookupFailed": "Domain RDAP lookup failed: {status}", + "troubleshooting": { + "title": "Troubleshooting Tips:", + "validDomain": "Ensure the domain name is valid and properly formatted", + "domainExists": "Check if the domain actually exists and is registered", + "rateLimiting": "Some registries may have rate limiting or access restrictions", + "tryAgain": "Try again in a few moments if the service is temporarily unavailable" + } + }, + + "educational": { + "title": "About RDAP Domain Lookups", + "whatIsRDAP": { + "title": "What is RDAP?", + "description": "RDAP (Registration Data Access Protocol) is the modern successor to WHOIS, providing structured JSON responses for domain registration information through IANA bootstrap registry routing." + }, + "whatYouGet": { + "title": "What You'll Get", + "statusDates": "Registration status and dates", + "nameservers": "Nameserver information", + "registrar": "Registrar details", + "contacts": "Contact information (if available)" + }, + "rdapVsWhois": { + "title": "RDAP vs WHOIS", + "structuredJSON": "Structured JSON instead of free text", + "unicodeSupport": "Unicode support for internationalized domains", + "rateLimiting": "Built-in rate limiting and privacy controls", + "restfulAPI": "RESTful API with standard HTTP methods" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/rdap-ip.json b/src/lib/i18n/translations/en/diagnostics/rdap-ip.json new file mode 100644 index 00000000..dcf4f3c1 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/rdap-ip.json @@ -0,0 +1,121 @@ +{ + "title": "IP Address RDAP Lookup", + "subtitle": "Look up IP address allocation and registration data using RDAP through Regional Internet Registry (RIR) services. Automatically routes queries to the appropriate RIR based on IP address prefix.", + + "examples": { + "title": "Common IP Examples", + "items": { + "googleDNS": { + "ip": "8.8.8.8", + "description": "Google DNS - Public DNS service", + "tooltip": "Perform RDAP lookup for 8.8.8.8 (Google DNS - Public DNS service)" + }, + "cloudflareDNS": { + "ip": "1.1.1.1", + "description": "Cloudflare DNS - Fast public resolver", + "tooltip": "Perform RDAP lookup for 1.1.1.1 (Cloudflare DNS - Fast public resolver)" + }, + "openDNS": { + "ip": "208.67.222.222", + "description": "OpenDNS - Cisco public DNS", + "tooltip": "Perform RDAP lookup for 208.67.222.222 (OpenDNS - Cisco public DNS)" + }, + "rfc5737": { + "ip": "192.0.2.1", + "description": "RFC 5737 - Documentation IP range", + "tooltip": "Perform RDAP lookup for 192.0.2.1 (RFC 5737 - Documentation IP range)" + }, + "googleIPv6": { + "ip": "2001:4860:4860::8888", + "description": "Google IPv6 DNS", + "tooltip": "Perform RDAP lookup for 2001:4860:4860::8888 (Google IPv6 DNS)" + }, + "cloudflareIPv6": { + "ip": "2606:4700:4700::1111", + "description": "Cloudflare IPv6 DNS", + "tooltip": "Perform RDAP lookup for 2606:4700:4700::1111 (Cloudflare IPv6 DNS)" + } + } + }, + + "form": { + "title": "RDAP Lookup Configuration", + "ipLabel": "IP Address", + "ipPlaceholder": "8.8.8.8 or 2001:4860:4860::8888", + "ipTooltip": "Enter an IPv4 or IPv6 address to query allocation data via RDAP", + "ipHint": "Supports both IPv4 (e.g., 8.8.8.8) and IPv6 (e.g., 2001:4860:4860::8888) addresses", + "lookupButton": "Lookup IP Address", + "performing": "Performing RDAP Lookup..." + }, + + "results": { + "title": "RDAP Data for {ip}", + "copy": "Copy Raw JSON", + "copied": "Copied!", + "ipAddress": "IP Address", + "ipAddressTooltip": "The IP address that was queried", + "rdapService": "RDAP Service", + "rdapServiceTooltip": "RDAP service used for the query", + "notAvailable": "Not available", + "networkInfo": { + "title": "Network Information", + "networkBlock": "Network Block:", + "networkName": "Network Name:", + "type": "Type:", + "country": "Country:", + "registry": "Registry:" + }, + "allocation": { + "title": "Allocation Details", + "status": "Status:", + "allocationDate": "Allocation Date:", + "lastChanged": "Last Changed:" + }, + "contacts": { + "title": "Contact Information", + "registrant": "Registrant", + "administrative": "Administrative", + "technical": "Technical", + "abuse": "Abuse", + "contact": "Contact", + "handle": "Handle: {handle}" + } + }, + + "error": { + "title": "RDAP Lookup Failed", + "unknownError": "Unknown error occurred", + "lookupFailed": "IP RDAP lookup failed: {status}", + "troubleshooting": { + "title": "Troubleshooting Tips:", + "validIP": "Ensure the IP address is valid and properly formatted", + "privateIP": "Private IP addresses (RFC 1918) may not have RDAP data", + "rateLimiting": "Some RIRs may have rate limiting or access restrictions", + "specialUse": "Reserved or special-use addresses may not be publicly queryable", + "tryAgain": "Try again in a few moments if the service is temporarily unavailable" + } + }, + + "educational": { + "title": "About IP Address RDAP Lookups", + "howItWorks": { + "title": "How it Works", + "description": "IP RDAP provides detailed allocation information from Regional Internet Registries (RIRs). The tool automatically routes queries to the appropriate RIR using IANA bootstrap registries." + }, + "rirs": { + "title": "Regional Internet Registries", + "arin": "ARIN: North America, parts of Caribbean", + "ripe": "RIPE NCC: Europe, Central Asia, Middle East", + "apnic": "APNIC: Asia Pacific region", + "lacnic": "LACNIC: Latin America, parts of Caribbean", + "afrinic": "AFRINIC: Africa" + }, + "whatYouGet": { + "title": "What You'll Get", + "networkBlock": "Network block and CIDR prefix", + "allocationType": "Allocation type and country", + "organization": "Organization responsible for the block", + "contacts": "Contact information (registrant, admin, technical, abuse)" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-alpn.json b/src/lib/i18n/translations/en/diagnostics/tls-alpn.json new file mode 100644 index 00000000..937730cf --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-alpn.json @@ -0,0 +1,25 @@ +{ + "title": "ALPN Protocol Checker", + "description": "Check Application-Layer Protocol Negotiation (ALPN) support for a server", + + "form": { + "title": "ALPN Check", + "hostname": { + "label": "Hostname", + "placeholder": "example.com" + }, + "port": { + "label": "Port", + "placeholder": "443" + }, + "check": "Check ALPN", + "checking": "Checking...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "ALPN Results", + "supported_protocols": "Supported Protocols", + "no_alpn": "ALPN not supported" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-banner.json b/src/lib/i18n/translations/en/diagnostics/tls-banner.json new file mode 100644 index 00000000..66433348 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-banner.json @@ -0,0 +1,143 @@ +{ + "title": "Service Banner Grabber", + "subtitle": "Retrieve service banners from SSH, SMTP, HTTP, FTP, and other network services", + + "services": { + "custom": "Custom port", + "ssh": "SSH Server", + "smtp": "SMTP Mail Server", + "whois": "WHOIS Service", + "http": "HTTP Web Server", + "https": "HTTPS Web Server", + "ftp": "FTP Server", + "telnet": "Telnet Server", + "pop3": "POP3 Mail Server", + "imap": "IMAP Mail Server", + "smtps": "SMTP over TLS", + "submission": "Mail Submission", + "imaps": "IMAP over TLS", + "pop3s": "POP3 over TLS", + "mysql": "MySQL Database", + "postgresql": "PostgreSQL Database", + "redis": "Redis Database", + "mongodb": "MongoDB Database", + "rdp": "Remote Desktop", + "vnc": "VNC Remote Desktop" + }, + + "examples": { + "title": "Quick Examples", + "items": { + "nmapSSH": { + "host": "scanme.nmap.org", + "port": 22, + "description": "Nmap SSH Test", + "tooltip": "Grab banner from scanme.nmap.org:22" + }, + "rebexFTP": { + "host": "test.rebex.net", + "port": 21, + "description": "Rebex FTP Test", + "tooltip": "Grab banner from test.rebex.net:21" + }, + "exampleHTTP": { + "host": "example.com", + "port": 80, + "description": "Example.com HTTP", + "tooltip": "Grab banner from example.com:80" + }, + "googleHTTP": { + "host": "www.google.com", + "port": 80, + "description": "Google HTTP", + "tooltip": "Grab banner from www.google.com:80" + }, + "googleMX": { + "host": "aspmx.l.google.com", + "port": 25, + "description": "Google MX Server", + "tooltip": "Grab banner from aspmx.l.google.com:25" + }, + "ianaWHOIS": { + "host": "whois.iana.org", + "port": 43, + "description": "IANA WHOIS", + "tooltip": "Grab banner from whois.iana.org:43" + }, + "freebsdFTP": { + "host": "ftp.freebsd.org", + "port": 21, + "description": "FreeBSD FTP", + "tooltip": "Grab banner from ftp.freebsd.org:21" + }, + "httpbinAPI": { + "host": "httpbin.org", + "port": 80, + "description": "HTTPBin API", + "tooltip": "Grab banner from httpbin.org:80" + }, + "verisignWHOIS": { + "host": "whois.verisign-grs.com", + "port": 43, + "description": "Verisign WHOIS", + "tooltip": "Grab banner from whois.verisign-grs.com:43" + } + } + }, + + "form": { + "title": "Target Service", + "serviceTypeLabel": "Service Type", + "hostLabel": "Host / IP Address", + "hostPlaceholder": "example.com or 192.168.1.1", + "portLabel": "Port", + "portPlaceholder": "1-65535", + "connecting": "Connecting...", + "grabButton": "Grab Banner" + }, + + "loading": { + "title": "Grabbing Banner", + "message": "Connecting to {host}:{port}..." + }, + + "error": { + "title": "Connection Failed", + "invalidInput": "Please enter a valid host and port (1-65535)", + "hostNotFound": "Host not found. Please check the hostname and try again.", + "connectionRefused": "Connection refused on port {port}. The service may be down or port closed.", + "timeout": "Connection timed out. The host may be unreachable or port filtered.", + "failed": "Failed to grab banner", + "unexpected": "An unexpected error occurred" + }, + + "results": { + "title": "Banner Information", + "connectionDetails": { + "title": "Connection Details", + "host": "Host:", + "port": "Port:", + "protocol": "Protocol:", + "protocolUnknown": "Unknown", + "responseTime": "Response Time:" + }, + "serviceBanner": { + "title": "Service Banner", + "noBannerReceived": "No banner received from service", + "noBannerHint": "The service may not send a banner or requires specific protocol handshake" + }, + "serviceAnalysis": { + "title": "Service Analysis", + "software": "Software", + "version": "Version", + "os": "Operating System", + "securityNotes": "Security Notes" + }, + "tlsInformation": { + "title": "TLS Information", + "protocol": "Protocol:", + "cipher": "Cipher:", + "certificateCN": "Certificate CN:" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-certificate.json b/src/lib/i18n/translations/en/diagnostics/tls-certificate.json new file mode 100644 index 00000000..29ecf3c4 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-certificate.json @@ -0,0 +1,125 @@ +{ + "title": "TLS Certificate Analyzer", + "description": "Analyze TLS certificates, view certificate chains, check expiration dates, and examine Subject Alternative Names (SANs). Supports custom SNI servername for multi-domain certificates.", + + "examples": [ + { + "host": "google.com:443", + "description": "Google TLS certificate" + }, + { + "host": "github.com:443", + "description": "GitHub certificate chain" + }, + { + "host": "cloudflare.com:443", + "description": "Cloudflare certificate" + }, + { + "host": "wikipedia.org:443", + "description": "Wikipedia certificate" + }, + { + "host": "stackoverflow.com:443", + "description": "Stack Overflow certificate" + }, + { + "host": "microsoft.com:443", + "description": "Microsoft certificate" + } + ], + + "examplesSection": { + "title": "Certificate Examples", + "tooltip": "Analyze certificate for {host} ({description})" + }, + + "form": { + "title": "Certificate Analysis Configuration", + "hostPort": { + "label": "Host:Port", + "placeholder": "google.com:443", + "tooltip": "Enter hostname:port (e.g., google.com:443)", + "error": "Invalid host:port format" + }, + "customSni": { + "label": "Use custom SNI servername", + "placeholder": "example.com", + "tooltip": "Custom servername for SNI (Server Name Indication)" + }, + "analyze": "Analyze Certificate", + "analyzing": "Analyzing Certificate...", + "errors": { + "analysisFailedStatus": "Certificate analysis failed ({status})", + "unknownError": "Unknown error occurred" + } + }, + + "results": { + "title": "Certificate Analysis Results", + "copy": "Copy Certificate Info", + "copied": "Copied!", + + "status": { + "expired": "Expired", + "expiresInDays": "Expires in {days} days", + "validForDays": "Valid for {days} days", + "notYetValid": "Not yet valid", + "currentlyValid": "Currently valid" + }, + + "certificate": { + "title": "Certificate Information", + "commonName": "Common Name:", + "organization": "Organization:", + "issuer": "Issuer:", + "serialNumber": "Serial Number:", + "validFrom": "Valid From:", + "validTo": "Valid To:", + "na": "N/A" + }, + + "san": { + "title": "Subject Alternative Names" + }, + + "fingerprints": { + "title": "Fingerprints", + "sha1": "SHA1:", + "sha256": "SHA256:" + }, + + "chain": { + "title": "Certificate Chain ({count} certificates)", + "level": "Level {level}", + "issuer": "Issuer: {issuer}", + "expires": "Expires: {date}" + }, + + "connection": { + "title": "Connection Details", + "tlsVersion": "TLS Version:", + "cipherSuite": "Cipher Suite:", + "alpnProtocol": "ALPN Protocol:" + } + }, + + "error": { + "title": "Certificate Analysis Failed" + }, + + "copyTemplate": { + "header": "TLS Certificate Analysis for {host}", + "generated": "Generated at: {timestamp}", + "subject": "Subject: {subject}", + "issuer": "Issuer: {issuer}", + "validFrom": "Valid From: {validFrom}", + "validTo": "Valid To: {validTo}", + "daysUntilExpiry": "Days Until Expiry: {days}", + "serialNumber": "Serial Number: {serialNumber}", + "fingerprintSha1": "Fingerprint (SHA1): {fingerprint}", + "fingerprintSha256": "Fingerprint (SHA256): {fingerprint256}", + "sanHeader": "Subject Alternative Names:", + "sanItem": " {san}" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-cipher-presets.json b/src/lib/i18n/translations/en/diagnostics/tls-cipher-presets.json new file mode 100644 index 00000000..62ce236d --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-cipher-presets.json @@ -0,0 +1,37 @@ +{ + "title": "TLS Cipher Presets", + "description": "Probe connectivity with preset cipher lists (modern/intermediate/legacy)", + + "examples": { + "github": "GitHub cipher support", + "cloudflare": "Cloudflare cipher support", + "google": "Google cipher support" + }, + + "form": { + "title": "Cipher Presets Configuration", + "hostname": { + "label": "Hostname and Port", + "placeholder": "example.com" + }, + "port": { + "placeholder": "443" + }, + "test": "Test Cipher Presets", + "testing": "Testing...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "Cipher Preset Results", + "presets": { + "modern": "Modern", + "intermediate": "Intermediate", + "legacy": "Legacy" + }, + "supported": "Supported", + "not_supported": "Not Supported", + "score": "Score: {score}%", + "grade": "Grade: {grade}" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-ct-log-search.json b/src/lib/i18n/translations/en/diagnostics/tls-ct-log-search.json new file mode 100644 index 00000000..8555641f --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-ct-log-search.json @@ -0,0 +1,24 @@ +{ + "title": "Certificate Transparency Log Search", + "description": "Search Certificate Transparency logs for issued certificates for a domain", + + "form": { + "title": "CT Log Search", + "domain": { + "label": "Domain", + "placeholder": "example.com" + }, + "search": "Search CT Logs", + "searching": "Searching...", + "error": "Please enter a domain" + }, + + "results": { + "title": "CT Log Results", + "certificates_found": "{count} certificates found", + "issuer": "Issuer", + "valid_from": "Valid From", + "valid_to": "Valid To", + "logged_at": "Logged At" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-handshake-analyzer.json b/src/lib/i18n/translations/en/diagnostics/tls-handshake-analyzer.json new file mode 100644 index 00000000..77f7a680 --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-handshake-analyzer.json @@ -0,0 +1,28 @@ +{ + "title": "TLS Handshake Analyzer", + "description": "Analyze TLS handshake details including cipher suites, protocols, and certificate information", + + "form": { + "title": "TLS Handshake Analysis", + "hostname": { + "label": "Hostname", + "placeholder": "example.com" + }, + "port": { + "label": "Port", + "placeholder": "443" + }, + "analyze": "Analyze Handshake", + "analyzing": "Analyzing...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "Handshake Analysis", + "protocol": "Protocol", + "cipher_suite": "Cipher Suite", + "key_exchange": "Key Exchange", + "server_signature": "Server Signature", + "perfect_forward_secrecy": "Perfect Forward Secrecy" + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-ocsp-stapling.json b/src/lib/i18n/translations/en/diagnostics/tls-ocsp-stapling.json new file mode 100644 index 00000000..768bf95e --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-ocsp-stapling.json @@ -0,0 +1,30 @@ +{ + "title": "OCSP Stapling Checker", + "description": "Check if a server supports OCSP stapling for certificate revocation validation", + + "form": { + "title": "OCSP Stapling Check", + "hostname": { + "label": "Hostname", + "placeholder": "example.com" + }, + "port": { + "label": "Port", + "placeholder": "443" + }, + "check": "Check OCSP Stapling", + "checking": "Checking...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "OCSP Stapling Results", + "supported": "OCSP Stapling Supported", + "not_supported": "OCSP Stapling Not Supported", + "status": { + "good": "Good", + "revoked": "Revoked", + "unknown": "Unknown" + } + } +} diff --git a/src/lib/i18n/translations/en/diagnostics/tls-versions.json b/src/lib/i18n/translations/en/diagnostics/tls-versions.json new file mode 100644 index 00000000..1c82f28f --- /dev/null +++ b/src/lib/i18n/translations/en/diagnostics/tls-versions.json @@ -0,0 +1,35 @@ +{ + "title": "TLS Version Checker", + "description": "Check which TLS/SSL protocol versions are supported by a server", + + "form": { + "title": "TLS Version Check", + "hostname": { + "label": "Hostname", + "placeholder": "example.com" + }, + "port": { + "label": "Port", + "placeholder": "443" + }, + "check": "Check TLS Versions", + "checking": "Checking...", + "error": "Please enter a hostname" + }, + + "results": { + "title": "TLS Version Support", + "versions": { + "tls13": "TLS 1.3", + "tls12": "TLS 1.2", + "tls11": "TLS 1.1", + "tls10": "TLS 1.0", + "ssl3": "SSL 3.0", + "ssl2": "SSL 2.0" + }, + "supported": "Supported", + "not_supported": "Not Supported", + "deprecated": "Deprecated", + "insecure": "Insecure" + } +} diff --git a/src/lib/i18n/translations/en/furniture.json b/src/lib/i18n/translations/en/furniture.json new file mode 100644 index 00000000..43d7b119 --- /dev/null +++ b/src/lib/i18n/translations/en/furniture.json @@ -0,0 +1,146 @@ +{ + "header": { + "subtitle": "The sysadmin's Swiss Army knife", + "shortcuts": "Keyboard shortcuts" + }, + + "footer": { + "view_github": "View on GitHub" + }, + + "navigation": { + "primary": "Primary navigation", + "mobile": "Mobile navigation", + "cidr_tools": "CIDR Tools", + "untitled_tool": "Untitled Tool" + }, + + "menu": { + "open": "Open menu", + "close": "Close menu" + }, + + "search": { + "placeholder": "Search tools and reference...", + "close": "Close search", + "no_results": "No results found" + }, + + "shortcuts": { + "title": "Keyboard Shortcuts", + "command_palette": "Command Palette", + "app_title": "Networking Toolbox", + "close": "Close shortcuts", + + "view_options": { + "shortcuts": "Keyboard Shortcuts", + "about": "About" + }, + + "categories": { + "navigation": "Navigation", + "bookmarks": "Bookmarks", + "general": "General" + }, + + "actions": { + "open_search": "Open search", + "open_settings": "Open settings", + "toggle_menu": "Toggle menu", + "go_home": "Go to homepage", + "show_shortcuts": "Show shortcuts", + "jump_bookmark": "Jump to bookmarked tool", + "close_dialogs": "Close dialogs/clear" + }, + + "bookmarks": { + "your_tools": "Your bookmarked tools ({count})", + "homepage": "Homepage", + "view_all": "View all Bookmarks", + "empty": "You don't have any bookmarks yet.", + "help": "Right-click on a tool to bookmark it for quick access and offline use." + }, + + "about": { + "description_1": "Networking Toolbox is an open-source collection of web-based networking tools designed to make network-related tasks quicker and easier.", + "description_2": "With 100+ tools, it's privacy-focused and self-hostable, fully customizable, and includes a free REST API for automation.", + "github": "GitHub", + "page_listing": "Page Listing", + "app_settings": "App Settings", + "documentation": "Documentation", + "support": "Support", + "legal": "Legal", + "sponsor_heading": "Finding Networking Toolbox useful?", + "sponsor_text": "Consider sponsoring us on GitHub to support ongoing development!", + "license": "licensed under", + "version": "v{version}" + } + }, + + "quick_tips": { + "title": "Quick tips", + "dismiss": "Dismiss tips", + "previous": "Previous tip", + "next": "Next tip", + + "tips": { + "customize": { + "title": "Customize the app in the settings", + "description": "Change themes, adjust accessibility, or modify the layout to suit your needs" + }, + "bookmarks": { + "title": "Bookmark tools for easy access and offline use", + "description": "Right-click any tool to save it for quick access" + }, + "search": { + "title": "Use Ctrl+K to quickly search", + "description": "Jump to any tool or reference page instantly" + }, + "keyboard": { + "title": "Press Ctrl+/ for keyboard shortcuts", + "description": "Learn all the productivity shortcuts available" + } + } + }, + + "settings_menu": { + "open": "Open Settings" + }, + + "settings_panel": { + "aria": { + "theme_selection": "Theme selection", + "font_scale": "Font scale", + "select_color": "Select color {color}" + }, + + "placeholders": { + "site_title": "Networking Toolbox", + "site_description": "Your companion for all-things networking", + "site_icon": "/favicon.svg or https://example.com/icon.png", + "hex_color": "#2563eb", + "custom_css": "/* Enter your custom CSS here */" + }, + + "color_picker": { + "title": "Color Picker" + }, + + "not_found": { + "title": "Not found what you were looking for?", + "line1": "Good news! The code is open source and easy to work with.", + "content": "Simply fork the repo, follow our dev setup instructions, make whatever changes and customizations you like, and then deploy your own instance.", + "support": "We also offer enterprise support services, where we can make custom changes for you." + }, + + "export": { + "settings": "Export Settings", + "styles": "Export Styles", + "env_vars_title": "Environment Variables", + "custom_css_title": "Custom CSS", + "custom_css_description": "Apply your custom CSS to your self-hosted instance by mounting a CSS file.", + "copy_clipboard": "Copy to Clipboard", + "copied": "Copied!" + } + } +} diff --git a/src/lib/i18n/translations/en/nav.json b/src/lib/i18n/translations/en/nav.json new file mode 100644 index 00000000..576ab27d --- /dev/null +++ b/src/lib/i18n/translations/en/nav.json @@ -0,0 +1,59 @@ +{ + "top": { + "subnetting": { + "label": "Subnetting", + "description": "Subnet calculators for IPv4, IPv6, VLSM, supernetting" + }, + "cidr": { + "label": "CIDR", + "description": "CIDR notation tools, converters, and calculators" + }, + "ip_tools": { + "label": "IP Tools", + "description": "IP address converters, validators, and generators" + }, + "dns_tools": { + "label": "DNS Tools", + "description": "DNS record generators, validators, and DNSSEC tools" + }, + "dhcp": { + "label": "DHCP", + "description": "DHCP option generators for wireless controllers and network configuration" + }, + "diagnostics": { + "label": "Lookups", + "description": "Network diagnostics and connectivity testing" + }, + "reference": { + "label": "Ref", + "description": "Networking reference guides and lookup tables" + } + }, + + "standalone": { + "bookmarks": { + "label": "Bookmarks", + "description": "Save and organize network calculations and tool results" + }, + "search": { + "label": "Search", + "description": "Find tools and reference content quickly" + }, + "settings": { + "label": "Settings", + "description": "Customize themes, layouts, and accessibility options" + } + }, + + "common_tools": { + "subnet_calculator": "Subnet Calculator", + "ipv6_subnet_calculator": "IPv6 Subnet Calculator", + "vlsm_calculator": "VLSM Calculator", + "supernet_calculator": "Supernet Calculator", + "cidr_summarizer": "CIDR Summarizer", + "cidr_splitter": "CIDR Splitter", + "ip_validator": "IP Validator", + "dns_lookup": "DNS Lookup", + "tls_certificate": "TLS Certificate Analyzer" + } +} diff --git a/src/lib/i18n/translations/en/pages/arp-vs-ndp.json b/src/lib/i18n/translations/en/pages/arp-vs-ndp.json new file mode 100644 index 00000000..1ab5fc17 --- /dev/null +++ b/src/lib/i18n/translations/en/pages/arp-vs-ndp.json @@ -0,0 +1,88 @@ +{ + "title": "ARP vs NDP: IPv4 and IPv6 Address Resolution", + "description": "Compare Address Resolution Protocol (ARP) for IPv4 with Neighbor Discovery Protocol (NDP) for IPv6, including their differences, advantages, and practical implications", + + "comparison": { + "title": "Side-by-Side Comparison", + "headers": { + "arp": "ARP (IPv4)", + "ndp": "NDP (IPv6)" + } + }, + + "arp": { + "messageTypes": { + "title": "ARP Message Types", + "fields": { + "description": "Description:", + "destination": "Destination:", + "response": "Response:" + } + }, + "process": { + "title": "ARP Process" + }, + "limitations": { + "title": "ARP Limitations" + } + }, + + "ndp": { + "messageTypes": { + "title": "NDP Message Types", + "fields": { + "icmpType": "ICMP Type:", + "description": "Description:", + "destination": "Destination:", + "purpose": "Purpose:" + } + }, + "process": { + "title": "NDP Process" + }, + "advantages": { + "title": "NDP Advantages Over ARP" + } + }, + + "practical": { + "title": "Practical Differences", + "fields": { + "arp": "ARP (IPv4):", + "ndp": "NDP (IPv6):", + "impact": "Impact:" + } + }, + + "troubleshooting": { + "title": "Troubleshooting Commands", + "headers": { + "ipv4": "IPv4 (ARP)", + "ipv6": "IPv6 (NDP)" + } + }, + + "issues": { + "title": "Common Issues", + "fields": { + "description": "Description:", + "detection": "Detection:", + "mitigation": "Mitigation:" + } + }, + + "bestPractices": { + "title": "Best Practices" + }, + + "quickReference": { + "title": "Quick Reference", + "arp": "ARP Key Points", + "ndp": "NDP Key Points" + }, + + "migration": { + "title": "IPv4 to IPv6 Migration Tips", + "considerations": "Important Considerations" + } +} diff --git a/src/lib/i18n/translations/en/pages/cgnat.json b/src/lib/i18n/translations/en/pages/cgnat.json new file mode 100644 index 00000000..301b542d --- /dev/null +++ b/src/lib/i18n/translations/en/pages/cgnat.json @@ -0,0 +1,86 @@ +{ + "cgnat": { + "addressRange": { + "title": "CGNAT Address Range", + "sharedSpace": "Shared Address Space", + "labels": { + "range": "Range", + "fullRange": "Full Range", + "totalAddresses": "Total Addresses", + "rfc": "RFC" + }, + "breakdown": { + "title": "Address Breakdown", + "headers": { + "network": "Network Block", + "addresses": "Available Addresses", + "use": "Typical Use" + } + } + }, + "natSystem": { + "title": "Two-Layer NAT System", + "headers": { + "layer": "Layer", + "location": "Location", + "insideAddress": "Inside Address", + "outsideAddress": "Outside Address", + "purpose": "Purpose" + } + }, + "trafficFlow": { + "title": "Traffic Flow" + }, + "identification": { + "labels": { + "description": "Description", + "cgnatIndicator": "CGNAT Indicator", + "normalIndicator": "Normal Indicator" + } + }, + "impacts": { + "title": "Impact on Services", + "negative": { + "title": "Negative Impacts", + "labels": { + "description": "Description", + "affectedServices": "Affected Services", + "workaround": "Workaround" + } + }, + "positive": { + "title": "Positive Aspects" + } + }, + "workarounds": { + "title": "Workarounds and Solutions", + "labels": { + "description": "Description", + "effectiveness": "Effectiveness", + "cost": "Cost" + } + }, + "troubleshooting": { + "title": "Troubleshooting Common Issues", + "labels": { + "cause": "Cause", + "diagnosis": "Diagnosis", + "solution": "Solution" + } + }, + "quickCheck": { + "title": "Quick CGNAT Check", + "stepsTitle": "Steps to Check", + "nextStepsTitle": "What to Do Next" + }, + "bestPractices": { + "title": "Best Practices" + }, + "ispPerspective": { + "title": "ISP Perspective", + "whyTitle": "Why ISPs Use CGNAT", + "tradeoffTitle": "Understanding the Trade-off", + "tradeoffDescription": "CGNAT is a necessary compromise. It allows ISPs to provide affordable internet service during IPv4 exhaustion, but at the cost of some functionality. The long-term solution is IPv6 adoption." + } + } +} diff --git a/src/lib/i18n/translations/en/pages/cidr-summarize.json b/src/lib/i18n/translations/en/pages/cidr-summarize.json new file mode 100644 index 00000000..efd5d334 --- /dev/null +++ b/src/lib/i18n/translations/en/pages/cidr-summarize.json @@ -0,0 +1,80 @@ +{ + "cidrSummarize": { + "about": { + "title": "About CIDR Summarization", + "description": "CIDR Summarization optimizes network routing by combining multiple IP addresses, ranges, and CIDR blocks into the minimal set of CIDR prefixes that covers the same address space." + }, + "benefits": { + "routeTable": { + "title": "Route Table Optimization", + "description": "Reduce routing table size by aggregating multiple routes into fewer, larger prefixes" + }, + "networkEfficiency": { + "title": "Network Efficiency", + "description": "Minimize routing protocol overhead and improve convergence times" + }, + "dualProtocol": { + "title": "Dual Protocol Support", + "description": "Handle mixed IPv4 and IPv6 inputs with separate optimized outputs" + }, + "flexibleInput": { + "title": "Flexible Input Formats", + "description": "Process single IPs, CIDR blocks, and explicit ranges in any combination" + } + }, + "modes": { + "title": "Summarization Modes", + "exactMerge": { + "title": "Exact Merge", + "description": "Conservative approach: Merges overlapping ranges exactly without additional aggregation" + }, + "minimalCover": { + "title": "Minimal Cover", + "description": "Aggressive optimization: Finds the smallest set of CIDR blocks that covers all inputs" + }, + "example": { + "label": "Example:" + } + }, + "useCases": { + "title": "Common Use Cases", + "bgp": { + "title": "BGP Route Aggregation", + "description": "Optimize BGP advertisements by summarizing customer routes into provider prefixes" + }, + "firewall": { + "title": "Firewall Rule Optimization", + "description": "Reduce ACL complexity by consolidating IP ranges into fewer CIDR rules" + }, + "planning": { + "title": "Network Planning", + "description": "Analyze address space utilization and optimize subnet allocations" + }, + "migration": { + "title": "Migration Planning", + "description": "Consolidate legacy network ranges during infrastructure modernization" + } + }, + "inputFormats": { + "title": "Supported Input Formats", + "singleIP": { + "title": "Single IP Addresses" + }, + "cidrBlocks": { + "title": "CIDR Blocks" + }, + "ipRanges": { + "title": "IP Ranges" + }, + "mixedLists": { + "title": "Mixed Lists", + "example1": "One item per line", + "example2": "IPv4 and IPv6 together" + } + }, + "optimizationTips": { + "title": "Optimization Tips", + "description": "For maximum efficiency, align your network allocations to power-of-2 boundaries. Contiguous address blocks summarize much more effectively than scattered allocations. Use the exact merge mode for conservative summarization or minimal cover for aggressive optimization." + } + } +} diff --git a/src/lib/i18n/translations/en/pages/ipv6-notation.json b/src/lib/i18n/translations/en/pages/ipv6-notation.json new file mode 100644 index 00000000..0d7c3fc9 --- /dev/null +++ b/src/lib/i18n/translations/en/pages/ipv6-notation.json @@ -0,0 +1,100 @@ +{ + "title": "Understanding IPv6 Address Notation", + "conversions": { + "title": "Conversion Use Cases & Applications", + "examples": { + "title": "Technical Examples & Standards" + } + }, + "formats": { + "expanded": { + "title": "Expanded (Full) Format", + "structure": "All 32 hexadecimal characters with colons every 4 digits", + "usage": "Debugging, detailed analysis, and when precision is required", + "benefits": "Shows complete address structure, easier to parse programmatically" + }, + "compressed": { + "title": "Compressed (Shortened) Format", + "structure": "Uses :: to represent consecutive zero groups, removes leading zeros", + "usage": "Configuration files, user interfaces, documentation", + "benefits": "Shorter, more readable, standard representation" + }, + "rules": { + "title": "Compression Rules", + "doubleColon": "Represents one or more consecutive zero groups", + "singleUse": "Only one :: allowed per address to avoid ambiguity", + "leadingZeros": "Remove leading zeros from each group (0001 β†’ 1)", + "preference": "Compress the longest sequence of consecutive zeros" + } + }, + "useCases": { + "expand": { + "title": "Expand IPv6 Addresses", + "networkAnalysis": "Compare addresses byte-by-byte", + "databaseStorage": "Consistent format for indexing", + "debugging": "See complete address structure", + "programming": "Easier parsing and manipulation", + "security": "Avoid address obfuscation issues" + }, + "compress": { + "title": "Compress IPv6 Addresses", + "userInterface": "Shorter, more readable addresses", + "configuration": "Cleaner config files and logs", + "documentation": "Standard format for examples", + "urls": "Shorter addresses in IPv6 URLs", + "networkEquipment": "Standard display format" + }, + "scenarios": { + "title": "Real-world Scenarios", + "networkMonitoring": "Consistent address formatting", + "apiIntegration": "Standardize input/output formats", + "dataMigration": "Convert between address formats", + "educationalTools": "Demonstrate IPv6 structure", + "qualityAssurance": "Validate address representations" + } + }, + "examples": { + "commonTypes": "Common Address Types", + "loopback": "Loopback:", + "linkLocal": "Link-Local:", + "documentation": "Documentation:", + "bestPractices": { + "title": "Best Practices", + "rfc5952": "Follow standard compression guidelines", + "consistency": "Use same format throughout applications", + "validation": "Always validate both input and output", + "caseSensitivity": "Lowercase preferred (RFC 5952)", + "leadingZeros": "Always remove for compressed form" + } + }, + "terms": { + "structure": "Structure:", + "example": "Example:", + "usage": "Usage:", + "benefits": "Benefits:", + "doubleColonLabel": "Double Colon (::):", + "singleUseLabel": "Single Use:", + "leadingZerosLabel": "Leading Zeros:", + "preferenceLabel": "Preference:", + "networkAnalysisLabel": "Network Analysis:", + "databaseStorageLabel": "Database Storage:", + "debuggingLabel": "Debugging:", + "programmingLabel": "Programming:", + "securityLabel": "Security:", + "userInterfaceLabel": "User Interface:", + "configurationLabel": "Configuration:", + "documentationLabel": "Documentation:", + "urlsLabel": "URLs:", + "networkEquipmentLabel": "Network Equipment:", + "networkMonitoringLabel": "Network Monitoring:", + "apiIntegrationLabel": "API Integration:", + "dataMigrationLabel": "Data Migration:", + "educationalToolsLabel": "Educational Tools:", + "qualityAssuranceLabel": "Quality Assurance:", + "rfc5952Label": "RFC 5952:", + "consistencyLabel": "Consistency:", + "validationLabel": "Validation:", + "caseSensitivityLabel": "Case Sensitivity:", + "leadingZerosRuleLabel": "Leading Zeros:" + } +} diff --git a/src/lib/i18n/translations/en/pages/ipv6-privacy.json b/src/lib/i18n/translations/en/pages/ipv6-privacy.json new file mode 100644 index 00000000..2a9effdb --- /dev/null +++ b/src/lib/i18n/translations/en/pages/ipv6-privacy.json @@ -0,0 +1,388 @@ +{ + "ipv6Privacy": { + "title": "IPv6 Privacy Addresses (RFC 4941/8981)", + "description": "SLAAC privacy extensions: temporary vs stable interface identifiers, how they protect privacy, and configuration guidance.", + + "sections": { + "overview": { + "title": "What are IPv6 Privacy Addresses?", + "content": "IPv6 privacy addresses (temporary addresses) are automatically generated to prevent tracking based on stable interface identifiers. They're created alongside stable addresses and change periodically.\n\nWithout privacy extensions, devices use predictable interface identifiers (often based on MAC addresses), making them trackable across networks." + }, + "problem": { + "title": "The Privacy Problem", + "content": "Standard IPv6 addresses often contain predictable interface identifiers that remain constant across different networks, creating privacy concerns similar to a permanent device fingerprint." + } + }, + + "addressTypes": { + "title": "IPv6 Address Types", + "types": [ + { + "type": "Stable Address (Standard SLAAC)", + "formation": "Prefix + EUI-64 or configured interface ID", + "example": "2001:db8:1234:5678:21a:2bff:fe3c:4d5e", + "characteristics": [ + "Interface identifier stays the same across networks", + "Often derived from MAC address using EUI-64", + "Predictable and trackable across network changes", + "Required for some services that need consistent addressing" + ], + "privacy": "Poor - enables tracking across networks" + }, + { + "type": "Temporary Address (Privacy Extension)", + "formation": "Prefix + cryptographically generated random bits", + "example": "2001:db8:1234:5678:a1b2:c3d4:e5f6:7890", + "characteristics": [ + "Randomly generated interface identifier", + "Changes periodically (daily by default)", + "Multiple temporary addresses can coexist", + "Used for outbound connections by default" + ], + "privacy": "Good - prevents cross-network tracking" + }, + { + "type": "Stable Private Address (RFC 7217)", + "formation": "Prefix + hash of secret key + network info", + "example": "2001:db8:1234:5678:9abc:def0:1234:5678", + "characteristics": [ + "Stable within the same network", + "Changes when moving to different networks", + "More predictable than temporary addresses", + "Good balance of privacy and stability" + ], + "privacy": "Better - network-specific but stable" + } + ] + }, + + "howItWorks": { + "title": "How Privacy Extensions Work", + "addressGenerationTitle": "Address Generation Process", + "addressGeneration": [ + "Device receives Router Advertisement with prefix", + "Creates stable address using EUI-64 or configured method", + "Generates temporary address using cryptographic random bits", + "Both addresses are assigned to the same interface", + "Temporary address preferred for outbound connections" + ], + "temporaryLifecycleTitle": "Temporary Address Lifecycle", + "temporaryLifecycle": [ + "New temporary address generated periodically", + "Old temporary addresses remain valid until expiry", + "Multiple temporary addresses can coexist", + "Addresses have preferred and valid lifetimes", + "Deprecated addresses still accept incoming traffic" + ], + "defaultBehaviorTitle": "Default Operating System Behavior", + "defaultBehavior": [ + "Outbound connections use temporary addresses", + "Inbound services use stable addresses", + "Applications can request specific address types", + "Operating system manages address selection automatically" + ] + }, + + "lifetimes": { + "title": "Address Lifetimes", + "preferredLifetime": { + "title": "Preferred Lifetime", + "description": "How long address is preferred for new connections", + "typical": "1 day (86400 seconds)", + "behavior": "After expiry, address can receive but not initiate connections" + }, + "validLifetime": { + "title": "Valid Lifetime", + "description": "How long address remains usable", + "typical": "7 days (604800 seconds)", + "behavior": "After expiry, address is completely removed" + }, + "regenerationInterval": { + "title": "Regeneration Interval", + "description": "How often new temporary addresses are created", + "typical": "5 minutes to 24 hours", + "behavior": "New address created before old one expires" + }, + "maxTempAddresses": { + "title": "Max Temporary Addresses", + "description": "Maximum temporary addresses per prefix", + "typical": "5-10 addresses", + "behavior": "Oldest addresses removed when limit reached" + } + }, + + "osImplementations": { + "title": "Operating System Support", + "implementations": [ + { + "os": "Windows", + "defaultBehavior": "Privacy extensions enabled by default (Vista+)", + "configuration": [ + "netsh interface ipv6 set global randomizeidentifiers=enabled", + "netsh interface ipv6 set privacy state=enabled", + "Registry: HKLM\\System\\CurrentControlSet\\Services\\Tcpip6\\Parameters" + ], + "commands": ["netsh interface ipv6 show privacy", "netsh interface ipv6 show addresses"] + }, + { + "os": "Linux", + "defaultBehavior": "Varies by distribution, often disabled by default", + "configuration": [ + "sysctl net.ipv6.conf.all.use_tempaddr=2", + "sysctl net.ipv6.conf.default.use_tempaddr=2", + "/proc/sys/net/ipv6/conf/*/use_tempaddr" + ], + "values": ["0 = Disabled", "1 = Enabled but prefer stable", "2 = Enabled and prefer temporary"], + "commands": ["ip -6 addr show scope global", "cat /proc/sys/net/ipv6/conf/eth0/use_tempaddr"] + }, + { + "os": "macOS", + "defaultBehavior": "Privacy extensions enabled by default", + "configuration": [ + "Built into system preferences", + "networksetup command line tool", + "System-wide setting affects all interfaces" + ], + "commands": ["ifconfig | grep inet6", "networksetup -getinfo Wi-Fi"] + }, + { + "os": "Android", + "defaultBehavior": "Privacy extensions enabled by default (Android 8+)", + "configuration": [ + "Settings > Network & Internet > Advanced", + "Developer options for advanced control", + "Per-network configuration possible" + ], + "behavior": "Randomizes MAC and uses privacy addresses" + } + ] + }, + + "identifyingAddresses": { + "title": "Identifying Address Types", + "methods": [ + { + "method": "Interface Identifier Pattern", + "stable": "Often contains 'fffe' in middle (EUI-64) or predictable pattern", + "temporary": "Random-looking interface identifier", + "example": "Stable: ::21a:2bff:fe3c:4d5e vs Temporary: ::a1b2:c3d4:e5f6:7890" + }, + { + "method": "Address Consistency", + "stable": "Same interface ID across different network prefixes", + "temporary": "Different interface ID on each network", + "example": "Device keeps same ::21a:2bff:fe3c:4d5e on all networks vs random on each" + }, + { + "method": "Command Output", + "stable": "Often labeled as 'permanent' or primary", + "temporary": "Labeled as 'temporary' or 'deprecated'", + "example": "Linux ip command shows 'temporary' flag" + } + ] + }, + + "troubleshooting": { + "title": "Troubleshooting", + "issues": [ + { + "issue": "Privacy addresses not working", + "symptoms": ["Same IPv6 address on different networks", "Tracking concerns"], + "diagnosis": "Check OS privacy extension settings", + "solutions": [ + "Enable privacy extensions in OS settings", + "Verify router supports SLAAC", + "Check for disabled IPv6 privacy in network manager" + ] + }, + { + "issue": "Too many IPv6 addresses", + "symptoms": ["Multiple IPv6 addresses per interface", "Address list constantly changing"], + "diagnosis": "Privacy extensions working normally", + "solutions": [ + "This is normal behavior for privacy extensions", + "Adjust regeneration timers if needed", + "Reduce max temporary addresses if causing issues" + ] + }, + { + "issue": "Applications using wrong address", + "symptoms": ["Server not reachable", "Unexpected source addresses"], + "diagnosis": "Address selection preference issues", + "solutions": [ + "Configure application to bind specific addresses", + "Adjust address selection policy", + "Use stable addresses for server applications" + ] + }, + { + "issue": "Privacy addresses not preferred", + "symptoms": ["Always using stable addresses for outbound"], + "diagnosis": "Address selection policy favoring stable addresses", + "solutions": [ + "Configure temporary address preference", + "Check application-specific settings", + "Verify privacy extension configuration" + ] + } + ] + }, + + "securityConsiderations": { + "title": "Security Considerations", + "aspects": [ + { + "aspect": "Privacy Protection", + "benefits": [ + "Prevents device tracking across networks", + "Makes traffic analysis more difficult", + "Reduces correlation of activities", + "Protects against location tracking" + ], + "limitations": [ + "Application-layer tracking still possible", + "DNS queries may reveal information", + "Stable addresses still exposed for services", + "Requires proper application configuration" + ] + }, + { + "aspect": "Network Management", + "benefits": [ + "Devices harder to target maliciously", + "Reduces effectiveness of IP-based blocking", + "Makes reconnaissance more difficult" + ], + "challenges": [ + "Harder to whitelist specific devices", + "Complicates network troubleshooting", + "May interfere with IP-based access control", + "Requires different monitoring approaches" + ] + } + ] + }, + + "bestPractices": { + "title": "Best Practices", + "practices": [ + "Enable privacy extensions on client devices", + "Use stable addresses only for servers and infrastructure", + "Configure appropriate regeneration intervals", + "Monitor for privacy extension support in applications", + "Balance privacy with network management needs", + "Document which services require stable addressing", + "Test applications with privacy addresses enabled", + "Consider RFC 7217 stable privacy addresses for better balance" + ] + }, + + "whenToUse": { + "title": "When to Use Privacy Extensions", + "scenarios": [ + { + "scenario": "Client Devices", + "recommendation": "Enable privacy extensions", + "reasoning": "Protects user privacy without impacting functionality", + "configuration": "Prefer temporary addresses for outbound connections" + }, + { + "scenario": "Servers", + "recommendation": "Use stable addresses", + "reasoning": "Consistent addressing needed for services", + "configuration": "Disable privacy extensions or use stable addresses only" + }, + { + "scenario": "IoT Devices", + "recommendation": "Consider device requirements", + "reasoning": "Balance privacy with device management needs", + "configuration": "May need stable addresses for remote management" + }, + { + "scenario": "Enterprise Networks", + "recommendation": "Policy-based approach", + "reasoning": "Different requirements for different device types", + "configuration": "Client devices: privacy on, servers: stable addresses" + } + ] + }, + + "commonMistakes": { + "title": "Common Mistakes", + "mistakes": [ + "Assuming all IPv6 addresses are permanent", + "Not testing applications with privacy addresses", + "Blocking temporary addresses in firewalls", + "Using temporary addresses for server services", + "Not understanding address selection preferences", + "Confusing temporary addresses with link-local addresses", + "Expecting consistent addressing with privacy extensions enabled" + ] + }, + + "quickReference": { + "title": "Quick Reference", + "addressTypesTitle": "Address Types", + "addressTypes": [ + "Stable: Same interface ID everywhere (trackable)", + "Temporary: Random interface ID, changes periodically (private)", + "Stable Privacy (7217): Stable per-network, changes between networks" + ], + "identificationTitle": "Identification", + "identification": [ + "EUI-64 pattern (fffe in middle) = stable address", + "Random-looking interface ID = temporary address", + "Multiple addresses per interface = privacy extensions active" + ], + "configurationTitle": "Configuration", + "configuration": [ + "Windows: netsh interface ipv6 set privacy state=enabled", + "Linux: sysctl net.ipv6.conf.all.use_tempaddr=2", + "macOS: System Preferences > Network > Advanced" + ], + "troubleshootingTitle": "Troubleshooting", + "troubleshooting": [ + "Multiple IPv6 addresses = normal with privacy extensions", + "Same address everywhere = privacy extensions disabled", + "Services unreachable = check stable address binding" + ], + "keyRuleTitle": "Key Point", + "keyRule": "Privacy extensions create multiple IPv6 addresses per interface. Temporary addresses change periodically for privacy, while stable addresses remain consistent for services. Both can coexist on the same interface." + }, + + "testingTools": { + "title": "Testing Tools", + "tools": [ + { "tool": "ip -6 addr show", "purpose": "Show all IPv6 addresses with flags (Linux)" }, + { "tool": "ipconfig /all", "purpose": "Display IPv6 addresses and configuration (Windows)" }, + { "tool": "ifconfig", "purpose": "Show network interfaces and addresses (Unix/macOS)" }, + { "tool": "netsh interface ipv6 show addresses", "purpose": "Detailed IPv6 address info (Windows)" }, + { "tool": "sysctl net.ipv6.conf.all.use_tempaddr", "purpose": "Check privacy extension status (Linux)" } + ] + }, + + "labels": { + "formation": "Formation", + "example": "Example", + "privacyLevel": "Privacy Level", + "characteristics": "Characteristics", + "typical": "Typical", + "behavior": "Behavior", + "defaultBehavior": "Default Behavior", + "configuration": "Configuration", + "commands": "Commands", + "values": "Values", + "method": "Method", + "stable": "Stable", + "temporary": "Temporary", + "symptoms": "Symptoms", + "diagnosis": "Diagnosis", + "solutions": "Solutions", + "benefits": "Benefits", + "limitations": "Limitations", + "challenges": "Challenges", + "recommendation": "Recommendation", + "reasoning": "Reasoning" + } + } +} diff --git a/src/lib/i18n/translations/en/pages/legal.json b/src/lib/i18n/translations/en/pages/legal.json new file mode 100644 index 00000000..ca1ebaf0 --- /dev/null +++ b/src/lib/i18n/translations/en/pages/legal.json @@ -0,0 +1,142 @@ +{ + "privacy": { + "title": "Privacy Policy", + "metaDescription": "Privacy policy for Networking Toolbox - how we handle your data", + "ogTitle": "Privacy Policy | {siteTitle}", + "ogDescription": "Learn about how Networking Toolbox handles your data and privacy", + + "hero": { + "title": "Privacy Policy", + "lead": "Your privacy matters. Actually, that's one of the reasons that I built this (and my other apps). I believe you should have total transparency of how an app works (e.g. access to it's code), and full control over your data. So, weather you're self-hosting Networking Toolbox, or using the public instance, you can be sure that your data is handled with care." + }, + + "principles": { + "title": "Principles", + "items": [ + "We only collect the minimum data needed for things to work", + "We never store anything which isn't 100% necessary", + "Any stored data stays local in your browser or is encrypted with your own key (we can't access it)", + "We don't use cookies or tracking of any kind", + "We never share your data with anyone else", + "We're transparent about what's collected, stored, used and why", + "Our code is fully open source, so you can verify our claims for yourself" + ] + }, + + "overview": { + "title": "Overview", + "description": "Networking Toolbox is designed with privacy in mind. All network calculations and most diagnostic tools run entirely in your browser, meaning your data never leaves your device." + }, + + "dataProcessing": { + "title": "Data Processing", + "clientSide": { + "title": "Client-Side Processing", + "description": "The majority of tools (CIDR calculators, IP converters, subnet planners, etc.) perform all calculations locally in your browser. No data from these tools is transmitted to any server." + }, + "serverSide": { + "title": "Server-Side Diagnostic Tools", + "description": "Some diagnostic tools (DNS lookups, DNSBL checks, TLS tests, etc.) require server-side processing to query external services. For these tools:", + "items": [ + "Only the specific domain or IP address you enter is sent to our server", + "Data is processed in real-time and not stored permanently", + "Requests may be temporarily logged for debugging purposes (logs are automatically purged)", + "No personally identifiable information is collected" + ] + } + }, + + "localStorage": { + "title": "Local Storage", + "description": "We use browser localStorage (not cookies) to save your preferences locally. This data never leaves your device and can be cleared at any time through your browser settings.", + "items": [ + { + "title": "Theme settings:", + "description": "Your chosen color theme and accessibility preferences" + }, + { + "title": "Layout preferences:", + "description": "Homepage and navigation layout choices" + }, + { + "title": "Bookmarked tools:", + "description": "Your saved favorite tools for quick access" + } + ], + "note": "All localStorage usage is optional and strictly for your convenience. The application works without it." + }, + + "analytics": { + "title": "Analytics & Tracking", + "description": "Unless disabled, we log basic anonymous usage statistics using a self-hosted Plausible Analytics instance. This helps us understand which features are most useful and improve the application.", + "features": [ + { + "title": "Privacy-focused:", + "description": "No cookies, no tracking across sites, no personal data collection" + }, + { + "title": "Anonymous only:", + "description": "We only collect page views and referrer information - no IP addresses, user agents, or any personally identifiable information" + }, + { + "title": "Self-hosted:", + "description": "Analytics data stays on our infrastructure and is never shared with third parties" + }, + { + "title": "Opt-out:", + "description": "You can disable analytics by enabling \"Do Not Track\" in your browser settings" + } + ], + "note": "We do not use any third-party advertising services or tracking scripts that follow you across the web." + }, + + "thirdParty": { + "title": "Third-Party Services", + "description": "When using diagnostic tools, your queries may be sent to third-party services:", + "services": [ + { + "title": "DNS queries:", + "description": "Public DNS resolvers (Google, Cloudflare, Quad9, etc.)" + }, + { + "title": "DNSBL checks:", + "description": "Spam blacklist providers (Spamhaus, SORBS, etc.)" + }, + { + "title": "WHOIS/RDAP:", + "description": "Regional Internet registries" + }, + { + "title": "Certificate checks:", + "description": "Certificate Transparency logs" + } + ], + "note": "These services have their own privacy policies and are outside our control." + }, + + "selfHosting": { + "title": "Self-Hosting", + "description": "For maximum privacy, you can self-host Networking Toolbox. When self-hosted, you have complete control over all data processing and server logs. See the {deployingLink} for instructions.", + "deployingLinkText": "Self-Hosting section" + }, + + "openSource": { + "title": "Open Source", + "description": "Networking Toolbox is fully open source. You can review the code to verify our privacy practices:", + "githubLinkText": "Source code on GitHub" + }, + + "changes": { + "title": "Changes to This Policy", + "description": "This privacy policy may be updated occasionally. Significant changes will be noted in the {changelogLink}.", + "changelogLinkText": "changelog", + "lastUpdated": "Last updated: January 2025" + }, + + "contact": { + "title": "Questions?", + "description": "If you have questions about this privacy policy, please open an issue on {githubLink}.", + "githubLinkText": "GitHub" + } + } +} diff --git a/src/lib/i18n/translations/en/pages/link-local-apipa.json b/src/lib/i18n/translations/en/pages/link-local-apipa.json new file mode 100644 index 00000000..7b69f9e4 --- /dev/null +++ b/src/lib/i18n/translations/en/pages/link-local-apipa.json @@ -0,0 +1,82 @@ +{ + "linkLocalApipa": { + "apipa": { + "addressRange": { + "title": "Address Range", + "network": "Network", + "fullRange": "Full Range", + "usableRange": "Usable Range", + "reserved": "Reserved" + }, + "whenUsedTitle": "When APIPA is Used", + "howItWorksTitle": "How APIPA Works", + "characteristicsTitle": "APIPA Characteristics", + "troubleshootingTitle": "Troubleshooting APIPA Issues", + "troubleshootingLabels": { + "meaning": "Meaning", + "solution": "Solution" + } + }, + "ipv6": { + "addressRange": { + "title": "Address Range", + "network": "Network", + "fullRange": "Full Range", + "commonFormat": "Common Format" + }, + "addressFormationTitle": "Address Formation", + "whenUsedTitle": "When IPv6 Link-Local is Used", + "characteristicsTitle": "IPv6 Link-Local Characteristics", + "typesTitle": "Types of IPv6 Link-Local Addresses", + "typeLabels": { + "example": "Example", + "privacy": "Privacy" + } + }, + "comparison": { + "title": "IPv4 APIPA vs IPv6 Link-Local Comparison", + "headers": { + "aspect": "Aspect", + "ipv4Apipa": "IPv4 APIPA", + "ipv6LinkLocal": "IPv6 Link-Local" + } + }, + "practicalExamples": { + "title": "Practical Examples", + "labels": { + "ipv4Behavior": "IPv4 Behavior", + "ipv6Behavior": "IPv6 Behavior", + "impact": "Impact" + } + }, + "troubleshootingCommands": { + "title": "Troubleshooting Commands", + "headers": { + "purpose": "Purpose", + "windows": "Windows", + "linux": "Linux", + "macOS": "macOS" + } + }, + "whenToWorry": { + "title": "When to Worry", + "labels": { + "concernLevel": "Concern Level", + "action": "Action" + } + }, + "bestPractices": { + "title": "Best Practices" + }, + "commonMistakes": { + "title": "Common Mistakes" + }, + "quickReference": { + "title": "Quick Reference", + "recognitionTitle": "Recognition", + "troubleshootingTitle": "Troubleshooting", + "keyDifferenceTitle": "Key Difference", + "keyDifferenceText": "IPv4 APIPA (169.254.x.x) indicates a problem - DHCP failed. IPv6 link-local (fe80::) is normal and required - every IPv6 interface has one." + } + } +} diff --git a/src/lib/i18n/translations/en/pages/reverse-zones.json b/src/lib/i18n/translations/en/pages/reverse-zones.json new file mode 100644 index 00000000..24dd8f1d --- /dev/null +++ b/src/lib/i18n/translations/en/pages/reverse-zones.json @@ -0,0 +1,305 @@ +{ + "reverseZones": { + "title": "Reverse Zones for CIDR Delegation", + "description": "Minimal reverse DNS zones needed to properly delegate IPv4 and IPv6 CIDR blocks with practical examples.", + + "sections": { + "overview": { + "title": "What are Reverse Zones?", + "content": "Reverse DNS zones map IP addresses back to domain names using special domains (.in-addr.arpa for IPv4, .ip6.arpa for IPv6). When you're delegated a CIDR block, you need to create the corresponding reverse zones for proper DNS operation.\n\nReverse zones are essential for mail servers, logging, security tools, and network troubleshooting." + }, + "delegation": { + "title": "How Reverse Delegation Works", + "content": "Your ISP or RIR delegates reverse DNS authority for your IP blocks to your DNS servers. You then create the reverse zones and populate them with PTR records that map IP addresses to hostnames.\n\nThe delegation happens at specific boundaries that align with IP addressing hierarchy." + } + }, + + "ipv4Zones": { + "title": "IPv4 Reverse Zones (in-addr.arpa)", + "classfullBoundariesTitle": "Classful Boundaries (Octet-Aligned)", + "classlessDelegationTitle": "Classless Delegation (CNAME Method)", + "practicalExamplesTitle": "Practical IPv4 Examples", + + "tableHeaders": { + "cidr": "CIDR", + "example": "Example", + "reverseZone": "Reverse Zone", + "description": "Description", + "delegation": "Delegation" + }, + + "classfullBoundaries": [ + { + "cidr": "/8", + "example": "10.0.0.0/8", + "reverseZone": "10.in-addr.arpa", + "description": "Entire Class A network", + "delegation": "Usually handled by RIRs, not end users" + }, + { + "cidr": "/16", + "example": "172.16.0.0/16", + "reverseZone": "16.172.in-addr.arpa", + "description": "Class B network", + "delegation": "Large organizations or ISPs" + }, + { + "cidr": "/24", + "example": "192.168.1.0/24", + "reverseZone": "1.168.192.in-addr.arpa", + "description": "Class C network - most common delegation", + "delegation": "Standard small business / organization" + } + ], + + "classlessDelegation": [ + { + "cidr": "/25", + "example": "203.0.113.0/25", + "addresses": "128 addresses", + "problem": "Doesn't align with octet boundaries", + "solution": "Use CNAME delegation with bit notation", + "zones": ["0-25.113.0.203.in-addr.arpa", "1-25.113.0.203.in-addr.arpa"] + }, + { + "cidr": "/26", + "example": "203.0.113.64/26", + "addresses": "64 addresses", + "problem": "Quarter of /24, doesn't align with octets", + "solution": "CNAME delegation for 64-127 range", + "zones": ["64-26.113.0.203.in-addr.arpa"] + }, + { + "cidr": "/27", + "example": "203.0.113.128/27", + "addresses": "32 addresses", + "problem": "Eighth of /24, complex delegation", + "solution": "CNAME delegation with range notation", + "zones": ["128-27.113.0.203.in-addr.arpa"] + } + ], + + "practicalExamples": [ + { + "scenario": "Small Business with /24", + "network": "192.0.2.0/24", + "reverseZone": "2.0.192.in-addr.arpa", + "ptrRecords": [ + "1.2.0.192.in-addr.arpa. IN PTR mail.example.com.", + "10.2.0.192.in-addr.arpa. IN PTR web.example.com.", + "50.2.0.192.in-addr.arpa. IN PTR server1.example.com." + ], + "delegation": "ISP delegates entire /24 reverse zone to customer DNS" + }, + { + "scenario": "Medium Business with /23", + "network": "198.51.100.0/23", + "reverseZones": ["100.51.198.in-addr.arpa", "101.51.198.in-addr.arpa"], + "description": "Two /24 reverse zones needed", + "delegation": "ISP delegates both zones or uses automation" + } + ] + }, + + "ipv6Zones": { + "title": "IPv6 Reverse Zones (ip6.arpa)", + "nibbleBoundariesTitle": "Nibble Boundaries (4-bit Aligned)", + "practicalExamplesTitle": "Practical IPv6 Examples", + + "nibbleBoundaries": [ + { + "cidr": "/32", + "example": "2001:db8::/32", + "reverseZone": "8.b.d.0.1.0.0.2.ip6.arpa", + "description": "Typical RIR allocation to ISP", + "delegation": "RIR delegates to ISP" + }, + { + "cidr": "/48", + "example": "2001:db8:1234::/48", + "reverseZone": "4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa", + "description": "Typical site allocation", + "delegation": "ISP delegates to organization" + }, + { + "cidr": "/56", + "example": "2001:db8:1234:ab00::/56", + "reverseZone": "0.0.b.a.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa", + "description": "Large home or small business", + "delegation": "Common residential allocation" + }, + { + "cidr": "/64", + "example": "2001:db8:1234:5678::/64", + "reverseZone": "8.7.6.5.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa", + "description": "Single subnet", + "delegation": "Individual subnet reverse zone" + } + ], + + "practicalExamples": [ + { + "scenario": "Enterprise with /48", + "network": "2001:db8:1234::/48", + "reverseZone": "4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa", + "subZones": [ + "0.0.0.0.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)", + "1.0.0.0.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)", + "a.b.c.d.4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa (/64)" + ], + "management": "Create master zone, delegate individual /64s as needed" + } + ] + }, + + "zoneCreation": { + "title": "Creating Reverse Zones", + + "ipv4Example": { + "network": "192.0.2.0/24", + "zoneName": "2.0.192.in-addr.arpa", + "zoneFile": "$TTL 86400\n2.0.192.in-addr.arpa. IN SOA ns1.example.com. hostmaster.example.com. (\n 2024010101 ; serial\n 3600 ; refresh\n 1800 ; retry\n 1209600 ; expire\n 86400 ) ; minimum\n\n IN NS ns1.example.com.\n IN NS ns2.example.com.\n\n1 IN PTR mail.example.com.\n10 IN PTR web.example.com.\n50 IN PTR server1.example.com.\n100 IN PTR workstation.example.com.", + "explanation": [ + "Zone name is network reversed + in-addr.arpa", + "SOA record defines zone authority and parameters", + "NS records point to authoritative name servers", + "PTR records map IP to hostname (just last octet for /24)" + ] + }, + + "ipv6Example": { + "network": "2001:db8:1234::/48", + "zoneName": "4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa", + "zoneFile": "$TTL 86400\n4.3.2.1.8.b.d.0.1.0.0.2.ip6.arpa. IN SOA ns1.example.com. hostmaster.example.com. (\n 2024010101 ; serial\n 3600 ; refresh\n 1800 ; retry\n 1209600 ; expire\n 86400 ) ; minimum\n\n IN NS ns1.example.com.\n IN NS ns2.example.com.\n\n; Delegate /64 subnets\n0.0.0.0 IN NS ns1.example.com.\n0.0.0.0 IN NS ns2.example.com.\n\n1.0.0.0 IN NS ns1.example.com.\n1.0.0.0 IN NS ns2.example.com.", + "explanation": [ + "Zone name is full prefix in nibble format + ip6.arpa", + "Each hex digit becomes separate label in reverse", + "Can delegate individual /64 subnets within /48", + "Much longer zone names than IPv4" + ] + } + }, + + "delegationScenarios": { + "title": "Delegation Scenarios", + "scenarios": [ + { + "scenario": "ISP to Customer (/24)", + "delegation": "ISP adds NS records for customer's DNS servers in their reverse zone", + "customerActions": [ + "Set up DNS servers with reverse zone", + "Create PTR records for important hosts", + "Test reverse lookups work correctly" + ], + "ispActions": [ + "Add NS delegation in parent zone", + "Update WHOIS records if required", + "Verify customer DNS servers are working" + ] + }, + { + "scenario": "Organization Internal (/16 split)", + "delegation": "Large organization splits /16 into /24s for different departments", + "process": [ + "Create master zone for entire /16", + "Delegate individual /24s to department DNS servers", + "Each department manages their own PTR records" + ] + } + ] + }, + + "troubleshooting": { + "title": "Troubleshooting", + "issues": [ + { + "issue": "Reverse lookups not working", + "causes": ["Zone not delegated", "DNS server not responding", "PTR records missing"], + "diagnosis": "Use dig -x [ip] to test reverse resolution", + "solution": "Check delegation, verify DNS server config, add PTR records" + }, + { + "issue": "Mail servers rejecting email", + "causes": ["Missing PTR record for mail server IP", "PTR doesn't match HELO name"], + "diagnosis": "Check mail server logs, test PTR record", + "solution": "Create PTR record that matches mail server hostname" + }, + { + "issue": "IPv6 reverse lookups failing", + "causes": ["Complex nibble format errors", "Zone delegation issues"], + "diagnosis": "Verify zone name format, test with dig -x", + "solution": "Double-check nibble format, verify IPv6 DNS configuration" + } + ] + }, + + "bestPractices": { + "title": "Best Practices", + "practices": [ + "Always create reverse zones for your allocated IP blocks", + "Ensure PTR records match forward DNS (A/AAAA records)", + "Use consistent naming conventions for reverse records", + "Monitor reverse DNS resolution for important services", + "Automate PTR record creation/updates where possible", + "Test reverse lookups from multiple external locations", + "Keep reverse zone serial numbers updated when making changes" + ] + }, + + "quickReference": { + "title": "Quick Reference", + "zoneFormulasTitle": "Zone Name Formulas", + "essentialRecordsTitle": "Essential Records", + "keyRule": "IPv4 reverse zones reverse the octets (192.0.2.0/24 β†’ 2.0.192.in-addr.arpa). IPv6 reverse zones reverse the nibbles (2001:db8::/32 β†’ 8.b.d.0.1.0.0.2.ip6.arpa).", + + "zoneFormulas": [ + "IPv4 /24: [third].[second].[first].in-addr.arpa", + "IPv4 /16: [second].[first].in-addr.arpa", + "IPv6 /48: [nibbles-reversed].ip6.arpa", + "IPv6 /64: [more-nibbles-reversed].ip6.arpa" + ], + + "essentialRecords": [ + "SOA record (required for all zones)", + "NS records (delegation to authoritative servers)", + "PTR records (actual IP to name mappings)", + "Match PTR with forward A/AAAA records" + ] + }, + + "testingTools": { + "title": "Testing Tools", + "tools": [ + { "tool": "dig -x [ip]", "purpose": "Test reverse DNS lookup" }, + { "tool": "nslookup [ip]", "purpose": "Basic reverse lookup test" }, + { "tool": "host [ip]", "purpose": "Simple reverse resolution check" }, + { "tool": "online reverse DNS tools", "purpose": "Test from external perspective" } + ] + }, + + "labels": { + "addresses": "Addresses", + "problem": "Problem", + "solution": "Solution", + "zoneNames": "Zone Names", + "network": "Network", + "reverseZone": "Reverse Zone", + "reverseZones": "Reverse Zones", + "ptrRecords": "PTR Records", + "delegation": "Delegation", + "description": "Description", + "zoneName": "Zone Name", + "zoneFile": "Zone File", + "explanation": "Explanation", + "masterZone": "Master Zone", + "subZones": "Sub-zones", + "management": "Management", + "customerActions": "Customer Actions", + "ispActions": "ISP Actions", + "process": "Process", + "possibleCauses": "Possible Causes", + "diagnosis": "Diagnosis", + "keyRuleTitle": "Key Rule" + } + } +} diff --git a/src/lib/i18n/translations/en/reference/private-vs-public-ip.json b/src/lib/i18n/translations/en/reference/private-vs-public-ip.json new file mode 100644 index 00000000..638a00bc --- /dev/null +++ b/src/lib/i18n/translations/en/reference/private-vs-public-ip.json @@ -0,0 +1,143 @@ +{ + "title": "Private vs Public IP Addresses", + "description": "Understanding the difference between private and public IP addresses, NAT implications, and quick identification methods.", + + "sections": { + "overview": { + "title": "What's the Difference?", + "content": "Private IP addresses are used within local networks and are not routed on the public internet. Public IP addresses are globally unique and can be reached from anywhere on the internet.\n\nThe key difference is reachability: private IPs are only reachable within their local network, while public IPs are reachable from anywhere on the internet." + } + }, + + "privateRanges": { + "title": "Private IP Address Ranges (RFC 1918)", + "labels": { + "fullRange": "Full Range:", + "totalAddresses": "Total Addresses:", + "commonUse": "Common Use:", + "examples": "Examples:" + } + }, + + "publicRanges": { + "title": "Public IP Addresses", + "description": "All IP addresses not in private, reserved, or special-use ranges", + "characteristics": { + "title": "Characteristics", + "items": [ + "Globally unique and routable on the internet", + "Assigned by Regional Internet Registries (RIRs)", + "Can be reached from anywhere on the internet", + "Cost money to obtain and maintain", + "Limited supply (IPv4 exhaustion)" + ] + }, + "examples": { + "title": "Examples", + "headers": { + "publicIp": "Public IP", + "ownerService": "Owner/Service" + } + } + }, + + "natImplications": { + "title": "NAT (Network Address Translation) Implications", + + "privateToPublic": { + "title": "Private Networks Accessing Internet", + "description": "Private addresses must be translated to public addresses to reach the internet", + "process": { + "title": "Process:", + "steps": [ + "Device with private IP (192.168.1.100) wants to access internet", + "Router/NAT device translates to public IP (203.0.113.50)", + "Internet sees traffic from public IP, not private IP", + "Return traffic is translated back to private IP" + ] + }, + "benefits": { + "title": "Benefits:", + "items": [ + "Allows many devices to share one public IP", + "Provides security through address hiding", + "Conserves public IP addresses", + "Enables local network management" + ] + } + }, + + "publicToPrivate": { + "title": "Internet Accessing Private Networks", + "description": "Direct access from internet to private IPs requires special configuration", + "challenges": { + "title": "Challenges:", + "items": [ + "Private IPs are not routed on internet", + "NAT blocks unsolicited inbound connections", + "Port forwarding needed for specific services", + "VPN required for general access" + ] + }, + "solutions": { + "title": "Solutions:", + "items": [ + "Port forwarding for specific services", + "VPN for secure remote access", + "DMZ for less secure but simple access", + "Reverse proxy for web services" + ] + } + } + }, + + "identification": { + "title": "Quick Identification Methods", + "headers": { + "method": "Method", + "privateIndicator": "Private Indicator", + "publicIndicator": "Public Indicator" + } + }, + + "tools": { + "title": "Useful Tools" + }, + + "commonScenarios": { + "title": "Common Network Scenarios", + "labels": { + "setup": "Setup:", + "privateIps": "Private IPs:", + "publicIp": "Public IP:", + "natBehavior": "NAT Behavior:" + } + }, + + "troubleshooting": { + "title": "Troubleshooting Common Issues", + "labels": { + "possibleCauses": "Possible Causes:", + "diagnosis": "Diagnosis:", + "solution": "Solution:" + } + }, + + "security": { + "title": "Security Considerations" + }, + + "bestPractices": { + "title": "Best Practices" + }, + + "quickReference": { + "title": "Quick Reference", + "privateRanges": { + "title": "Private IP Ranges" + }, + "identificationTips": { + "title": "Identification Tips" + } + } +} diff --git a/src/lib/i18n/translations/en/settings.json b/src/lib/i18n/translations/en/settings.json new file mode 100644 index 00000000..b05e1ebe --- /dev/null +++ b/src/lib/i18n/translations/en/settings.json @@ -0,0 +1,94 @@ +{ + "title": "Settings", + "description": "Customize your experience with themes, layouts, and accessibility options.", + + "disabled": { + "title": "Settings Disabled", + "message": "Settings for this instance have been disabled by your administrator." + }, + + "language": { + "title": "Language" + }, + + "theme": { + "title": "Theme", + "show_more": "Show more themes", + "show_less": "Show less" + }, + + "font_scale": { + "title": "Font Scale" + }, + + "homepage_layout": { + "title": "Homepage Layout" + }, + + "top_navigation": { + "title": "Top Navigation" + }, + + "accessibility": { + "title": "Accessibility", + "show_all": "Show all a11y options", + "show_less": "Show less" + }, + + "site_branding": { + "title": "Site Branding", + "description": "Customize the site title, description, and icon.", + "site_title": "Site Title", + "site_description": "Description", + "site_icon_url": "Icon URL", + "apply": "Apply", + "reset": "Reset" + }, + + "primary_color": { + "title": "Primary Color", + "description": "Choose a primary color for the interface.", + "custom_color": "Use Custom Color", + "color_picker": "Color Picker", + "hex_code": "Hex Code", + "reset": "Reset" + }, + + "custom_css": { + "title": "Custom CSS", + "description": "Add your own CSS to customize the appearance globally.", + "placeholder": "/* Enter your custom CSS here */", + "characters": "characters", + "apply": "Apply", + "clear": "Clear" + }, + + "more_info": { + "title": "Not found what you were looking for?", + "line1": "Good news! The code is open source and easy to work with.", + "content": "Simply fork the repo, follow our dev setup instructions, make whatever changes and customizations you like, and then deploy your own instance.", + "support": "We also offer enterprise support services, where we can make custom changes for you." + }, + + "delete_data": { + "title": "Delete Data", + "caution": "Caution: This will reset all local data.", + "button": "Clear all Data" + }, + + "sync": { + "title": "Syncing Settings and Backup/Restore", + "line1": "Your settings are saved in your browser's local storage, and so they will be retained even after you quit the app.", + "content": "Since we don't require login/signup to use the app, there is currently no way to automatically sync your settings across devices. But if you're self-hosting Networking Toolbox, you can apply settings in your config, by including the following environment variables. This way, you're settings will be applied to all users across all devices.", + "export_settings": "Export Settings", + "export_styles": "Export Styles", + "env_vars_title": "Environment Variables", + "env_vars_description": "Current environment variable values. Copy and paste into your config file for self-hosted instances.", + "copy_clipboard": "Copy to Clipboard", + "copied": "Copied!", + "custom_css_title": "Custom CSS", + "custom_css_description": "Apply your custom CSS to your self-hosted instance by mounting a CSS file." + }, + + "more_settings": "More Settings" +} diff --git a/src/lib/i18n/translations/en/tools.json b/src/lib/i18n/translations/en/tools.json new file mode 100644 index 00000000..fef802be --- /dev/null +++ b/src/lib/i18n/translations/en/tools.json @@ -0,0 +1,2068 @@ +{ + "subnet_calculator": { + "title": "Subnet Calculator", + "description": "Calculate network, broadcast, and host information for any subnet.", + + "input": { + "cidr_label": "Network Address (CIDR)", + "cidr_placeholder": "192.168.1.0/24" + }, + + "sections": { + "network_info": "Network Information", + "host_info": "Host Information", + "binary_representation": "Binary Representation" + }, + + "fields": { + "network_address": "Network Address", + "broadcast_address": "Broadcast Address", + "subnet_mask": "Subnet Mask", + "wildcard_mask": "Wildcard Mask", + "total_hosts": "Total Hosts", + "usable_hosts": "Usable Hosts", + "first_host": "First Host", + "last_host": "Last Host", + "network_binary": "Network:", + "mask_binary": "Mask:", + "broadcast_binary": "Broadcast:" + }, + + "tooltips": { + "network_address": "First IP in subnet - identifies the network", + "broadcast_address": "Last IP in subnet - sends to all hosts", + "subnet_mask": "Defines network vs host portion of IP", + "wildcard_mask": "Inverse of subnet mask - used in ACLs", + "total_hosts": "All IP addresses in this subnet", + "usable_hosts": "IPs available for devices (excludes network/broadcast)", + "first_host": "First IP address available for devices", + "last_host": "Last IP address available for devices", + "network_binary": "Network address in binary format", + "mask_binary": "Subnet mask in binary format", + "broadcast_binary": "Broadcast address in binary format" + }, + + "explainer": { + "title": "Understanding Subnet Calculations", + "network_address": { + "title": "Network Address", + "description": "The first IP address in a subnet, used to identify the network itself. Hosts cannot be assigned this address as it represents the entire network segment." + }, + "broadcast_address": { + "title": "Broadcast Address", + "description": "The last IP address in a subnet, used to send messages to all devices on the network. When a packet is sent to this address, it reaches every host in the subnet." + }, + "subnet_mask": { + "title": "Subnet Mask", + "description": "Defines which portion of an IP address represents the network and which represents the host. A mask of /24 means the first 24 bits identify the network." + }, + "wildcard_mask": { + "title": "Wildcard Mask", + "description": "The inverse of a subnet mask, used in access control lists. Where the subnet mask has 1s, the wildcard has 0s, and vice versa." + }, + "usable_hosts": { + "title": "Usable Hosts", + "description": "The number of IP addresses available for devices. Always 2 less than total addresses because network and broadcast addresses are reserved." + }, + "cidr_notation": { + "title": "CIDR Notation", + "description": "Classless Inter-Domain Routing notation (e.g., /24) indicates how many bits are used for the network portion. Higher numbers mean smaller subnets with fewer hosts." + } + }, + + "tips": { + "title": "Pro Tips", + "plan_growth": "Plan for Growth: Choose subnet sizes that accommodate future expansion", + "binary_understanding": "Binary Understanding: Learning binary helps understand how subnetting works", + "common_sizes": "Common Sizes: /24 (254 hosts), /25 (126 hosts), /26 (62 hosts), /30 (2 hosts for point-to-point)", + "private_networks": "Private Networks: Use RFC 1918 addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x) for internal networks" + }, + + "actions": { + "copied": "Copied!", + "copy_network": "Copy network address to clipboard", + "copy_broadcast": "Copy broadcast address to clipboard", + "copy_network_aria": "Copy network address", + "copy_broadcast_aria": "Copy broadcast address", + "calculating": "Calculating subnet..." + }, + + "binary": { + "network_label": "Network:", + "mask_label": "Mask:", + "broadcast_label": "Broadcast:" + } + }, + + "ipv6_subnet_calculator": { + "title": "IPv6 Subnet Calculator", + "description": "Calculate IPv6 subnet information with 128-bit addressing and modern network prefix notation.", + + "sections": { + "networkConfiguration": "Network Configuration", + "commonNetworks": "Common IPv6 Networks", + "subnetInfo": "IPv6 Subnet Information", + "networkDetails": "Network Details", + "addressStructure": "IPv6 Address Structure", + "addressBreakdown": "128-bit Address Breakdown", + "calculationError": "Calculation Error" + }, + + "form": { + "networkAddressLabel": "IPv6 Network Address", + "networkAddressPlaceholder": "2001:db8::/64", + "prefixLengthLabel": "Prefix Length", + "customPrefixLength": "Custom prefix length" + }, + + "tooltips": { + "networkAddressHelp": "Enter IPv6 address with prefix (e.g., 2001:db8::/64) or address only", + "compressedNotation": "Compressed IPv6 notation using :: for consecutive zero groups", + "expandedNotation": "Full 128-bit IPv6 representation with all zero groups shown", + "subnetMask": "IPv6 subnet mask showing network portion (compressed format)", + "addressRange": "First and last assignable addresses in the subnet", + "assignableAddresses": "Number of addresses available for host assignment (excluding network/broadcast concepts)", + "reverseDns": "PTR record zone for reverse DNS lookups", + "binaryRepresentation": "128-bit binary representation showing network (1) and host (0) bits" + }, + + "presets": { + "documentation48": "Documentation /48", + "standardSubnet64": "Standard Subnet /64", + "linkLocal64": "Link-Local /64", + "loopback128": "Loopback /128", + "googleDns48": "Google DNS /48", + "multicastAllNodes": "Multicast All Nodes" + }, + + "results": { + "network": "Network", + "totalAddresses": "Total Addresses", + "networkCompressed": "Network Address (Compressed)", + "networkExpanded": "Network Address (Expanded)", + "subnetMask": "Subnet Mask", + "addressRange": "Address Range", + "assignableAddresses": "Assignable Addresses", + "reverseDnsZone": "Reverse DNS Zone", + "binaryPrefix": "Binary Prefix Representation" + }, + + "actions": { + "hide": "Hide", + "show": "Show", + "binary": "Binary" + }, + + "visualization": { + "networkPortion": "Network Portion", + "hostPortion": "Host Portion", + "bits": "bits", + "showingPortionsFor": "Showing network and host portions for" + }, + + "format": { + "digits": "digits" + } + }, + + "vlsm_calculator": { + "title": "VLSM Calculator", + "description": "Design efficient subnets with Variable Length Subnet Masking for optimal address space utilization.", + + "networkConfig": { + "title": "Network Configuration", + "networkAddress": { + "label": "Network Address", + "placeholder": "192.168.1.0" + }, + "cidrNotation": { + "label": "CIDR Notation" + } + }, + + "subnetRequirements": { + "title": "Subnet Requirements", + "addSubnet": "Add Subnet", + "subnetName": { + "placeholder": "Subnet name" + }, + "hostsNeeded": "Hosts needed:", + "description": { + "placeholder": "Description (optional)" + } + }, + + "summary": { + "title": "VLSM Summary", + "totalSubnets": "Total Subnets", + "hostsRequested": "Hosts Requested", + "hostsProvided": "Hosts Provided", + "wastedHosts": "Wasted Hosts", + "efficiency": "Efficiency", + "remainingAddresses": "Remaining Addresses" + }, + + "table": { + "title": "Subnet Allocation Table", + "columns": { + "subnet": "Subnet", + "network": "Network", + "hosts": "Hosts", + "mask": "Mask", + "efficiency": "Efficiency", + "actions": "Actions" + }, + "hostsNeeded": "{count} needed", + "hostsProvided": "{count} provided", + "hostsWasted": "{count} wasted" + }, + + "details": { + "networkAddress": { + "label": "Network Address", + "tooltip": "First IP address in the subnet - identifies the network" + }, + "broadcastAddress": { + "label": "Broadcast Address", + "tooltip": "Last IP address in the subnet - sends to all hosts" + }, + "firstUsableHost": { + "label": "First Usable Host", + "tooltip": "First IP address available for host assignment" + }, + "lastUsableHost": { + "label": "Last Usable Host", + "tooltip": "Last IP address available for host assignment" + }, + "subnetMask": { + "label": "Subnet Mask", + "tooltip": "Defines which portion of IP represents network vs host" + }, + "wildcardMask": { + "label": "Wildcard Mask", + "tooltip": "Inverse of subnet mask - used in access control lists" + }, + "binaryMask": { + "label": "Binary Mask", + "tooltip": "Binary representation of the subnet mask" + }, + "hostBits": { + "label": "Host Bits", + "tooltip": "Number of bits available for host addressing", + "value": "{count} bits" + } + }, + + "actions": { + "expandDetails": "Expand subnet details", + "collapseDetails": "Collapse subnet details", + "copyNetworkInfo": "Copy network info" + }, + + "error": { + "title": "Calculation Error" + }, + + "defaultSubnetName": "Subnet {number}" + }, + + "subnet_planner": { + "title": "Subnet Planner", + "description": "Plan and design subnet allocation for networks with optimal address space utilization.", + + "strategy": { + "title": "Allocation Strategy", + "fitBest": { + "label": "Best Fit", + "description": "Minimize address space waste" + }, + "preserveOrder": { + "label": "Preserve Order", + "description": "Allocate in the order specified" + }, + "usableHosts": { + "label": "Optimize for Usable Hosts", + "description": "Account for network and broadcast addresses" + } + }, + + "parentNetwork": { + "label": "Parent Network", + "placeholder": "e.g., 192.168.0.0/16" + }, + + "requirements": { + "title": "Subnet Requirements", + "addSubnet": "Add Subnet", + "clearAll": "Clear All", + "emptyState": "No subnet requirements defined. Add subnets to begin planning." + }, + + "examples": { + "title": "Example Scenarios", + "officeNetwork": { + "label": "Small Office Network", + "subnets": { + "sales": "Sales Department (25 hosts)", + "engineering": "Engineering Team (15 hosts)", + "servers": "Server VLAN (10 hosts)" + } + }, + "largeCorporate": { + "label": "Large Corporate Network", + "subnets": { + "hq": "Headquarters (500 hosts)", + "branchOffice": "Branch Office (100 hosts)", + "dmz": "DMZ Servers (50 hosts)", + "management": "Management Network (20 hosts)" + } + }, + "dataCenter": { + "label": "Data Center Network", + "subnets": { + "webServers": "Web Server Farm (200 hosts)", + "databaseCluster": "Database Cluster (50 hosts)", + "loadBalancers": "Load Balancers (10 hosts)", + "monitoring": "Monitoring Systems (30 hosts)" + } + } + }, + + "actions": { + "plan": "Generate Plan", + "planning": "Planning...", + "export": "Export Plan", + "copy": "Copy", + "copied": "Copied!" + }, + + "results": { + "title": "Subnet Plan", + "summary": { + "title": "Planning Summary", + "totalSubnets": "Total Subnets", + "addressesUsed": "Addresses Used", + "addressesWasted": "Addresses Wasted", + "efficiency": "Efficiency" + }, + "plan": { + "title": "Subnet Allocation Plan", + "subnet": "Subnet", + "network": "Network", + "hosts": "Hosts", + "size": "Size", + "utilization": "Utilization" + } + }, + + "errors": { + "title": "Planning Error", + "invalidNetwork": "Invalid parent network", + "insufficientSpace": "Insufficient address space", + "planningFailed": "Subnet planning failed" + } + }, + + "ptr_generator": { + "title": "PTR Record Generator", + "description": "Generate reverse DNS PTR records for IP addresses and CIDR blocks with proper zone file formatting.", + + "overview": { + "reverseDNS": { + "title": "Reverse DNS", + "content": "PTR records provide reverse DNS lookups, mapping IP addresses back to hostnames" + }, + "zoneStructure": { + "title": "Zone Structure", + "content": "IPv4 uses .in-addr.arpa zones, IPv6 uses .ip6.arpa with nibble boundaries" + }, + "zoneFiles": { + "title": "Zone Files", + "content": "Generated records can be used directly in BIND9 and other DNS server configurations" + } + }, + + "input": { + "label": "IP Address or CIDR Block", + "placeholder": "192.168.1.100 or 192.168.1.0/24", + "singleMode": "Single Address", + "cidrMode": "CIDR Block", + "help": "Enter an IP address for single PTR record or CIDR block for bulk generation", + "type": { + "label": "Input Type", + "singleIP": "Single IP Address", + "cidrBlock": "CIDR Block" + }, + "address": { + "labelSingle": "IP Address", + "placeholderSingle": "e.g., 192.168.1.100" + }, + "options": { + "generateZoneFiles": "Generate Zone Files", + "zoneFilesHint": "Include complete zone file format for DNS servers" + } + }, + + "examples": { + "title": "Quick Examples", + "singleIPv4": { + "label": "Single IPv4", + "description": "Generate PTR for single IPv4 address" + }, + "singleIPv6": { + "label": "Single IPv6", + "description": "Generate PTR for single IPv6 address" + }, + "ipv4Subnet24": { + "label": "IPv4 /24 Subnet", + "description": "Generate PTRs for entire /24 subnet" + }, + "ipv4SmallBlock": { + "label": "IPv4 /28 Small Block", + "description": "Generate PTRs for /28 block (16 addresses)" + }, + "ipv6Network": { + "label": "IPv6 /64 Network", + "description": "Generate PTRs for IPv6 network block" + }, + "largeBlock": { + "label": "Large Block (/16)", + "description": "Demonstration of large block generation" + }, + "types": { + "singleIP": "Single IP Address", + "cidrBlock": "CIDR Block" + } + }, + + "options": { + "showZoneFiles": "Show zone file format", + "showZoneFilesTooltip": "Display complete zone file content for DNS server configuration" + }, + + "actions": { + "generate": "Generate PTR Records", + "generating": "Generating...", + "copyAll": "Copy All", + "exportTxt": "Export TXT", + "exportBind": "Export BIND", + "exportJson": "Export JSON", + "copied": "Copied!", + "copyZoneFile": "Copy Zone File" + }, + + "results": { + "title": "PTR Records", + "summary": { + "totalPTRs": "Total PTR Records", + "ipv4": "IPv4 Records", + "zones": "DNS Zones", + "totalEntries": "Total Entries", + "ipv4Entries": "IPv4 Entries", + "ipv6Entries": "IPv6 Entries", + "uniqueZones": "Unique Zones" + }, + "records": { + "title": "PTR Record Details", + "ipAddress": "IP Address", + "ptrName": "PTR Record Name", + "type": "Type", + "zone": "Zone" + }, + "entries": { + "title": "PTR Entries", + "count": "({count})", + "ipAddress": "IP Address", + "ptrRecord": "PTR Record", + "reverseZone": "Reverse Zone", + "type": "Type", + "copyTooltip": "Copy PTR record" + }, + "zoneFiles": { + "title": "Zone File Contents", + "count": "({count})", + "zoneName": "Zone: {name}", + "zoneType": "Type: {type}", + "entriesCount": "{count} entries", + "copyZoneTooltip": "Copy zone file content" + } + }, + + "errors": { + "title": "Error", + "invalidInput": "Invalid input format", + "unsupportedFormat": "Unsupported IP format", + "tooManyEntries": "Too many entries (maximum {max})", + "processingError": "Error processing input" + }, + + "warnings": { + "largeBlock": "Large CIDR block will generate many records", + "performanceNote": "Generation may take some time for large blocks" + }, + + "education": { + "whatArePTRRecords": { + "title": "What are PTR Records?", + "content": "PTR records provide reverse DNS lookups, mapping IP addresses back to domain names for verification and logging purposes" + }, + "zoneStructure": { + "title": "Zone Structure", + "content": "IPv4 reverse zones use .in-addr.arpa (e.g., 1.168.192.in-addr.arpa), IPv6 uses .ip6.arpa with nibble boundaries" + }, + "zoneDelegation": { + "title": "Zone Delegation", + "content": "Reverse DNS zones must be properly delegated by your ISP or hosting provider to function correctly" + }, + "bestPractices": { + "title": "Best Practices", + "content": "Always set up reverse DNS for mail servers, use descriptive hostnames, and ensure forward/reverse DNS match" + } + } + }, + + "rp_builder": { + "title": "RP Record Builder", + "subtitle": "Create RP (Responsible Person) records to specify administrative contacts for your domains", + + "examples": { + "title": "Common Role Examples", + "roles": { + "systemAdmin": { + "name": "System Administrator", + "description": "Primary system administrator contact" + }, + "webmaster": { + "name": "Webmaster", + "description": "Website administrator contact" + }, + "security": { + "name": "Security Contact", + "description": "Security incident response contact" + }, + "dnsAdmin": { + "name": "DNS Administrator", + "description": "DNS zone administrator" + } + } + }, + + "form": { + "domain": { + "label": "Domain Name", + "placeholder": "example.com", + "help": "The domain name for this RP record", + "tooltip": "The domain name for which this RP record will be created" + }, + "converter": { + "title": "Email to Domain Name Converter", + "placeholder": "admin@example.com", + "button": "Convert", + "help": "Enter an email to automatically convert to domain name format" + }, + "mailbox": { + "label": "Mailbox Domain Name", + "placeholder": "admin.example.com.", + "help": "Domain name encoding the email address (use \".\" for no contact)", + "tooltip": "Domain name encoding the email address. Use '.' for no contact specified.", + "emailPreview": "Email:" + }, + "txt": { + "label": "TXT Domain Name", + "placeholder": "admin-info.example.com.", + "help": "Domain name where TXT record with contact info can be found (use \".\" for none)", + "tooltip": "Domain name where TXT record with additional contact information can be found. Use '.' for no additional info." + } + }, + + "output": { + "rpRecord": "Generated RP Record", + "txtRecord": "Suggested TXT Record", + "txtHelp": "This TXT record should be created at the specified domain", + "placeholder": "Fill in the required fields to generate the RP record", + "invalidFormat": "Invalid format" + }, + + "buttons": { + "copy": "Copy Records", + "copied": "Copied!", + "download": "Download", + "downloaded": "Downloaded!" + }, + + "alerts": { + "info": { + "title": "Information", + "noMailbox": "Using \".\" for mailbox means no mailbox is specified", + "noTxt": "Using \".\" for TXT means no additional text information is provided", + "txtRecord": "Remember to create the TXT record at {txtDname} with contact information" + }, + "warnings": { + "title": "Configuration Warnings", + "mailboxFqdn": "Mailbox domain name should be a fully qualified domain name", + "txtFqdn": "TXT domain name should be a fully qualified domain name or \".\"", + "mailboxDot": "Domain names in RP records should end with a dot (.) for absolute names", + "txtDot": "TXT domain name should end with a dot (.) for absolute names" + } + }, + + "info": { + "about": { + "title": "About RP Records", + "description": "RP (Responsible Person) records identify the responsible person for a domain or host. They specify both a mailbox (encoded as a domain name) and optionally point to a TXT record with additional contact information. This allows automated discovery of administrative contacts." + }, + "encoding": { + "title": "Email Encoding", + "description": "Email addresses are encoded as domain names:", + "examples": { + "simple": { + "email": "admin@example.com", + "encoded": "admin.example.com." + }, + "complex": { + "email": "user.name@example.com", + "encoded": "user\\.name.example.com." + } + }, + "note": "Dots in the local part are escaped with backslashes" + }, + "useCases": { + "title": "Common Use Cases", + "items": { + "zone": "Zone administrator contact", + "server": "Server administrator contact", + "security": "Security incident response", + "automated": "Automated contact discovery", + "compliance": "Compliance requirements" + } + }, + "bestPractices": { + "title": "Best Practices", + "items": { + "fqdn": "Always use fully qualified domain names ending with a dot", + "txtRecords": "Create corresponding TXT records with detailed contact information", + "upToDate": "Keep contact information up to date and monitored", + "rolesBased": "Consider creating role-based contacts rather than personal ones" + } + } + } + }, + + "ipv6_teredo": { + "title": "IPv6 Teredo Parser", + "description": "Parse Teredo IPv6 addresses to extract server IPv4, flags, mapped port, and client IPv4", + + "overview": { + "tunneling": { + "title": "Teredo Tunneling:", + "description": "Allows IPv6 connectivity for hosts behind IPv4 NATs by encapsulating IPv6 packets in IPv4 UDP." + }, + "format": { + "title": "Address Format:", + "description": "where components are encoded and obfuscated." + }, + "obfuscation": { + "title": "Obfuscation:", + "description": "Client IP and port are XOR'ed to prevent some NATs from interfering with the tunnel." + } + }, + + "examples": { + "title": "Quick Examples", + "microsoftTeredo": "Microsoft Teredo", + "microsoftTeredoDesc": "Microsoft Teredo server example", + "compressedForm": "Compressed Form", + "compressedFormDesc": "Same address in compressed format", + "behindNATCone": "Behind NAT (Cone)", + "behindNATConeDesc": "Client behind cone NAT", + "directConnection": "Direct Connection", + "directConnectionDesc": "Direct connection without NAT" + }, + + "input": { + "label": "Teredo IPv6 Address", + "placeholder": "Enter IPv6 Teredo address...", + "help": "Enter a Teredo IPv6 address (starts with 2001::/32)" + }, + + "results": { + "validAddress": "Valid Teredo Address", + "invalidAddress": "Invalid Address" + }, + + "breakdown": { + "title": "Address Structure", + "prefix": { + "label": "Prefix", + "description": "Teredo identifier" + }, + "server": { + "label": "Server", + "description": "IPv4:" + }, + "flags": { + "label": "Flags" + }, + "port": { + "label": "Port", + "description": "Actual:" + }, + "client": { + "label": "Client", + "description": "IPv4:" + } + }, + + "components": { + "title": "Extracted Components", + "teredoServer": { + "title": "Teredo Server", + "description": "The Teredo relay server handling this tunnel" + }, + "clientIPv4": { + "title": "Client IPv4", + "description": "The client's public IPv4 address (may be NAT address)" + }, + "clientPort": { + "title": "Client Port", + "description": "The UDP port used by the client (may be NAT-mapped)" + }, + "natType": { + "title": "NAT Detection", + "description": "Indicates the type of NAT the client is behind" + } + }, + + "natTypes": { + "cone": "Cone NAT", + "symmetric": "Symmetric NAT", + "direct": "Direct connection (no NAT)" + }, + + "explanation": { + "title": "Parsing Steps", + "step1": "1. Teredo prefix: {prefix} (identifies this as a Teredo tunnel)", + "step2": "2. Server IPv4: {hex} β†’ {ipv4}", + "step3": "3. Flags: {hex} β†’ {flags}", + "step4": "4. Client port: {hex} XOR FFFF β†’ {port}", + "step5": "5. Client IPv4: {hex} XOR FFFFFFFF β†’ {ipv4}" + }, + + "errors": { + "invalidFormat": "Invalid IPv6 address format", + "notTeredo": "Not a valid Teredo address (must start with 2001::/32)", + "invalidComponents": "Invalid address components detected" + }, + + "actions": { + "parse": "Parse", + "clear": "Clear", + "copy": "Copy", + "copied": "Copied!" + } + }, + + "ipv6_notation_converter": { + "title": "IPv6 Notation Converter", + "description": "Convert between compressed and expanded IPv6 notation formats with validation.", + "input": { + "label": "IPv6 Address", + "placeholder": "2001:db8::1", + "help": "Enter IPv6 address in any valid format", + "tooltipExpand": "Enter compressed IPv6 address to expand", + "tooltipCompress": "Enter expanded IPv6 address to compress" + }, + "output": { + "label": "Converted Address", + "expandedForm": "Expanded Form", + "compressedForm": "Compressed Form" + }, + "examples": { + "title": "Quick Examples", + "documentationPrefix": "Documentation Prefix", + "loopbackAddress": "Loopback Address", + "linkLocalAddress": "Link-Local Address", + "globalUnicast": "Global Unicast", + "ipv4MappedIPv6": "IPv4-mapped IPv6", + "multicastAddress": "Multicast Address" + }, + "actions": { + "convert": "Convert", + "copy": "Copy", + "copied": "Copied!" + }, + "errors": { + "title": "Conversion Error", + "enterAddress": "Please enter an IPv6 address", + "invalidFormat": "Invalid IPv6 address format", + "conversionFailed": "Conversion failed" + } + }, + + "ip_enumerate": { + "title": "IP Address Enumerator", + "description": "Generate all IP addresses in a CIDR block, range, or network with customizable display limits.", + "input": { + "placeholder": "192.168.1.0/28 or 10.0.0.1-10.0.0.50", + "maxDisplay": { + "label": "Maximum addresses to display" + } + }, + "options": { + "network": "Network", + "broadcast": "Broadcast" + }, + "examples": { + "title": "Quick Examples", + "smallSubnet": { + "label": "Small Subnet", + "description": "16 addresses in /28 subnet" + }, + "standardSubnet": { + "label": "Standard Subnet", + "description": "256 addresses in /24 subnet" + }, + "ipRange": { + "label": "IP Range", + "description": "Custom IP address range" + }, + "singleAddress": { + "label": "Single Address", + "description": "Single IP address" + }, + "largeBlock": { + "label": "Large Block", + "description": "Large subnet demonstration" + } + }, + "safety": { + "title": "Safety Warning", + "message": "Large address ranges may impact browser performance" + }, + "actions": { + "generating": "Generating addresses..." + } + }, + + "free_space_finder": { + "title": "Free Space Finder", + "description": "Find available address space within IP pools by analyzing allocated subnets.", + "examples": { + "title": "Example Scenarios", + "officeNetworkGaps": { + "label": "Office Network Gaps" + }, + "largePoolAnalysis": { + "label": "Large Pool Analysis" + }, + "homeNetworkSpace": { + "label": "Home Network Space" + }, + "ipv6Planning": { + "label": "IPv6 Planning" + }, + "datacenterInventory": { + "label": "Datacenter Inventory" + } + }, + "input": { + "pools": { + "label": "IP Pools", + "placeholder": "192.168.0.0/16\\n10.0.0.0/8" + }, + "allocations": { + "label": "Allocated Subnets", + "placeholder": "192.168.1.0/24\\n192.168.10.0/24" + }, + "targetPrefix": { + "label": "Target Prefix Length", + "placeholder": "24" + } + }, + "actions": { + "clearFilter": "Clear Filter" + }, + "results": { + "title": "Available Space", + "blocks": "Free Blocks", + "addresses": "Available Addresses" + }, + "visualization": { + "title": "Address Space Visualization" + } + }, + + "next_available": { + "title": "Next Available Subnet Finder", + "description": "Find the next available subnet within IP pools based on existing allocations.", + "searchCriteria": { + "title": "Search Criteria", + "byPrefix": { + "label": "By Prefix Length" + }, + "byHosts": { + "label": "By Host Count" + } + }, + "input": { + "pools": { + "label": "Available Pools", + "placeholder": "192.168.0.0/16\\n10.0.0.0/8" + }, + "allocations": { + "label": "Existing Allocations", + "placeholder": "192.168.1.0/24\\n192.168.10.0/24" + }, + "targetPrefix": { + "label": "Target Prefix Length" + }, + "policy": { + "label": "Allocation Policy", + "options": { + "firstFit": "First Fit", + "bestFit": "Best Fit" + } + }, + "maxCandidates": { + "label": "Max Candidates" + }, + "options": { + "usableHosts": { + "label": "Calculate usable hosts" + } + } + }, + "examples": { + "title": "Example Scenarios", + "officeSubnets": { + "label": "Office Subnets" + }, + "hostBasedSearch": { + "label": "Host-based Search" + }, + "multiplePools": { + "label": "Multiple Pools" + }, + "ipv6Example": { + "label": "IPv6 Example" + } + } + }, + + "nth_ip": { + "title": "Nth IP Address Calculator", + "description": "Calculate the nth IP address from network ranges using flexible syntax.", + "input": { + "title": "Network Ranges", + "label": "Input", + "placeholder": "192.168.1.0/24 @ 10\\n10.0.0.0-10.0.0.255 [50]", + "help": "Support for CIDR, ranges, and various offset syntaxes", + "globalOffset": { + "label": "Global Offset", + "placeholder": "0", + "help": "Apply offset to all calculations" + } + }, + "examples": { + "title": "Example Patterns", + "tenthFromSubnet": { + "description": "10th address from subnet" + }, + "multipleRanges": { + "description": "Multiple network ranges" + }, + "ipv6Networks": { + "description": "IPv6 network support" + }, + "largeNetworks": { + "description": "Large network handling" + }, + "firstLastIP": { + "description": "First and last IP patterns" + }, + "sequentialRanges": { + "description": "Sequential range processing" + }, + "largeIPv6": { + "description": "Large IPv6 networks" + }, + "specialUse": { + "description": "Special use addresses" + } + } + }, + + "ip_regex_generator": { + "title": "IP Address Regex Generator", + "description": "Generate regular expressions for IP address validation with customizable options.", + "modes": { + "simple": "Simple Mode", + "advanced": "Advanced Mode" + }, + "types": { + "title": "IP Address Types", + "ipv4Only": "IPv4 Only", + "ipv6Only": "IPv6 Only", + "both": "Both IPv4 & IPv6" + }, + "results": { + "title": "Generated Regex", + "patternLabel": "Regular Expression Pattern", + "edit": "Edit Pattern", + "testUrl": "Test on Regex101", + "copy": "Copy Regex" + }, + "testCases": { + "title": "Test Cases", + "editTestCases": "Edit Test Cases", + "validTitle": "Should Match", + "invalidTitle": "Should Not Match" + }, + "languageExamples": { + "title": "Language Examples" + } + }, + + "cidr_overlap": { + "title": "CIDR Overlap Checker", + "description": "Analyze overlapping IP ranges between two sets of CIDR blocks with detailed comparison.", + "examples": { + "basicOverlap": "Basic Overlap", + "noOverlap": "No Overlap", + "partialOverlap": "Partial Overlap", + "ipv6Overlap": "IPv6 Overlap" + }, + "options": { + "title": "Analysis Options", + "mergeInputs": "Merge overlapping inputs", + "mergeInputsTooltip": "Combine overlapping ranges in input sets", + "showOnlyBoolean": "Show only boolean result", + "showOnlyBooleanTooltip": "Display simple yes/no overlap results" + }, + "input": { + "setALabel": "Set A (Networks)", + "setATooltip": "First set of CIDR blocks to compare", + "setAPlaceholder": "192.168.1.0/24\\n10.0.0.0/16", + "setBLabel": "Set B (Networks)", + "setBTooltip": "Second set of CIDR blocks to compare", + "setBPlaceholder": "192.168.1.128/25\\n10.0.1.0/24", + "clearAll": "Clear All" + }, + "results": { + "title": "Overlap Analysis" + }, + "visualization": { + "title": "Network Overlap Visualization" + } + }, + + "cidr_contains": { + "title": "CIDR Containment Checker", + "description": "Check if IP ranges and CIDR blocks are contained within other ranges", + "examples": { + "basicContainment": "Basic Containment", + "mixedResults": "Mixed Results", + "partialOverlap": "Partial Overlap", + "ipv6Containment": "IPv6 Containment" + }, + "options": { + "title": "Analysis Options", + "mergeContainers": "Merge overlapping containers", + "mergeContainersTooltip": "Combine overlapping ranges in container set", + "strictEquality": "Strict equality check", + "strictEqualityTooltip": "Require exact matches for containment" + }, + "input": { + "setALabel": "Containers (Larger Networks)", + "setATooltip": "Networks that may contain others", + "setAPlaceholder": "192.168.0.0/16\\n10.0.0.0/8", + "setBLabel": "Candidates (Networks to Check)", + "setBTooltip": "Networks to check for containment", + "setBPlaceholder": "192.168.1.0/24\\n192.168.1.100/32" + }, + "results": { + "title": "Containment Results" + }, + "errors": { + "unknownError": "Unknown error occurred" + } + }, + + "reverse_zones_calculator": { + "title": "Reverse DNS Zone Calculator", + "description": "Calculate reverse DNS zone files and delegation requirements for IP address ranges.", + "examples": { + "ipv4_24": { + "label": "IPv4 /24 Network", + "description": "Standard class C network" + }, + "ipv4_16": { + "label": "IPv4 /16 Network", + "description": "Class B network with multiple zones" + }, + "ipv6_64": { + "label": "IPv6 /64 Network", + "description": "Standard IPv6 subnet" + }, + "small_block": { + "label": "Small Block (/28)", + "description": "Small IPv4 block" + } + }, + "results": { + "title": "Reverse Zone Analysis", + "zones": "Required Zones", + "delegation": "Delegation Requirements", + "totalZones": "Total Zones", + "ipv4Zones": "IPv4 Zones", + "ipv6Zones": "IPv6 Zones" + } + }, + + "ula_generator": { + "title": "ULA Generator", + "description": "Generate Unique Local IPv6 addresses with proper fc00::/7 prefix allocation.", + "form": { + "count": { + "label": "Number of addresses", + "placeholder": "1" + }, + "subnetIds": { + "label": "Subnet IDs", + "placeholder": "1, 2, 3", + "helpText": "Comma-separated subnet identifiers" + } + }, + "buttons": { + "generate": "Generate ULA Addresses" + }, + "parser": { + "title": "ULA Parser", + "description": "Parse and analyze existing ULA addresses", + "form": { + "address": { + "label": "ULA Address", + "placeholder": "fc00::/7" + } + } + } + }, + + "ipv6_zone_id": { + "title": "IPv6 Zone ID Processor", + "description": "Process IPv6 addresses with zone identifiers for link-local and multicast address scoping.", + "input": { + "label": "IPv6 Addresses", + "placeholder": "fe80::1\\nfe80::1%eth0\\nff02::1%eth0", + "help": "Enter IPv6 addresses with or without zone identifiers (%), one per line" + }, + "results": { + "title": "Zone ID Processing Results", + "input": "Input", + "address": "Address", + "zoneId": "Zone ID", + "scope": "Scope", + "recommendation": "Recommendation", + "status": "Status", + "valid": "Valid", + "invalid": "Invalid" + }, + "scope": { + "linkLocal": "Link-Local", + "siteLocal": "Site-Local", + "multicast": "Multicast", + "global": "Global", + "loopback": "Loopback", + "unspecified": "Unspecified" + }, + "recommendations": { + "zoneRequired": "Zone ID required for this scope", + "zoneOptional": "Zone ID optional", + "zoneNotNeeded": "Zone ID not needed", + "addZone": "Add zone ID for clarity" + }, + "info": { + "title": "IPv6 Zone Identifiers", + "description": "Zone identifiers specify the network interface for link-local and multicast addresses", + "whenRequired": { + "title": "When Zone IDs Are Required", + "linkLocal": { + "type": "Link-Local Addresses", + "description": "Required to specify which network interface" + }, + "multicast": { + "type": "Multicast Addresses", + "description": "Required for link-local and site-local multicast" + } + }, + "commonIdentifiers": { + "title": "Common Zone Identifiers", + "examples": "eth0, wlan0, lo0, 1, 2, 3" + } + } + }, + + "ipv6_normalize": { + "title": "IPv6 Normalizer", + "description": "Normalize IPv6 addresses to RFC 5952 canonical representation.", + "input": { + "label": "IPv6 Addresses", + "placeholder": "2001:0db8:0000:0000:0000:ff00:0042:8329", + "help": "Enter IPv6 addresses to normalize, one per line" + }, + "rfc": { + "title": "RFC 5952 Normalization", + "description": "Canonical IPv6 address representation rules", + "rules": { + "lowercase": "Use lowercase hexadecimal", + "leadingZeros": "Remove leading zeros", + "compression": "Use :: for longest zero sequence", + "singleZero": "Represent single zero fields as 0" + } + } + }, + + "eui64": { + "title": "EUI-64 IPv6 Generator", + "description": "Generate IPv6 addresses using EUI-64 interface identifiers from MAC addresses.", + "input": { + "title": "Configuration", + "addresses": { + "label": "MAC Addresses", + "placeholder": "00:1A:2B:3C:4D:5E", + "help": "Enter MAC addresses, one per line" + }, + "globalPrefix": { + "label": "Global IPv6 Prefix", + "placeholder": "2001:db8::/64", + "help": "IPv6 network prefix for address generation" + } + }, + "info": { + "title": "EUI-64 Process", + "description": "Extended Unique Identifier conversion process for IPv6", + "steps": { + "split": "Split MAC address in half", + "insert": "Insert FF:FE in the middle", + "flip": "Flip universal/local bit", + "result": "Combine with IPv6 prefix" + } + } + }, + + "cidr_overlap": { + "examples": { + "title": "Example Networks", + "basicOverlap": "Basic network overlap test", + "noOverlap": "Non-overlapping networks", + "partialOverlap": "Partial network overlap", + "ipv6Overlap": "IPv6 network overlap" + }, + "options": { + "title": "Options", + "mergeInputs": "Merge overlapping inputs", + "mergeInputsTooltip": "Automatically merge overlapping input networks", + "showOnlyBoolean": "Show only boolean result", + "showOnlyBooleanTooltip": "Display only overlap status without detailed analysis" + }, + "input": { + "setALabel": "Network Set A", + "setATooltip": "Enter CIDR networks for set A, one per line", + "setAPlaceholder": "192.168.1.0/24\n10.0.0.0/16", + "setBLabel": "Network Set B", + "setBTooltip": "Enter CIDR networks for set B, one per line", + "setBPlaceholder": "172.16.0.0/12\n192.168.0.0/16", + "clearAll": "Clear All" + } + }, + + "ip-validator": { + "title": "IP Address Validator", + "description": "Validate IPv4 and IPv6 addresses with detailed error analysis and format checking", + "examples": { + "validIPv4": "Valid IPv4 address", + "validIPv6": "Valid IPv6 address", + "ipv4LeadingZeros": "IPv4 with leading zeros", + "ipv4OctetTooLarge": "IPv4 octet out of range", + "ipv6MultipleDoubleColon": "IPv6 with multiple double colons", + "ipv6TooManyGroups": "IPv6 with too many groups" + }, + "form": { + "enterLabel": "Enter IP Address", + "placeholder": "e.g., 192.168.1.1 or 2001:db8::1", + "hint": "Supports IPv4 (192.168.1.1), IPv6 (2001:db8::1), and various formats" + }, + "testCases": { + "title": "Quick Test Cases" + }, + "results": { + "valid": "Valid", + "invalid": "Invalid", + "ipAddress": "IP Address", + "normalized": "Normalized:", + "issuesFound": "Issues Found ({{count}})", + "warnings": "Warnings ({{count}})", + "addressDetails": "Address Details", + "type": "Type:", + "scope": "Scope:", + "routing": "Routing:", + "private": "Private", + "public": "Public", + "compressed": "Compressed:", + "embeddedIPv4": "Embedded IPv4:", + "zoneId": "Zone ID:", + "additionalInfo": "Additional Information" + }, + "errors": { + "general": { + "unknownFormat": "Unknown IP address format - must be IPv4 or IPv6" + }, + "ipv4": { + "mustContainDots": "IPv4 addresses must contain dots (.) to separate octets", + "wrongOctetCount": "IPv4 addresses must have exactly {{expected}} octets, found {{found}}", + "octetEmpty": "Octet {{number}} is empty", + "nonNumericCharacters": "Octet {{number}} contains non-numeric characters: '{{part}}'", + "leadingZeros": "Octet {{number}} has leading zeros: '{{part}}'", + "outOfRange": "Octet {{number}} value {{value}} is out of range (0-255)" + }, + "ipv6": { + "invalidCharacter": "Invalid character '{{char}}' in IPv6 address", + "multipleDoubleColon": "IPv6 addresses can only contain one '::' compression sequence", + "invalidFormat": "Invalid IPv6 format", + "tooManyGroups": "IPv6 addresses must have exactly 8 groups, found {{count}}", + "embeddedIPv4Error": "Invalid embedded IPv4 address: {{error}}", + "emptyGroup": "IPv6 group cannot be empty (except when using ::)", + "groupTooLong": "IPv6 group '{{group}}' is too long (maximum 4 characters)", + "invalidHexadecimal": "IPv6 group '{{group}}' contains invalid hexadecimal characters" + } + }, + "warnings": { + "ipv4": { + "networkAddress": "This appears to be a network address (host portion is 0)", + "broadcastAddress": "This appears to be a broadcast address (host portion is all 1s)" + }, + "ipv6": { + "siteLocalDeprecated": "Site-local addresses are deprecated (RFC 3879)", + "documentationAddress": "This is a documentation address (not for production use)" + } + }, + "addressTypes": { + "ipv4": { + "loopback": "Loopback", + "privateA": "Private (Class A)", + "privateB": "Private (Class B)", + "privateC": "Private (Class C)", + "linkLocal": "Link-Local (APIPA)", + "multicast": "Multicast (Class D)", + "reserved": "Reserved (Class E)", + "networkAddress": "Network Address", + "broadcastAddress": "Broadcast Address", + "public": "Public" + }, + "ipv6": { + "loopback": "Loopback", + "unspecified": "Unspecified", + "linkLocal": "Link-Local", + "siteLocal": "Site-Local (Deprecated)", + "uniqueLocal": "Unique Local", + "multicast": "Multicast", + "documentation": "Documentation", + "globalUnicast": "Global Unicast", + "reserved": "Reserved" + } + }, + "scopes": { + "host": "Host", + "privateNetwork": "Private Network", + "linkLocal": "Link-Local", + "multicast": "Multicast", + "reserved": "Reserved", + "specialUse": "Special Use", + "network": "Network", + "internet": "Internet", + "siteLocal": "Site-Local", + "documentation": "Documentation" + }, + "info": { + "loopbackCommunications": "Used for local loopback communications", + "rfc1918Private": "RFC 1918 private address space", + "apipa": "Automatic Private IP Addressing", + "multicastCommunications": "Used for multicast communications", + "reservedFuture": "Reserved for future use", + "thisNetwork": "\"This network\" address", + "publiclyRoutable": "Publicly routable address", + "zoneIdSpecified": "Zone ID specified: %{{zoneId}}", + "embeddedIPv4": "Contains embedded IPv4 address: {{ipv4}}", + "ipv6Loopback": "IPv6 loopback address (::1)", + "ipv6Unspecified": "IPv6 unspecified address (::)", + "ipv6LinkLocal": "IPv6 link-local address", + "deprecatedSiteLocal": "Deprecated site-local address", + "rfc4193UniqueLocal": "RFC 4193 Unique Local Address", + "ipv6Multicast": "IPv6 multicast address", + "rfc3849Documentation": "RFC 3849 documentation address", + "globallyRoutable": "Globally routable IPv6 address", + "reservedAddressSpace": "Reserved address space", + "standardCompressed": "Standard compressed form: {{form}}" + }, + "education": { + "howToTell": { + "title": "How to Tell if an IP Address is Valid", + "description": "Valid IP addresses follow specific rules. For IPv4, you need exactly four numbers (0-255) separated by dots, like 192.168.1.1. For IPv6, you need eight groups of hex digits separated by colons, though you can compress consecutive zeros with :: (like 2001:db8::1). The validator checks these rules and tells you exactly what's wrong when something doesn't match." + }, + "whatHappens": { + "title": "What Happens When Addresses Are Invalid", + "description": "Invalid IP addresses cause real problems. Your router might reject them, network connections fail, or software crashes. Common mistakes include typos like \"192.168.1.256\" (256 is too big), missing parts like \"192.168.1\", or extra zeros like \"192.168.01.01\". This tool catches these errors before they break your network setup." + }, + "whyWarnings": { + "title": "Why Some Addresses Have Warnings", + "description": "Some valid addresses come with warnings because they have special meanings. For example, addresses ending in .0 are usually network addresses, and ones ending in .255 are broadcast addresses. Private addresses like 192.168.x.x won't work on the internet. The tool explains what each address type means so you know if it's right for your use case." + } + } + }, + + "ip_enumerate": { + "title": "IP Address Enumerator", + "description": "Generate and list all IP addresses in a network range or CIDR block", + "examples": { + "title": "Example Networks", + "smallSubnet": { + "label": "/30 Network (4 IPs)", + "description": "Small point-to-point subnet with minimal hosts" + }, + "standardSubnet": { + "label": "/24 Network (256 IPs)", + "description": "Standard Class C subnet for typical LAN" + }, + "ipRange": { + "label": "IP Range", + "description": "Continuous range of IP addresses" + }, + "singleAddress": { + "label": "Single IP", + "description": "Individual IP address (/32)" + }, + "largeBlock": { + "label": "/16 Network (65,536 IPs)", + "description": "Large enterprise network block" + } + }, + "input": { + "placeholder": "Enter CIDR network or IP range (e.g., 192.168.1.0/24)", + "maxDisplay": { + "label": "Maximum IPs to display" + }, + "options": { + "network": "Include network address", + "broadcast": "Include broadcast address" + } + }, + "safety": { + "title": "Large Network Warning", + "message": "This network contains a large number of IP addresses. Displaying all IPs may affect performance." + }, + "actions": { + "generating": "Generating IP list..." + } + }, + + "reverse_zones_calculator": { + "title": "Reverse Zone Calculator", + "description": "Calculate DNS reverse zones for IP ranges", + "examples": { + "title": "Common Examples", + "ipv4_20": { + "label": "/20 Network (16 Class C)", + "description": "Large enterprise network requiring multiple reverse zones" + }, + "ipv4_24": { + "label": "/24 Network (Class C)", + "description": "Standard LAN subnet with single reverse zone" + }, + "ipv4_16": { + "label": "/16 Network (Class B)", + "description": "Large network requiring Class B reverse delegation" + }, + "ipv4_28": { + "label": "/28 Network (16 IPs)", + "description": "Small subnet with single reverse zone delegation" + }, + "ipv6_48": { + "label": "/48 IPv6 Network", + "description": "Standard IPv6 site prefix with hierarchical zones" + }, + "ipv6_64": { + "label": "/64 IPv6 Network", + "description": "Standard IPv6 subnet for single network segment" + } + }, + "delegationTypes": { + "classC": "Class C Delegation" + }, + "overview": { + "zoneBoundaries": { + "title": "Zone Boundaries", + "description": "Understanding where reverse DNS zones align with network boundaries" + }, + "delegation": { + "title": "DNS Delegation", + "description": "How reverse DNS authority is delegated to authoritative servers" + }, + "optimization": { + "title": "Zone Optimization", + "description": "Best practices for efficient reverse DNS zone management" + } + }, + "input": { + "label": "IP Network", + "placeholder": "Enter CIDR network (e.g., 192.168.1.0/24)" + }, + "results": { + "title": "Reverse Zone Results", + "analysis": { + "totalZones": "Total Reverse Zones", + "delegationType": "Delegation Type" + }, + "zones": { + "title": "Reverse Zones" + }, + "configuration": { + "title": "DNS Configuration", + "bindConfig": "BIND Zone Configuration", + "delegationCommands": "Delegation Commands" + } + } + }, + + "tlsa_generator": { + "title": "TLSA Record Generator", + "subtitle": "Generate TLSA DNS records for certificate pinning", + "service": { + "title": "Service Information", + "domain": { + "label": "Domain Name", + "placeholder": "example.com", + "tooltip": "The domain name for which to create the TLSA record" + }, + "port": { + "label": "Port Number", + "placeholder": "443", + "tooltip": "The port number for the service (e.g., 443 for HTTPS)" + }, + "protocol": { + "label": "Protocol", + "tooltip": "The transport protocol (TCP or UDP)", + "options": { + "tcp": "TCP", + "udp": "UDP" + } + } + }, + "parameters": { + "title": "TLSA Parameters", + "usage": { + "label": "Certificate Usage", + "tooltip": "Certificate usage - how the certificate should be used for authentication", + "options": { + "caConstraint": "0 - CA Constraint", + "serviceConstraint": "1 - Service Certificate Constraint", + "trustAnchor": "2 - Trust Anchor Assertion", + "domainIssued": "3 - Domain-Issued Certificate" + }, + "descriptions": { + "caConstraint": "CA Constraint - Certificate must be issued by the CA represented in the TLSA record", + "serviceConstraint": "Service Certificate Constraint - Certificate must match the one in the TLSA record", + "trustAnchor": "Trust Anchor Assertion - Certificate must chain to the CA in the TLSA record", + "domainIssued": "Domain-Issued Certificate - Certificate must match the one specified (most common)" + } + }, + "selector": { + "label": "Selector", + "tooltip": "Which part of the certificate to use", + "options": { + "fullCert": "0 - Full Certificate", + "spki": "1 - Subject Public Key Info" + }, + "descriptions": { + "fullCert": "Full Certificate - Use the entire certificate", + "spki": "Subject Public Key Info - Use only the public key portion (recommended)" + } + }, + "matching": { + "label": "Matching Type", + "tooltip": "How to process the certificate data", + "options": { + "exact": "0 - Exact Match", + "sha256": "1 - SHA-256 Hash", + "sha512": "2 - SHA-512 Hash" + }, + "descriptions": { + "exact": "Exact Match - Use the certificate/key data as-is (not recommended)", + "sha256": "SHA-256 Hash - Use SHA-256 hash of the certificate/key (recommended)", + "sha512": "SHA-512 Hash - Use SHA-512 hash of the certificate/key" + } + } + }, + "certificate": { + "title": "Certificate Data", + "pemOption": "Certificate/Public Key (PEM)", + "hashOption": "Hash Value", + "pemLabel": "Certificate/Public Key", + "pemTooltip": "Paste the PEM-encoded certificate or public key", + "hashLabel": "Hash Value", + "hashTooltip": "Enter the {{type}} hash value", + "exact": "exact", + "generateButton": "Generate Hash", + "generateTooltip": "Generate hash from certificate (demo mode)" + }, + "output": { + "title": "Generated TLSA Record", + "generated_hash_placeholder": "Generated hash would appear here (requires certificate parsing)", + "copy": { + "button": "Copy Record", + "copied": "Copied!", + "tooltip": "Copy TLSA record to clipboard" + }, + "export": { + "button": "Export Zone", + "downloaded": "Downloaded", + "tooltip": "Download as zone file" + }, + "breakdown": { + "title": "Record Breakdown:", + "service": "Service:", + "usage": "Usage:", + "selector": "Selector:", + "matching": "Matching:" + } + }, + "validation": { + "title": "Validation", + "status": { + "label": "Status:", + "valid": "Valid", + "invalid": "Invalid" + }, + "readyToDeploy": "TLSA record is valid and ready to deploy!", + "errors": { + "domainRequired": "Domain is required", + "portRange": "Port must be between 1 and 65535", + "certificateRequired": "Certificate data is required", + "hashRequired": "Hash value is required", + "hexOnly": "Hash must contain only hexadecimal characters" + }, + "warnings": { + "domainTld": "Domain should include TLD (e.g., .com, .org)", + "pemFormat": "Certificate should be in PEM format", + "sha256Length": "SHA-256 hash should be exactly 64 hexadecimal characters", + "sha512Length": "SHA-512 hash should be exactly 128 hexadecimal characters", + "usageTypes02": "Usage types 0 and 2 require careful CA certificate management", + "selectorFull": "Full certificate selector (0) is less flexible than SPKI selector (1)", + "exactMatch": "Exact match (0) is not recommended - use SHA-256 (1) or SHA-512 (2)" + } + }, + "security": { + "title": "Security Best Practices", + "tips": { + "usage3": "Use usage type 3 (Domain-Issued Certificate) for most scenarios", + "selectorSpki": "Prefer selector 1 (SPKI) over selector 0 (full certificate) for flexibility", + "hashTypes": "Use SHA-256 (1) or SHA-512 (2) matching types, avoid exact match (0)", + "multipleCerts": "Pin multiple certificates to avoid service disruption during certificate rotation", + "testRecords": "Test TLSA records with DANE validation tools before deployment" + } + }, + "examples": { + "title": "Example Configurations", + "https": { + "name": "HTTPS Certificate Pin", + "description": "Pin a specific certificate for HTTPS" + }, + "smtp": { + "name": "SMTP TLS Certificate", + "description": "DANE for email server TLS" + }, + "ca": { + "name": "CA Trust Anchor", + "description": "Trust anchor for certificate authority" + }, + "config": { + "port": "Port", + "usage": "Usage", + "selector": "Selector", + "type": "Type" + } + } + }, + + "edns_size_estimator": { + "title": "EDNS Size Estimator", + "subtitle": "Estimate DNS message size and UDP fragmentation risk with EDNS buffer recommendations", + "overview": { + "sizeEstimation": { + "title": "Size Estimation:", + "description": "Calculate total DNS message size including headers and record data." + }, + "fragmentationRisk": { + "title": "Fragmentation Risk:", + "description": "Assess likelihood of UDP packet fragmentation and delivery issues." + }, + "ednsRecommendations": { + "title": "EDNS Recommendations:", + "description": "Get buffer size recommendations for optimal DNS performance." + } + }, + "examples": { + "title": "Response Examples", + "recordCount": "{{count}} record{{count === 1 ? '' : 's'}}", + "simple": { + "name": "Simple A Record", + "description": "Basic single A record response" + }, + "multiple": { + "name": "Multiple A Records", + "description": "Load-balanced web servers" + }, + "txt": { + "name": "Long TXT Records", + "description": "SPF and DMARC policies" + }, + "mx": { + "name": "MX Records", + "description": "Mail server configuration" + }, + "large": { + "name": "Large Response", + "description": "Many A records causing fragmentation risk" + } + }, + "config": { + "title": "Query Configuration", + "includeQuery": { + "label": "Include query section in estimate", + "tooltip": "Include the DNS query section in size calculations. Queries add ~20-50 bytes depending on name length." + }, + "queryName": { + "label": "Query Name", + "tooltip": "The domain name being queried. Longer names result in larger message sizes." + }, + "queryType": { + "label": "Query Type", + "tooltip": "The type of DNS record being requested (A, AAAA, MX, etc.)." + } + }, + "records": { + "title": "Response Records", + "addRecord": "Add Record", + "empty": "No records added. Click \"Add Record\" to start building your DNS response.", + "removeTooltip": "Remove this record", + "fields": { + "name": "Name", + "type": "Type", + "value": "Value", + "valuePlaceholder": "Record value", + "priority": "Priority", + "ttl": "TTL" + } + }, + "results": { + "title": "Size Analysis", + "copyText": "DNS Message Size Estimate:\nTotal Size: {{totalSize}} bytes\nUDP Safe: {{udpSafe}}\nFragmentation Risk: {{fragmentationRisk}}", + "copySummary": "Copy Summary", + "yes": "Yes", + "no": "No", + "unknown": "unknown", + "udpSafe": "UDP Safe", + "requiresEdns": "Requires EDNS0", + "sizeBreakdown": { + "title": "Size Breakdown", + "dnsHeader": "DNS Header", + "recordsData": "Records Data", + "totalSize": "Total Size" + }, + "fragmentation": { + "title": "Fragmentation Analysis", + "risk": "RISK", + "riskLevels": { + "low": "LOW", + "medium": "MEDIUM", + "high": "HIGH" + }, + "thresholds": { + "classic": "≀ 512 bytes (Classic DNS)", + "safe": "≀ 1232 bytes (Safe for most networks)", + "edns": "≀ 4096 bytes (Common EDNS buffer)" + } + }, + "recommendations": { + "title": "Recommendations", + "tcp": "Consider using TCP for queries expecting large responses", + "fragment": "Some networks may fragment packets - monitor delivery success", + "edns0": "EDNS0 support required - advertise appropriate buffer size" + } + }, + "education": { + "udpLimitations": { + "title": "UDP Limitations", + "description": "Classic DNS over UDP is limited to 512 bytes. Larger responses require EDNS0 extension to advertise bigger buffer sizes. Without EDNS0, servers must truncate responses." + }, + "fragmentationIssues": { + "title": "Fragmentation Issues", + "description": "UDP packets larger than ~1232 bytes may be fragmented by network devices. Fragmented packets are more likely to be dropped, causing DNS resolution failures." + }, + "ednsBufferSizes": { + "title": "EDNS Buffer Sizes", + "description": "Common EDNS buffer sizes are 1232, 4096, and 8192 bytes. Larger buffers allow bigger responses but increase fragmentation risk. Choose based on your network environment." + }, + "optimizationStrategies": { + "title": "Optimization Strategies", + "description": "Minimize record sizes with shorter names and values. Use separate queries for large responses. Consider TCP for consistently large responses like DNSSEC-signed zones." + } + } + }, + + "idn-punycode-converter": { + "title": "IDN Punycode Converter", + "description": "Convert between internationalized domain names and Punycode", + "examples": { + "german": "German (Umlaut)", + "japanese": "Japanese (Hiragana)", + "russian": "Russian (Cyrillic)", + "arabic": "Arabic Script", + "korean": "Korean (Hangul)", + "greek": "Greek Script" + }, + "modes": { + "unicodeToPunycode": "Unicode to Punycode", + "punycodeToUnicode": "Punycode to Unicode" + }, + "conversion": { + "error": "Conversion error" + } + }, + + "loc_builder": { + "title": "LOC Record Builder", + "description": "Convert latitude/longitude coordinates ↔ DNS LOC records for geographic positioning", + "modes": { + "coordsToLoc": "Coordinates β†’ LOC", + "locToCoords": "LOC β†’ Coordinates" + }, + "examples": { + "title": "City Examples", + "sanFrancisco": { + "label": "San Francisco" + }, + "newYork": { + "label": "New York" + }, + "london": { + "label": "London" + }, + "tokyo": { + "label": "Tokyo" + }, + "sydney": { + "label": "Sydney" + } + }, + "form": { + "domain": { + "label": "Domain Name", + "placeholder": "example.com", + "tooltip": "Domain name for the LOC record" + }, + "locString": { + "label": "LOC Record String", + "placeholder": "example.com. IN LOC 37 46 29.000 N 122 25 10.000 W 10.00m 1m 10000m 10m", + "tooltip": "Paste existing LOC record to parse" + }, + "latitude": { + "label": "Latitude *", + "placeholder": "37.7749", + "tooltip": "Latitude in decimal degrees (-90 to 90)" + }, + "longitude": { + "label": "Longitude *", + "placeholder": "-122.4194", + "tooltip": "Longitude in decimal degrees (-180 to 180)" + }, + "altitude": { + "label": "Altitude (m) *", + "placeholder": "10", + "tooltip": "Altitude in meters above sea level" + }, + "size": { + "label": "Size (m)", + "placeholder": "1", + "tooltip": "Size/diameter of the location in meters" + }, + "horizontalPrecision": { + "label": "H. Precision (m)", + "placeholder": "10000", + "tooltip": "Horizontal precision in meters" + }, + "verticalPrecision": { + "label": "V. Precision (m)", + "placeholder": "10", + "tooltip": "Vertical precision in meters" + } + }, + "output": { + "parsedCoords": "Parsed Coordinates", + "generatedLoc": "Generated LOC Record", + "placeholder": "Fill in the required fields to generate the LOC record" + }, + "actions": { + "copyRecord": "Copy Record" + }, + "about": { + "title": "About LOC Records" + } + }, + + "mx_planner": { + "title": "MX Record Planner", + "description": "Plan and generate MX (Mail Exchange) records with proper priority configuration" + }, + + "naptr_builder": { + "title": "NAPTR Record Builder", + "description": "Generate NAPTR (Name Authority Pointer) records for advanced DNS resolution" + }, + + "ptr_sweep_planner": { + "title": "PTR Sweep Planner", + "description": "Analyze PTR record coverage for network blocks and identify missing or extra records", + + "overview": { + "coverageAnalysis": { + "title": "Coverage Analysis", + "description": "Compare expected PTR records for a CIDR block against actual existing records." + }, + "patternMatching": { + "title": "Pattern Matching", + "description": "Validate existing PTR records against regex naming patterns for compliance." + }, + "gapAnalysis": { + "title": "Gap Analysis", + "description": "Identify missing PTRs, extra PTRs, and generate remediation plans." + } + }, + + "examples": { + "title": "Quick Examples", + "partialCoverage": { + "label": "Partial Coverage", + "description": "Network with some missing PTRs" + }, + "mixedNaming": { + "label": "Mixed Naming", + "description": "Check pattern compliance" + }, + "ipv6Network": { + "label": "IPv6 Network", + "description": "IPv6 PTR coverage analysis" + } + }, + + "form": { + "cidrBlock": { + "label": "CIDR Block to Analyze", + "placeholder": "192.168.1.0/24 or 2001:db8::/64", + "tooltip": "Enter the CIDR block to analyze PTR coverage for" + }, + "existingPtrs": { + "label": "Existing PTR Records", + "placeholder": "100.1.168.192.in-addr.arpa\n101.1.168.192.in-addr.arpa\n...", + "tooltip": "Paste existing PTR record names, one per line" + }, + "namingPattern": { + "label": "Naming Pattern (Optional)", + "placeholder": ".*\\.example\\.com\\.$", + "tooltip": "Optional regex pattern to validate PTR target naming" + } + }, + + "patternHelp": { + "title": "Common Patterns", + "anyExample": "Any hostname ending in .example.com.", + "hostCorp": "Hostnames starting with \"host-\" in corp.com", + "ipBased": "IP-based hostnames in net.example.com", + "serverWorkstation": "Names starting with \"server-\" or \"workstation-\"" + }, + + "results": { + "title": "Coverage Analysis Results", + "coverage": "{{percentage}}% Coverage", + "copyList": "Copy List", + + "stats": { + "expectedPtrs": "Expected PTRs", + "foundPtrs": "Found PTRs", + "missingPtrs": "Missing PTRs", + "extraPtrs": "Extra PTRs", + "patternMatches": "Pattern Matches" + }, + + "sections": { + "missingPtrs": { + "title": "Missing PTR Records ({{count}})", + "truncated": "... and {{count}} more missing records" + }, + "extraPtrs": { + "title": "Extra PTR Records ({{count}})", + "truncated": "... and {{count}} more extra records" + } + }, + + "actions": { + "title": "Recommended Actions", + "createMissing": { + "title": "Create Missing PTR Records", + "description": "Add {{count}} missing PTR records to your reverse zone files.", + "copyZoneLines": "Copy Zone Lines" + }, + "reviewExtra": { + "title": "Review Extra Records", + "description": "Review {{count}} extra PTR records that don't correspond to addresses in this CIDR block." + }, + "fixPattern": { + "title": "Fix Naming Pattern Violations", + "description": "{{count}} existing PTR records don't match the naming pattern." + }, + "excellentCoverage": { + "title": "Excellent Coverage!", + "description": "Your reverse DNS coverage is excellent with {{percentage}}% completeness." + } + } + }, + + "error": { + "title": "Analysis Error", + "checkInput": "Check your input", + "help": { + "cidr": "CIDR notation: 192.168.1.0/24, 2001:db8::/64", + "ptrRecords": "PTR records: One per line, proper format", + "pattern": "Pattern: Valid JavaScript regex syntax" + } + }, + + "education": { + "ptrCoverage": { + "title": "PTR Coverage Planning", + "description": "PTR coverage analysis helps identify gaps in reverse DNS configuration. Complete coverage ensures all IPs in your network blocks have proper reverse DNS entries for troubleshooting and compliance requirements." + }, + "namingPattern": { + "title": "Naming Pattern Validation", + "description": "Use regex patterns to enforce consistent hostname naming conventions. Patterns like .*\\.corp\\.example\\.com\\.$ ensure all PTR records point to properly formatted hostnames within your domain structure." + }, + "commonIssues": { + "title": "Common PTR Issues", + "description": "Missing PTRs can cause mail delivery problems and failed reverse lookups. Extra PTRs may indicate outdated records or configuration drift. Regular PTR sweeps help maintain DNS hygiene and network documentation accuracy." + }, + "remediation": { + "title": "Remediation Best Practices", + "description": "Create missing PTRs in batches, verify forward/reverse consistency (A/AAAA records), and establish monitoring to detect future gaps. Use descriptive hostnames that include network or service information for easier troubleshooting." + } + } + } +} diff --git a/src/lib/i18n/translations/en/tools/caa-builder.json b/src/lib/i18n/translations/en/tools/caa-builder.json new file mode 100644 index 00000000..9c0a858f --- /dev/null +++ b/src/lib/i18n/translations/en/tools/caa-builder.json @@ -0,0 +1,119 @@ +{ + "title": "CAA Record Builder", + "description": "Build CAA (Certificate Authority Authorization) records to control which CAs can issue certificates for your domain.", + + "domain": { + "title": "Domain Configuration", + "label": "Domain:", + "tooltip": "Domain to create CAA records for", + "placeholder": "example.com" + }, + + "records": { + "title": "CAA Records", + "addButtons": { + "issue": { + "label": "Issue", + "tooltip": "Add certificate issuance authorization" + }, + "issuewild": { + "label": "Wildcard", + "tooltip": "Add wildcard certificate issuance authorization" + }, + "iodef": { + "label": "Contact", + "tooltip": "Add incident reporting contact" + } + }, + "removeTooltip": "Remove this record" + }, + + "tags": { + "issue": "Authorize certificate issuance for this domain", + "issuewild": "Authorize wildcard certificate issuance for this domain", + "iodef": "Contact information for certificate abuse reports" + }, + + "flags": { + "nonCritical": "Flag 0 (Non-critical)", + "critical": "Flag 128 (Critical)", + "descriptions": { + "0": "Non-critical flag - unknown tags can be ignored", + "128": "Critical flag - unknown tags must cause rejection" + } + }, + + "placeholders": { + "issue": "letsencrypt.org or ; (to deny all)", + "issuewild": "letsencrypt.org or ; (to deny all)", + "iodef": "security@example.com or https://example.com/security" + }, + + "caShortcuts": { + "label": "Common CAs:", + "denyAll": "Deny All", + "denyAllTooltip": "Deny all certificate issuance" + }, + + "output": { + "title": "Generated CAA Records", + "copyButton": "Copy", + "copyTooltip": "Copy all CAA records to clipboard", + "copied": "Copied!", + "exportButton": "Export", + "exportTooltip": "Download as zone file", + "downloaded": "Downloaded!", + "noRecords": "Enable and configure CAA records to see output" + }, + + "validation": { + "title": "Policy Validation", + "activeRecordsLabel": "Active Records:", + "statusLabel": "Status:", + "valid": "Valid", + "invalid": "Invalid", + "success": "CAA configuration is valid and ready to deploy!", + + "errors": { + "domainRequired": "Domain is required", + "invalidIodefEmail": "Invalid iodef email format - use \"mailto:user@domain.com\" or \"user@domain.com\"" + }, + + "warnings": { + "domainNoTLD": "Domain should include TLD (e.g., .com, .org)", + "noRecordsEnabled": "No CAA records enabled - this will not provide any protection", + "noIssueRecords": "No issue or issuewild records - certificates can be issued by any CA", + "wildcardWithoutBase": "Wildcard authorization without base domain authorization may cause issues", + "denyAll": "Both issue and issuewild set to \";\" - this will block ALL certificate issuance", + "iodefURLFormat": "iodef URL should start with http:// or https://", + "duplicateRecords": "Duplicate CAA records found: {duplicates}" + } + }, + + "securityGuide": { + "title": "Security Tips", + "tips": [ + "Start with monitoring: Add iodef records first to receive notifications", + "Use specific CAs: Only authorize certificate authorities you actually use", + "Include wildcards: Add issuewild records if you use wildcard certificates", + "Monitor regularly: Check iodef notifications for unauthorized issuance attempts", + "Test thoroughly: Verify legitimate certificate renewals still work after deployment" + ] + }, + + "examples": { + "title": "Example Configurations", + "letsEncryptOnly": { + "name": "Let's Encrypt Only", + "description": "Allow only Let's Encrypt certificates" + }, + "multipleCAs": { + "name": "Multiple CAs", + "description": "Allow certificates from multiple providers" + }, + "noCertificates": { + "name": "No Certificates", + "description": "Block all certificate issuance" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-alignment.json b/src/lib/i18n/translations/en/tools/cidr-alignment.json new file mode 100644 index 00000000..a03a1c6a --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-alignment.json @@ -0,0 +1,71 @@ +{ + "title": "CIDR Boundary Alignment", + "description": "Check if IP addresses, ranges, and CIDR blocks align to specific prefix boundaries", + + "examples": { + "title": "Quick Examples", + "basicIPv4": "Basic IPv4 Alignment", + "mixedIPTypes": "Mixed IP Types", + "subnetAggregation": "Subnet Aggregation Check", + "networkConsolidation": "Network Consolidation", + "vlanAlignment": "VLAN Alignment Check", + "pointToPointLinks": "Point-to-Point Links" + }, + + "input": { + "networkInputsTitle": "Network Inputs", + "networkInputsTooltip": "Enter IP addresses, CIDR blocks, or ranges to check alignment", + "inputsLabel": "IP Addresses, CIDRs, or Ranges", + "inputsTooltip": "Enter one per line: CIDR blocks, IP ranges, or individual IP addresses", + "inputsPlaceholder": "192.168.1.0/24\n10.0.0.0-10.0.0.255\n172.16.1.5\n2001:db8::/32", + "inputHelp": "Enter one per line: CIDR blocks (192.168.1.0/24), IP ranges (10.0.0.1-10.0.0.100), or single IPs (172.16.1.5)", + "targetPrefixLabel": "Target Prefix Length", + "targetPrefixTooltip": "The prefix length boundary to check alignment against (e.g., 24 for /24 boundaries)", + "targetPrefixPlaceholder": "24", + "targetPrefixHelp": "Prefix length to check alignment against (0-32 for IPv4, 0-128 for IPv6)", + "targetPreview": "Target: /{targetPrefix}" + }, + + "validation": { + "mustBeValidNumber": "Target prefix length must be a valid number", + "outOfRange": "Target prefix length must be between 0 and 128", + "ipv4OutOfRange": "Target prefix length cannot exceed 32 for IPv4 addresses", + "ipv6OutOfRange": "Target prefix length cannot exceed 128 for IPv6 addresses", + "notPractical": "Target prefix length of 0 is not practical for alignment checking" + }, + + "loading": "Checking alignment...", + + "results": { + "errorsTitle": "Errors", + "summaryTitle": "Alignment Summary", + "summaryTooltip": "Overview of alignment results across all inputs", + "totalInputsLabel": "Total Inputs", + "totalInputsTooltip": "Total number of network inputs processed", + "alignedLabel": "Aligned", + "alignedTooltip": "Networks that align to the target prefix boundary", + "misalignedLabel": "Misaligned", + "misalignedTooltip": "Networks that do not align to the target prefix boundary", + "alignmentRateLabel": "Alignment Rate", + "alignmentRateTooltip": "Percentage of inputs that align to the target boundary", + "checksTitle": "Alignment Checks", + "checksTooltip": "Detailed results for each network input", + "exportCSV": "Export CSV", + "exportJSON": "Export JSON" + }, + + "check": { + "alignedTo": "Aligned to /{targetPrefix}", + "notAlignedTo": "Not aligned to /{targetPrefix}", + "alignedCIDRLabel": "Aligned CIDR:", + "alignedCIDRTooltip": "The CIDR block that properly aligns to the target prefix boundary", + "copyAlignedCIDR": "Copy aligned CIDR to clipboard", + "reasonLabel": "Reason:", + "reasonTooltip": "Explanation of why this input aligns or doesn't align", + "suggestionsLabel": "Suggestions:", + "suggestionsTooltip": "Alternative CIDR configurations that would align to the target boundary", + "copySuggestedCIDR": "Copy suggested CIDR to clipboard", + "efficiency": "Efficiency: {efficiency}%", + "efficiencyTooltip": "Address space utilization efficiency of this suggestion" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-allocator.json b/src/lib/i18n/translations/en/tools/cidr-allocator.json new file mode 100644 index 00000000..217c995c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-allocator.json @@ -0,0 +1,77 @@ +{ + "title": "CIDR Allocator", + "description": "Pack requested subnet sizes into pools using intelligent bin-packing algorithms", + + "examples": { + "title": "Quick Examples", + "officeNetwork": "Office Network Planning", + "multiPool": "Multi-Pool Allocation", + "densePacking": "Dense Packing Challenge", + "campusVlan": "Campus VLAN Allocation", + "cloudInfrastructure": "Cloud Infrastructure", + "ispCustomer": "ISP Customer Allocation", + "algorithm": "Algorithm: {algorithm}" + }, + + "algorithm": { + "title": "Allocation Algorithm", + "titleTooltip": "Choose between first-fit (faster) and best-fit (more efficient packing) algorithms", + "firstFit": { + "title": "First-Fit", + "description": "Fast allocation - uses the first available block that fits (good for speed)" + }, + "bestFit": { + "title": "Best-Fit", + "description": "Optimal packing - uses the smallest available block that fits (reduces fragmentation)" + } + }, + + "input": { + "poolsLabel": "Available Pools", + "poolsTooltip": "Available network pools - one CIDR block per line", + "poolsPlaceholder": "192.168.0.0/16\n10.0.0.0/20", + "requestsLabel": "Subnet Requests", + "requestsTooltip": "Subnet requests in format '/24 - Description' - one per line", + "requestsPlaceholder": "/24 - Main Office\n/26 - Servers\n/28 - Management" + }, + + "results": { + "title": "Allocation Results", + "titleTooltip": "Summary of subnet allocation requests and pool utilization", + "copyAll": "Copy All", + "copied": "Copied!" + }, + + "summary": { + "allocated": "Allocated", + "failed": "Failed", + "efficiency": "Efficiency", + "totalPoolSpace": "Total Pool Space:", + "allocatedSpace": "Allocated:", + "remaining": "Remaining:", + "addresses": "{count} addresses" + }, + + "allocations": { + "title": "Subnet Allocations", + "titleTooltip": "Individual subnet allocation results with assigned CIDR blocks", + "allocated": "Allocated", + "failed": "Failed", + "in": "in", + "noSuitableBlock": "No suitable block found with proper alignment" + }, + + "pools": { + "title": "Pool Utilization", + "titleTooltip": "Detailed breakdown of how address space was used in each pool", + "used": "{percent}% used", + "allocatedSubnets": "Allocated Subnets", + "availableSpace": "Available Space", + "moreBlocks": "+{count} more blocks" + }, + + "errors": { + "title": "Allocation Error", + "unknown": "Unknown error occurred" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-compare.json b/src/lib/i18n/translations/en/tools/cidr-compare.json new file mode 100644 index 00000000..80329517 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-compare.json @@ -0,0 +1,63 @@ +{ + "title": "CIDR Compare", + "description": "Compare two lists of networks to identify changes for auditing", + + "examples": { + "title": "Quick Examples", + "networkAddition": "Network Addition", + "networkAdditionDesc": "Added 172.16.0.0/24", + "networkRemoval": "Network Removal", + "networkRemovalDesc": "Removed 172.16.0.0/12", + "mixedChanges": "Mixed Changes", + "mixedChangesDesc": "Swapped subnets", + "vlanReconfig": "VLAN Reconfiguration", + "vlanReconfigDesc": "Replaced VLANs 20,30 with 25,35", + "networkConsolidation": "Network Consolidation", + "networkConsolidationDesc": "Merged 4 /24s into 1 /22", + "branchOfficeMigration": "Branch Office Migration", + "branchOfficeMigrationDesc": "Migrated 172.16.x.x to 10.10.x.x" + }, + + "input": { + "networkListsHeading": "Network Lists", + "networkListsTooltip": "Compare two network lists to identify additions, removals, and unchanged items", + "swapButton": "Swap", + "swapTooltip": "Swap List A and List B", + "listALabel": "List A (Before)", + "listATooltip": "Original or 'before' state - CIDR blocks, IP ranges, or individual IPs", + "listAPlaceholder": "192.168.0.0/16\n10.0.0.0/8\n172.16.1.0-172.16.1.255", + "listBLabel": "List B (After)", + "listBTooltip": "Updated or 'after' state - CIDR blocks, IP ranges, or individual IPs", + "listBPlaceholder": "192.168.0.0/16\n10.0.0.0/8\n192.168.100.0/24" + }, + + "summary": { + "title": "Comparison Summary", + "titleTooltip": "Overview of changes between the two network lists", + "addedLabel": "Added", + "removedLabel": "Removed", + "unchangedLabel": "Unchanged", + "listATotal": "List A: {count} items", + "listBTotal": "List B: {count} items" + }, + + "categories": { + "addedTitle": "Added Networks ({count})", + "addedTooltip": "Networks present in List B but not in List A", + "removedTitle": "Removed Networks ({count})", + "removedTooltip": "Networks present in List A but not in List B", + "unchangedTitle": "Unchanged Networks ({count})", + "unchangedTooltip": "Networks present in both List A and List B" + }, + + "empty": { + "noAdded": "No networks added", + "noRemoved": "No networks removed", + "noUnchanged": "No networks remained unchanged" + }, + + "error": { + "title": "Comparison Error", + "unknown": "Unknown error occurred" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-contains.json b/src/lib/i18n/translations/en/tools/cidr-contains.json new file mode 100644 index 00000000..4777a575 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-contains.json @@ -0,0 +1,84 @@ +{ + "title": "CIDR Containment Checker", + "description": "Check if IP ranges and CIDR blocks are contained within other ranges", + + "examples": { + "title": "Quick Examples", + "basicContainment": "Basic Containment", + "mixedResults": "Mixed Results", + "partialOverlap": "Partial Overlap", + "ipv6Containment": "IPv6 Containment" + }, + + "options": { + "title": "Options", + "mergeContainers": "Merge/normalize containers first", + "mergeContainersTooltip": "Combine overlapping ranges in set A before checking containment", + "strictEquality": "Strict equality counts as contain", + "strictEqualityTooltip": "Treat exact matches as 'equal' instead of 'inside'" + }, + + "input": { + "setALabel": "Set A (Containers)", + "setATooltip": "The containing set - these ranges may contain items from Set B", + "setAPlaceholder": "192.168.0.0/16\n10.0.0.0/8", + "setBLabel": "Set B (Candidates)", + "setBTooltip": "Items to check for containment within Set A", + "setBPlaceholder": "192.168.1.0/24\n172.16.0.0/24", + "clearAll": "Clear All" + }, + + "results": { + "errorsTitle": "Parse Errors", + "analysisTitle": "Containment Analysis", + "exportCSV": "CSV", + "exportJSON": "JSON", + "resultsTitle": "Containment Results" + }, + + "errors": { + "unknownError": "Unknown error occurred" + }, + + "stats": { + "containersLabel": "Containers (A)", + "itemsCount": "{count} items", + "addressesCount": "{count} addresses", + "candidatesLabel": "Candidates (B)", + "checkedForContainment": "checked for containment", + "insideLabel": "Inside", + "fullyContained": "fully contained", + "equalLabel": "Equal", + "exactMatches": "exact matches", + "partialLabel": "Partial", + "partialOverlap": "partial overlap", + "outsideLabel": "Outside", + "noOverlap": "no overlap" + }, + + "table": { + "candidateColumn": "Candidate", + "statusColumn": "Status", + "coverageColumn": "Coverage", + "containersColumn": "Containers", + "gapsColumn": "Gaps", + "noDash": "-" + }, + + "status": { + "inside": "Inside", + "equal": "Equal", + "partial": "Partial", + "outside": "Outside" + }, + + "visualization": { + "title": "Containment Visualization", + "candidateRange": "Candidate Range", + "containerCoverage": "Container Coverage", + "uncoveredGaps": "Uncovered Gaps", + "candidateTooltip": "Candidate{label}\nSize: {size}{cidr}", + "containerTooltip": "Container{label}\nSize: {size}{cidr}", + "gapTooltip": "Gap{label}\nSize: {size}{cidr}" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-deaggregate.json b/src/lib/i18n/translations/en/tools/cidr-deaggregate.json new file mode 100644 index 00000000..8d6b153d --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-deaggregate.json @@ -0,0 +1,64 @@ +{ + "title": "CIDR Deaggregate", + "description": "Decompose CIDR blocks and ranges into uniform target prefix subnets", + + "examples": { + "title": "Quick Examples", + "break22to24": "Break /22 into /24s", + "decomposeRangeTo28": "Decompose Range to /28s", + "multipleBlocksTo26": "Multiple Blocks to /26s", + "enterpriseCampusTo25": "Enterprise Campus to /25s", + "dataCenterRacksTo29": "Data Center Racks to /29s", + "serviceProviderTo30": "Service Provider to /30s", + "targetPreview": "Target: /{targetPrefix}" + }, + + "input": { + "networksLabel": "Input Networks/Ranges", + "networksTooltip": "Enter CIDR blocks, IP ranges, or individual IPs - one per line", + "networksPlaceholder": "192.168.0.0/22\n10.0.0.0-10.0.255.255\n172.16.1.1", + "targetPrefixLabel": "Target Prefix Length", + "targetPrefixTooltip": "Target prefix length for uniform decomposition (e.g., 24 for /24 subnets)", + "prefixHint": "/{prefix}", + "subnetSize": "Each /{prefix} subnet = {count} addresses" + }, + + "results": { + "title": "Deaggregated Subnets", + "titleTooltip": "Generated uniform subnets from input networks and ranges", + "subnetsCount": "{count} subnets", + "addressesCount": "{count} addresses", + "copyAll": "Copy All", + "copied": "Copied!" + }, + + "summary": { + "inputLabel": "Input:", + "inputTooltip": "Original networks, ranges, and addresses provided", + "inputValue": "{count} items, {addresses} addresses", + "outputLabel": "Output:", + "outputTooltip": "Uniform subnets generated from input", + "outputValue": "{count} /{prefix} subnets, {addresses} addresses", + "noteLabel": "Note:", + "noteTooltip": "Address count difference due to subnet boundary alignment", + "expanded": "Expanded by {count} addresses (due to alignment to /{prefix} boundaries)", + "reduced": "Reduced by {count} addresses (due to alignment to /{prefix} boundaries)" + }, + + "subnet": { + "addressesCount": "{count} addresses", + "sizeClass": "{count}Γ—/16", + "sizeClassSmall": "{count}Γ—/24", + "copyAria": "Copy CIDR block" + }, + + "empty": { + "title": "No Subnets Generated", + "message": "The target prefix length may be too large for the input networks, or the input is empty." + }, + + "error": { + "title": "Deaggregation Error", + "unknown": "Unknown error occurred" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-diff.json b/src/lib/i18n/translations/en/tools/cidr-diff.json new file mode 100644 index 00000000..0b21886e --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-diff.json @@ -0,0 +1,74 @@ +{ + "title": "CIDR Set Difference", + "description": "Calculate the difference between two sets of IP addresses and CIDR blocks (A - B)", + + "alignmentMode": { + "title": "Alignment Mode", + "minimal": { + "label": "Minimal", + "description": "Generate the most efficient CIDR blocks" + }, + "constrained": { + "label": "Constrained", + "description": "Align to specific prefix boundaries", + "prefixLabel": "Constrained prefix length", + "prefixTooltip": "Force alignment to this prefix boundary" + } + }, + + "input": { + "setALabel": "Set A (Base)", + "setATooltip": "The base set of IP addresses, CIDR blocks, or ranges", + "setAPlaceholder": "192.168.1.0/24\n10.0.0.0-10.0.0.100", + "setBLabel": "Set B (Subtract)", + "setBTooltip": "The set to subtract from Set A (can be empty)", + "setBPlaceholder": "192.168.1.128/25\n10.0.0.50-10.0.0.75", + "clearTooltip": "Clear both input sets" + }, + + "examples": { + "title": "Quick Examples", + "tooltip": "Click any example to load it into the input fields", + "basicIPv4": "Basic IPv4 Subtraction", + "multipleRanges": "Multiple Ranges", + "ipv6Example": "IPv6 Example", + "mixedOperations": "Mixed Operations" + }, + + "results": { + "errorsTitle": "Parse Errors", + "title": "Difference Results (A - B)", + "exportText": "Text", + "exportJSON": "JSON", + "noResultsTitle": "No Results", + "noResultsMessage": "The difference A - B resulted in an empty set. Set B completely contains or covers Set A." + }, + + "stats": { + "setALabel": "Set A (Input)", + "itemsCount": "{count} items", + "addressesCount": "{count} addresses", + "setBLabel": "Set B (Subtract)", + "resultLabel": "Result (A - B)", + "cidrsCount": "{count} CIDRs", + "efficiencyLabel": "Efficiency", + "efficiencyValue": "{percent}%", + "removedCount": "{count} removed" + }, + + "visualization": { + "title": "Set Operation Visualization", + "tooltip": "Visual representation showing the relationship between sets A, B, and the result", + "setALegend": "Set A (Base)", + "setBLegend": "Set B (Subtract)", + "resultLegend": "Result (A - B)", + "setATooltip": "Set A\nRange: {startIP} - {endIP}\nSize: {size}{cidr}", + "setBTooltip": "Set B\nRange: {startIP} - {endIP}\nSize: {size}{cidr}", + "resultTooltip": "Set result\nRange: {startIP} - {endIP}\nSize: {size}{cidr}" + }, + + "output": { + "ipv4Title": "IPv4 Results ({count})", + "ipv6Title": "IPv6 Results ({count})" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-overlap.json b/src/lib/i18n/translations/en/tools/cidr-overlap.json new file mode 100644 index 00000000..5d005bba --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-overlap.json @@ -0,0 +1,67 @@ +{ + "title": "CIDR Overlap Checker", + "description": "Find overlapping address ranges between two sets of networks", + + "examples": { + "title": "Quick Examples", + "basicOverlap": "Basic Overlap", + "noOverlap": "No Overlap", + "partialOverlap": "Partial Overlap", + "ipv6Overlap": "IPv6 Overlap" + }, + + "options": { + "title": "Options", + "mergeInputs": "Merge overlapping inputs first", + "mergeInputsTooltip": "Combine overlapping ranges within each set before comparison", + "showOnlyBoolean": "Show only boolean result", + "showOnlyBooleanTooltip": "Display just yes/no overlap instead of detailed intersection blocks" + }, + + "input": { + "setALabel": "Set A", + "setATooltip": "First set of IP addresses, CIDR blocks, or ranges", + "setAPlaceholder": "192.168.1.0/24\n10.0.0.0-10.0.0.100", + "setBLabel": "Set B", + "setBTooltip": "Second set of IP addresses, CIDR blocks, or ranges", + "setBPlaceholder": "192.168.1.128/25\n10.0.0.50-10.0.0.150", + "clearAll": "Clear All" + }, + + "status": { + "overlapDetected": "Overlap Detected", + "noOverlap": "No Overlap", + "overlapMessage": "Sets A and B have overlapping address ranges ({percent}% of smaller set)", + "noOverlapMessage": "Sets A and B do not share any common address ranges", + "errorTitle": "Parse Errors" + }, + + "results": { + "title": "Intersection Results (A ∩ B)", + "copyText": "Text", + "copyJSON": "JSON", + "setALabel": "Set A", + "setBLabel": "Set B", + "intersectionLabel": "Intersection", + "overlapLabel": "Overlap", + "itemsCount": "{count} items", + "cidrsCount": "{count} CIDRs", + "addressesCount": "{count} addresses", + "ofSmallerSet": "of smaller set" + }, + + "visualization": { + "title": "Overlap Visualization", + "setALegend": "Set A", + "setBLegend": "Set B", + "intersectionLegend": "Intersection (A ∩ B)", + "setALabel": "Set A", + "setBLabel": "Set B", + "intersectionLabel": "A ∩ B" + }, + + "intersection": { + "ipv4Title": "IPv4 Intersection ({count})", + "ipv6Title": "IPv6 Intersection ({count})" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-splitter.json b/src/lib/i18n/translations/en/tools/cidr-splitter.json new file mode 100644 index 00000000..1379ee13 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-splitter.json @@ -0,0 +1,67 @@ +{ + "title": "CIDR Subnet Splitter", + "description": "Split a network into equal child subnets by count or target prefix length.", + + "modes": { + "title": "Split Mode", + "byCount": { + "label": "By Count", + "description": "Split into N equal subnets" + }, + "byPrefix": { + "label": "By Prefix", + "description": "Split to target prefix length" + } + }, + + "input": { + "parentNetworkTitle": "Parent Network", + "parentCIDRLabel": "Parent CIDR block", + "parentCIDRTooltip": "Enter IPv4 or IPv6 network in CIDR notation (e.g., 192.168.1.0/24)", + "parentCIDRPlaceholder": "192.168.1.0/24", + "clearInputTooltip": "Clear input", + "subnetCountLabel": "Number of subnets", + "subnetCountTooltip": "How many equal subnets to create (will be rounded to nearest power of 2)", + "targetPrefixLabel": "Target prefix length", + "targetPrefixTooltip": "The prefix length for child subnets (must be larger than parent prefix)" + }, + + "examples": { + "title": "Quick Examples", + "split24To4": "Split /24 β†’ 4 subnets", + "split16To20": "Split /16 β†’ /20", + "splitIPv6_48To16": "IPv6 /48 β†’ 16 subnets", + "splitIPv6_32To40": "IPv6 /32 β†’ /40", + "splitToCount": "Split into {count} subnets", + "splitToPrefix": "Split to /{prefix} prefix" + }, + + "results": { + "title": "Split Results", + "copyAllCIDRs": "Copy All CIDRs", + "errorTitle": "Split Error", + "parentNetworkLabel": "Parent Network", + "parentNetworkTooltip": "The original network that was split", + "childSubnetsLabel": "Child Subnets", + "childSubnetsTooltip": "Number of child subnets created", + "childPrefixLabel": "Child Prefix", + "childPrefixTooltip": "Prefix length of each child subnet", + "addressesPerChildLabel": "Addresses per Child", + "addressesPerChildTooltip": "Total IP addresses in each child subnet", + "utilizationLabel": "Utilization", + "utilizationTooltip": "Percentage of parent network's address space used" + }, + + "visualization": { + "title": "Address Space Visualization", + "subnetTooltip": "{cidr}\nRange: {network} - {broadcast}\nHosts: {totalHosts}" + }, + + "subnets": { + "title": "Child Subnets", + "networkLabel": "Network:", + "broadcastLabel": "Broadcast:", + "usableLabel": "Usable:", + "hostsLabel": "Hosts:" + } +} diff --git a/src/lib/i18n/translations/en/tools/cidr-summarizer.json b/src/lib/i18n/translations/en/tools/cidr-summarizer.json new file mode 100644 index 00000000..f0fadf43 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/cidr-summarizer.json @@ -0,0 +1,69 @@ +{ + "title": "CIDR Summarization Tool", + "description": "Convert mixed IP addresses, CIDR blocks, and ranges into optimized CIDR prefixes with separate IPv4/IPv6 results.", + + "modes": { + "title": "Summarization Mode", + "exactMerge": { + "label": "Exact Merge", + "description": "Merge overlapping ranges exactly without additional aggregation" + }, + "minimalCover": { + "label": "Minimal Cover", + "description": "Find the smallest set of CIDR blocks that covers all inputs" + } + }, + + "examples": { + "title": "Quick Examples", + "mixedIPv4v6": "Mixed IPv4/IPv6", + "overlappingRanges": "Overlapping Ranges", + "singleIPs": "Single IPs", + "modeComparison": "Mode Comparison", + "complexMix": "Complex Mix" + }, + + "input": { + "title": "Input Data", + "label": "Enter IP addresses, CIDR blocks, or ranges (one per line)", + "tooltip": "Supports: single IPs (192.168.1.1), CIDR blocks (10.0.0.0/8), ranges (172.16.0.1-172.16.0.100)", + "placeholder": "192.168.1.0/24\n10.0.0.1-10.0.0.10\n2001:db8::/32", + "clearTooltip": "Clear input" + }, + + "results": { + "errorsTitle": "Parsing Errors", + "title": "Summarization Results", + "copyAllTooltip": "Copy all IPv4 and IPv6 results to clipboard", + "copyAll": "Copy All", + "ipv4Title": "IPv4 CIDR Blocks ({count})", + "ipv4Tooltip": "Copy all IPv4 CIDR blocks to clipboard", + "ipv6Title": "IPv6 CIDR Blocks ({count})", + "ipv6Tooltip": "Copy all IPv6 CIDR blocks to clipboard", + "copyCIDRTooltip": "Copy this CIDR block to clipboard" + }, + + "stats": { + "title": "Summarization Statistics", + "originalIPv4Items": { + "label": "Original IPv4 Items", + "tooltip": "Number of individual IPv4 items in the original input" + }, + "summarizedIPv4Blocks": { + "label": "Summarized IPv4 Blocks", + "tooltip": "Number of CIDR blocks after IPv4 summarization" + }, + "originalIPv6Items": { + "label": "Original IPv6 Items", + "tooltip": "Number of individual IPv6 items in the original input" + }, + "summarizedIPv6Blocks": { + "label": "Summarized IPv6 Blocks", + "tooltip": "Number of CIDR blocks after IPv6 summarization" + }, + "totalAddressesCovered": { + "label": "Total Addresses Covered", + "tooltip": "Total number of individual IP addresses covered by all summarized blocks" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/clientid-option61.json b/src/lib/i18n/translations/en/tools/clientid-option61.json new file mode 100644 index 00000000..bf0ae27b --- /dev/null +++ b/src/lib/i18n/translations/en/tools/clientid-option61.json @@ -0,0 +1,108 @@ +{ + "title": "DHCPv4 Client Identifier (Option 61)", + "subtitle": "Build and decode DHCPv4 Client Identifier (Option 61) with hardware type + MAC address or arbitrary opaque data per RFC 2132.", + + "nav": { + "build": "Build", + "decode": "Decode" + }, + + "build": { + "title": "Build Client Identifier", + "helpText": "Configure DHCPv4 Client Identifier for device identification", + + "mode": { + "label": "Mode", + "hardware": "Hardware Type + MAC Address", + "opaque": "Opaque Data (Text or Hex)" + }, + + "hardwareType": { + "label": "Hardware Type", + "options": { + "ethernet": "Ethernet (1)", + "experimentalEthernet": "Experimental Ethernet (2)", + "ieee802": "IEEE 802 (6)", + "arcnet": "ARCNET (7)", + "frameRelay": "Frame Relay (15)", + "atm": "ATM (16)", + "hdlc": "HDLC (17)", + "fibreChannel": "Fibre Channel (18)", + "ieee1394": "IEEE 1394 (24)", + "infiniband": "InfiniBand (32)" + } + }, + + "macAddress": { + "label": "MAC Address", + "placeholder": "00:0c:29:4f:a3:d2", + "helpText": "Hardware address in any common format" + }, + + "dataFormat": { + "label": "Data Format", + "text": "Text (ASCII)", + "hex": "Hexadecimal" + }, + + "opaqueData": { + "labelHex": "Hex Data", + "labelText": "Text Data", + "placeholderHex": "0123456789abcdef", + "placeholderText": "client-device-001", + "helpTextHex": "Hexadecimal string (even length)", + "helpTextText": "Plain text identifier" + } + }, + + "decode": { + "title": "Decode Client Identifier", + "helpText": "Decode hex-encoded Client Identifier back to fields", + + "hexData": { + "label": "Hex Data", + "placeholder": "01000c294fa3d2", + "helpText": "Paste hex-encoded Client Identifier to decode" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "buildTitle": "Generated Client Identifier", + "decodeTitle": "Decoded Client Identifier", + + "mode": "Mode:", + "modeHardware": "Hardware Type + MAC", + "modeOpaque": "Opaque Data", + "detectedMode": "Detected Mode:", + "length": "Length:", + "lengthBytes": "{length} bytes", + + "outputs": { + "hexadecimal": "Hexadecimal", + "wireFormat": "Wire Format (Spaced)" + }, + + "breakdown": "Breakdown", + + "configs": { + "iscDhcpd": "ISC DHCPd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration" + }, + + "fields": { + "hardwareType": "Hardware Type", + "hardwareTypeUnknown": "Unknown", + "macAddress": "MAC Address", + "opaqueData": "Opaque Data" + } + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-duid-generator.json b/src/lib/i18n/translations/en/tools/dhcp-duid-generator.json new file mode 100644 index 00000000..51b578de --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-duid-generator.json @@ -0,0 +1,95 @@ +{ + "title": "DUID Generator", + "subtitle": "Generate DHCP Unique Identifier (DUID) for DHCPv6 clients per RFC 8415. Supports DUID-LLT, DUID-EN, DUID-LL, and DUID-UUID types with configuration export.", + + "exampleDescription": "{type} configuration example", + + "input": { + "title": "DUID Configuration", + "helpText": "Configure DHCP Unique Identifier for DHCPv6 client identification", + "duidType": { + "label": "DUID Type", + "options": { + "duidLlt": "DUID-LLT (Type 1) - Link-layer address + time", + "duidEn": "DUID-EN (Type 2) - Enterprise number", + "duidLl": "DUID-LL (Type 3) - Link-layer address", + "duidUuid": "DUID-UUID (Type 4) - UUID" + } + }, + "macAddress": { + "label": "MAC Address", + "placeholder": "00:1A:2B:3C:4D:5E or 001A2B3C4D5E", + "hint": "Enter MAC address in any common format" + }, + "hardwareType": { + "label": "Hardware Type", + "options": { + "ethernet": "Ethernet (1)", + "experimentalEthernet": "Experimental Ethernet (2)", + "ieee802": "IEEE 802 (6)", + "arcnet": "ARCNET (7)", + "frameRelay": "Frame Relay (15)", + "atm": "ATM (16)", + "hdlc": "HDLC (17)", + "fibreChannel": "Fibre Channel (18)", + "ieee1394": "IEEE 1394 (24)", + "infiniband": "InfiniBand (32)" + } + }, + "timestamp": { + "label": "Timestamp (seconds since Jan 1, 2000 UTC)", + "placeholder": "Leave empty for current time", + "hint": "Current: {timestamp} seconds since epoch", + "useCurrentTooltip": "Use current timestamp", + "clearTooltip": "Clear timestamp" + }, + "enterpriseNumber": { + "label": "Enterprise Number (IANA)", + "placeholder": "e.g., 9 for Cisco, 311 for Microsoft", + "hint": "IANA Private Enterprise Number" + }, + "enterpriseIdentifier": { + "label": "Enterprise Identifier (hex)", + "placeholder": "e.g., 0123456789abcdef", + "hint": "Custom identifier in hexadecimal format" + }, + "uuid": { + "label": "UUID", + "placeholder": "e.g., 550e8400-e29b-41d4-a716-446655440000", + "hint": "Standard UUID format (with or without hyphens)" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Generated DUID", + "summary": { + "type": "Type:", + "typeCode": "(Type {code})", + "totalLength": "Total Length:", + "bytes": "{length} bytes" + }, + "hexEncoded": { + "title": "Hex Encoded DUID" + }, + "wireFormat": { + "title": "Wire Format (Spaced)" + }, + "breakdown": { + "title": "DUID Breakdown" + } + }, + + "config": { + "keaDhcp6": "Kea DHCPv6 Configuration", + "iscDhcpd": "ISC DHCPd Configuration" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-fingerprinting.json b/src/lib/i18n/translations/en/tools/dhcp-fingerprinting.json new file mode 100644 index 00000000..237526d7 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-fingerprinting.json @@ -0,0 +1,122 @@ +{ + "title": "DHCP Fingerprinting Database", + "subtitle": "Identify devices based on their DHCP fingerprints using Parameter Request List (Option 55) and Vendor Class Identifier (Option 60). Database contains {count} known fingerprints from common devices, operating systems, and IoT equipment.", + + "tabs": { + "lookup": "Fingerprint Lookup", + "reverse": "Device Search" + }, + + "examples": { + "windows": { + "label": "Windows 10/11", + "description": "Modern Windows desktop" + }, + "macOS": { + "label": "macOS/iOS", + "description": "Apple device" + }, + "android": { + "label": "Android", + "description": "Android smartphone" + }, + "linux": { + "label": "Linux (dhclient)", + "description": "Linux with ISC dhclient" + }, + "ciscoPhone": { + "label": "Cisco IP Phone", + "description": "Cisco VoIP device" + }, + "raspberryPi": { + "label": "Raspberry Pi", + "description": "Raspberry Pi OS (Debian)" + }, + "samsungTV": { + "label": "Samsung Smart TV", + "description": "Smart TV device" + } + }, + + "lookup": { + "title": "Device Fingerprint Lookup", + "parameterList": { + "label": "Parameter Request List (Option 55)", + "placeholder": "e.g., 1,3,6,15 or 0103060f or 1 3 6 15", + "hint": "Enter as comma-separated, hex, or space-separated numbers" + }, + "vendorClass": { + "label": "Vendor Class Identifier (Option 60) - Optional", + "placeholder": "e.g., MSFT, dhcpcd, Cisco", + "hint": "Helps improve match accuracy" + }, + "error": "Error:", + "requestedOptions": { + "title": "Requested DHCP Options", + "parameterList": "Parameter List:", + "hexEncoded": "Hex Encoded:", + "unknown": "Unknown" + }, + "securityWarnings": { + "title": "Security Warnings" + }, + "optionAnalysis": { + "title": "Option Analysis", + "unusualTitle": "Unusual Options Detected", + "unusualDescription": "These options may indicate vendor-specific configurations:", + "missingTitle": "Missing Options (vs. Best Match)", + "missingDescription": "Options present in the best match but not in your fingerprint:" + }, + "matches": { + "title": "Matching Devices ({count})", + "exportJSON": "Export JSON", + "exportCSV": "Export CSV", + "confidence": "{level} confidence", + "matchScore": "{score}%", + "matchLabel": "Match", + "osLabel": "OS:", + "matchedOnLabel": "Matched On:", + "descriptionLabel": "Description:", + "knownParamsLabel": "Known Parameters:" + }, + "noMatches": { + "title": "No Matches Found", + "description": "The provided fingerprint doesn't match any known devices in the database. This could be:", + "reasons": { + "custom": "A custom DHCP client configuration", + "uncommon": "An uncommon device or operating system", + "modified": "A device with a modified DHCP request list" + }, + "hint": "Try adding the Vendor Class Identifier if available." + } + }, + + "reverse": { + "title": "Search by Device or OS", + "search": { + "label": "Search for Device/OS/Vendor", + "placeholder": "e.g., iPhone, Windows, Cisco, Printer...", + "hint": "Search the database by device name, OS, or vendor" + }, + "results": { + "title": "Found {count} Device{plural}", + "confidence": "{level} confidence", + "osLabel": "OS:", + "descriptionLabel": "Description:", + "parameterListLabel": "Parameter Request List:", + "vendorPatternLabel": "Vendor Class Pattern:" + }, + "noResults": { + "title": "No Devices Found", + "description": "No devices matched \"{query}\". Try a different search term." + } + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied", + "copyAria": "Copy", + "copyHexAria": "Copy hex", + "copyParamListAria": "Copy parameter list" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-lease-time-calculator.json b/src/lib/i18n/translations/en/tools/dhcp-lease-time-calculator.json new file mode 100644 index 00000000..d38daf13 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-lease-time-calculator.json @@ -0,0 +1,68 @@ +{ + "title": "DHCP Lease Time Calculator", + "subtitle": "Calculate optimal DHCP lease times based on network size, client turnover, and utilization. Includes T1/T2 renewal times and configuration examples.", + + "input": { + "title": "Network Configuration", + "helpText": "Enter your network characteristics to calculate optimal lease times", + "poolSize": { + "label": "IP Pool Size", + "placeholder": "100", + "hint": "Total available IP addresses in your DHCP pool" + }, + "expectedClients": { + "label": "Expected Clients", + "placeholder": "50", + "hint": "Average number of concurrent clients" + }, + "networkType": { + "label": "Network Type", + "custom": "Custom (use churn rate)" + }, + "churnRate": { + "label": "Client Churn Rate", + "low": "Low - {time}", + "medium": "Medium - {time}", + "high": "High - {time}", + "custom": "Custom", + "hint": "How long devices typically stay connected" + }, + "customChurn": { + "label": "Custom Churn Time (hours)", + "placeholder": "24", + "hint": "Average hours a device stays connected" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Calculated Lease Times", + "summary": { + "poolUtilization": "Pool Utilization:", + "recommendedLease": "Recommended Lease:" + }, + "exhaustionWarning": "Address Exhaustion:", + "leaseTimes": { + "defaultLease": "Default Lease Time", + "t1Renewal": "T1 (Renewal)", + "t2Rebinding": "T2 (Rebinding)", + "seconds": "{seconds} seconds" + }, + "recommendations": { + "title": "Recommendations" + } + }, + + "config": { + "iscDhcpd": "ISC DHCPd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option119-builder.json b/src/lib/i18n/translations/en/tools/dhcp-option119-builder.json new file mode 100644 index 00000000..2b73d6fa --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option119-builder.json @@ -0,0 +1,126 @@ +{ + "title": "DHCP Option 119 - Domain Search List", + "subtitle": "Encode and decode Domain Search List (RFC 3397/6731) to/from RFC 1035 wire format with domain compression. Generate configurations for ISC dhcpd and Kea DHCP.", + + "modes": { + "encode": "Encode", + "decode": "Decode" + }, + + "encodeExamples": { + "corporate": { + "label": "Corporate", + "description": "Corporate network with domain compression" + }, + "multiSite": { + "label": "Multi-site", + "description": "Multiple sites sharing common suffix" + }, + "development": { + "label": "Development", + "description": "Development environments" + } + }, + + "decodeExamples": { + "corporate": { + "label": "Corporate", + "description": "corp.example.com, example.com (with compression)" + }, + "multiSite": { + "label": "Multi-site", + "description": "site1.example.com, site2.example.com, example.com" + }, + "singleDomain": { + "label": "Single Domain", + "description": "example.com (no compression)" + } + }, + + "encode": { + "domainListTitle": "Domain List", + "domain": { + "title": "Domain {number}", + "placeholder": "example.com" + }, + "addDomain": "Add Domain", + "networkSettings": { + "title": "Network Settings (Optional)", + "help": "Customize network values for configuration examples below", + "subnet": { + "label": "Subnet", + "placeholder": "192.168.1.0" + }, + "netmask": { + "label": "Netmask", + "placeholder": "255.255.255.0" + }, + "rangeStart": { + "label": "Range Start", + "placeholder": "192.168.1.100" + }, + "rangeEnd": { + "label": "Range End", + "placeholder": "192.168.1.200" + }, + "errorsTitle": "Network Settings Errors" + } + }, + + "decode": { + "title": "Decode Option 119 Hex", + "input": { + "label": "Hex-Encoded Option 119", + "placeholder": "Enter hex string (e.g., 0765786d706c6503636f6d00)" + }, + "button": "Decode" + }, + + "errors": { + "title": "Validation Errors", + "atLeastOneDomain": "At least one domain is required", + "domainRequired": "Domain {number}: Value is required", + "invalidCharacters": "Domain {number}: Invalid characters (use only letters, numbers, dots, hyphens)", + "cannotStartOrEndWithDot": "Domain {number}: Cannot start or end with a dot", + "consecutiveDots": "Domain {number}: Cannot contain consecutive dots", + "exceedsMaxLength": "Domain {number}: Exceeds maximum length of 253 characters", + "emptyLabel": "Domain {number}: Empty label found", + "labelTooLong": "Domain {number}: Label \"{label}\" exceeds maximum length of 63 characters", + "labelInvalidHyphen": "Domain {number}: Label \"{label}\" cannot start or end with hyphen", + "invalidSubnet": "Invalid subnet address", + "invalidNetmask": "Invalid netmask", + "invalidRangeStart": "Invalid range start address", + "invalidRangeEnd": "Invalid range end address", + "invalidHex": "Invalid hex input: only hexadecimal characters allowed", + "encodingFailed": "Encoding failed", + "decodingFailed": "Decoding failed" + }, + + "results": { + "encodeTitle": "Encoded Option 119", + "hexEncoded": { + "title": "Hex-Encoded (Compact)" + }, + "wireFormat": { + "title": "Wire Format (Spaced)" + }, + "summary": { + "totalLength": "Total Length:", + "domains": "Domains:", + "bytes": "{length} bytes" + }, + "configExamplesTitle": "Configuration Examples", + "formats": { + "iscDhcpd": "ISC dhcpd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration" + }, + "decodeTitle": "Decoded Domain Search List", + "domainsFound": "Domains Found:", + "domainListTitle": "Domain List" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option121-builder.json b/src/lib/i18n/translations/en/tools/dhcp-option121-builder.json new file mode 100644 index 00000000..1790becb --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option121-builder.json @@ -0,0 +1,136 @@ +{ + "title": "DHCP Option 121/249 - Classless Static Routes", + "subtitle": "Encode and decode Classless Static Routes (RFC 3442 / MSFT 249) with bit-packed network prefixes. Generate configurations for ISC dhcpd and Kea DHCP.", + + "modes": { + "encode": "Encode", + "decode": "Decode" + }, + + "encodeExamples": { + "privateNetworks": { + "label": "Private Networks", + "description": "Routes to RFC 1918 private networks" + }, + "defaultSpecific": { + "label": "Default + Specific", + "description": "Default route with specific override" + }, + "multiSiteVPN": { + "label": "Multi-site VPN", + "description": "Multiple VPN site routes" + } + }, + + "decodeExamples": { + "privateNetworks": { + "label": "Private Networks", + "description": "10.0.0.0/8 and 172.16.0.0/12 via 192.168.1.1" + }, + "defaultRoute": { + "label": "Default Route", + "description": "0.0.0.0/0 via 192.168.1.1" + }, + "specific24": { + "label": "Specific /24", + "description": "192.168.10.0/24 via 192.168.1.1" + } + }, + + "encode": { + "staticRoutesTitle": "Static Routes", + "route": { + "title": "Route {number}", + "destination": { + "label": "Destination (CIDR)", + "placeholder": "10.0.0.0/8" + }, + "gateway": { + "label": "Gateway", + "placeholder": "192.168.1.1" + } + }, + "addRoute": "Add Route", + "networkSettings": { + "title": "Network Settings (Optional)", + "help": "Customize network values for configuration examples below", + "subnet": { + "label": "Subnet", + "placeholder": "192.168.1.0" + }, + "netmask": { + "label": "Netmask", + "placeholder": "255.255.255.0" + }, + "rangeStart": { + "label": "Range Start", + "placeholder": "192.168.1.100" + }, + "rangeEnd": { + "label": "Range End", + "placeholder": "192.168.1.200" + }, + "errorsTitle": "Network Settings Errors" + } + }, + + "decode": { + "title": "Decode Option 121/249 Hex", + "input": { + "label": "Hex-Encoded Option 121/249", + "placeholder": "Enter hex string (e.g., 080ac0a80101acc01000c0a80101)" + }, + "button": "Decode" + }, + + "errors": { + "title": "Validation Errors", + "atLeastOneRoute": "At least one route is required", + "destinationRequired": "Route {number}: Destination is required", + "gatewayRequired": "Route {number}: Gateway is required", + "invalidCIDR": "Route {number}: Invalid CIDR notation (use format: x.x.x.x/y)", + "invalidPrefixLength": "Route {number}: Prefix length must be 0-32", + "invalidIPv4Dest": "Route {number}: Invalid IPv4 address in destination", + "invalidIPv4Octets": "Route {number}: Invalid IPv4 address (octets must be 0-255)", + "invalidGateway": "Route {number}: Invalid gateway IPv4 address", + "invalidGatewayOctets": "Route {number}: Invalid gateway address (octets must be 0-255)", + "invalidSubnet": "Invalid subnet address", + "invalidNetmask": "Invalid netmask", + "invalidRangeStart": "Invalid range start address", + "invalidRangeEnd": "Invalid range end address", + "invalidHex": "Invalid hex input: only hexadecimal characters allowed", + "encodingFailed": "Encoding failed", + "decodingFailed": "Decoding failed" + }, + + "results": { + "encodeTitle": "Encoded Option 121/249", + "hexEncoded": { + "title": "Hex-Encoded (Compact)" + }, + "wireFormat": { + "title": "Wire Format (Spaced)" + }, + "summary": { + "totalLength": "Total Length:", + "routes": "Routes:", + "bytes": "{length} bytes" + }, + "configExamplesTitle": "Configuration Examples", + "formats": { + "iscDhcpd": "ISC dhcpd Configuration (Option 121)", + "keaDhcp4": "Kea DHCPv4 Configuration", + "msftOption249": "Microsoft Option 249 Configuration" + }, + "decodeTitle": "Decoded Classless Static Routes", + "routesFound": "Routes Found:", + "routeListTitle": "Route List", + "destination": "Destination:", + "gateway": "Gateway:" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option150-builder.json b/src/lib/i18n/translations/en/tools/dhcp-option150-builder.json new file mode 100644 index 00000000..39759de7 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option150-builder.json @@ -0,0 +1,170 @@ +{ + "title": "DHCP Options 150/66/67 - TFTP Server Configuration", + "subtitle": "Configure TFTP servers for PXE boot and Cisco IP phones. Option 150 (Cisco TFTP list), Option 66 (TFTP server name), and Option 67 (bootfile name).", + + "modes": { + "encode": "Encode", + "decode": "Decode" + }, + + "encodeExamples": { + "ciscoIPPhones": { + "label": "Cisco IP Phones", + "description": "Redundant TFTP servers for Cisco IP phone configuration" + }, + "pxeBootStandard": { + "label": "PXE Boot (Standard)", + "description": "Standard PXE boot with single TFTP server" + }, + "pxeBootUEFI": { + "label": "PXE Boot (UEFI)", + "description": "UEFI PXE boot configuration" + }, + "combined": { + "label": "Combined (Option 150 + 67)", + "description": "Cisco phones with redundant TFTP and config template" + } + }, + + "decodeExamples": { + "option150DualTFTP": { + "label": "Option 150: Dual TFTP", + "description": "192.168.1.10 and 192.168.1.11" + }, + "option66Hostname": { + "label": "Option 66: Hostname", + "description": "pxe.example.com" + }, + "option67PXEBoot": { + "label": "Option 67: PXE Boot", + "description": "pxelinux.0" + }, + "option67UEFIBoot": { + "label": "Option 67: UEFI Boot", + "description": "bootx64.efi" + } + }, + + "encode": { + "option150": { + "title": "Option 150: Cisco TFTP Server List", + "helpText": "Multiple IPv4 addresses for redundant TFTP servers (Cisco IP phones)", + "serverLabel": "Server {number}", + "serverPlaceholder": "192.168.1.10", + "addButton": "Add TFTP Server" + }, + "option66": { + "title": "Option 66: TFTP Server Name (Standard)", + "helpText": "Single hostname or IP address for standard PXE boot", + "label": "TFTP Server Hostname/IP", + "placeholder": "tftp.example.com or 192.168.1.10" + }, + "option67": { + "title": "Option 67: Bootfile Name", + "helpText": "Filename to boot from TFTP server (e.g., pxelinux.0 for BIOS, bootx64.efi for UEFI)", + "label": "Bootfile Name", + "placeholder": "pxelinux.0" + }, + "networkSettings": { + "title": "Network Settings (Optional)", + "helpText": "Customize network values for configuration examples below", + "subnet": { + "label": "Subnet", + "placeholder": "192.168.1.0" + }, + "netmask": { + "label": "Netmask", + "placeholder": "255.255.255.0" + }, + "rangeStart": { + "label": "Range Start", + "placeholder": "192.168.1.100" + }, + "rangeEnd": { + "label": "Range End", + "placeholder": "192.168.1.200" + }, + "errorsTitle": "Network Settings Errors" + } + }, + + "decode": { + "title": "Decode TFTP Option", + "optionType": { + "label": "Option Type", + "option150": "Option 150: TFTP Server List", + "option66": "Option 66: TFTP Server Name", + "option67": "Option 67: Bootfile Name" + }, + "hexInput": { + "label": "Hex-Encoded Option Data", + "placeholder": "Enter hex string (e.g., c0a8010ac0a8010b for Option 150)" + }, + "decodeButton": "Decode" + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "option150": { + "title": "Option 150: TFTP Server List", + "hexEncodedTitle": "Hex-Encoded (Compact)", + "wireFormatTitle": "Wire Format (Spaced)", + "totalLength": "Total Length:", + "bytes": "{length} bytes", + "servers": "Servers:", + "serverCount": "{count}" + }, + "option66": { + "title": "Option 66: TFTP Server Name", + "valueTitle": "Value", + "hexEncodedTitle": "Hex-Encoded", + "totalLength": "Total Length:", + "bytes": "{length} bytes" + }, + "option67": { + "title": "Option 67: Bootfile Name", + "valueTitle": "Value", + "hexEncodedTitle": "Hex-Encoded", + "totalLength": "Total Length:", + "bytes": "{length} bytes" + }, + "configExamples": { + "title": "Configuration Examples", + "iscDhcpd": "ISC dhcpd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration", + "ciscoIos": "Cisco IOS Configuration" + } + }, + + "decodeResults": { + "option150": { + "title": "Decoded Option 150: TFTP Server List", + "totalLength": "Total Length:", + "bytes": "{length} bytes", + "serversFound": "Servers Found:", + "serverCount": "{count}", + "serversTitle": "TFTP Servers", + "serverLabel": "Server {number}:" + }, + "option66": { + "title": "Decoded Option 66: TFTP Server Name", + "totalLength": "Total Length:", + "bytes": "{length} bytes", + "serverTitle": "TFTP Server" + }, + "option67": { + "title": "Decoded Option 67: Bootfile Name", + "totalLength": "Total Length:", + "bytes": "{length} bytes", + "bootfileTitle": "Bootfile" + } + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option43-generator.json b/src/lib/i18n/translations/en/tools/dhcp-option43-generator.json new file mode 100644 index 00000000..926f5542 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option43-generator.json @@ -0,0 +1,83 @@ +{ + "title": "DHCP Option 43 Generator", + "subtitle": "Generate vendor-specific DHCP Option 43 for wireless controller discovery (Cisco, Aruba, Ruckus, Meraki, UniFi)", + + "examples": { + "ciscoCatalyst": "Cisco Catalyst with dual controllers", + "ciscoMeraki": "Single Meraki cloud controller", + "ruckusSmartzone": "Ruckus SmartZone controller", + "aruba": "Aruba wireless controller", + "unifi": "UniFi Network Controller", + "ruckusZonedirector": "Ruckus ZoneDirector (legacy)", + "tooltip": "Generate Option 43 for {vendor}" + }, + + "input": { + "title": "Generator Configuration", + "vendor": { + "label": "Wireless Controller Vendor" + }, + "ipAddresses": { + "label": "Controller IP Address", + "labelPlural": "Controller IP Addresses", + "maxHint": "(max {max})", + "placeholder": "Enter IP address (one per line or comma-separated)\ne.g., 192.168.1.10, 192.168.1.11", + "placeholderPlural": "Enter IP addresses (one per line or comma-separated)\ne.g., 192.168.1.10, 192.168.1.11" + }, + "generateButton": "Generate" + }, + + "errors": { + "noIPs": "Please enter at least one IP address", + "invalidIP": "Invalid IP address format: {ips}", + "maxExceeded": "{vendor} supports maximum {max} controller. You entered {count}.", + "maxExceededPlural": "{vendor} supports maximum {max} controllers. You entered {count}." + }, + + "results": { + "title": "Generated Option 43 Values", + "commandSection": { + "defaultLabel": "DHCP Server Command", + "calculationTitle": "How this value is calculated:" + }, + "formats": { + "hex": { + "title": "Hexadecimal String", + "hint": "Raw hexadecimal - used in most DHCP server configurations" + }, + "colonHex": { + "title": "Colon-Separated Hex", + "hint": "Used by Infoblox and some network appliances" + }, + "windowsBinary": { + "title": "Windows DHCP Binary", + "hint": "Enter in Windows DHCP Server's Binary field for Option 43" + }, + "iscDhcp": { + "title": "ISC DHCP Configuration", + "hint": "Add to dhcpd.conf for ISC DHCP server" + }, + "mikrotik": { + "title": "Mikrotik Configuration", + "hint": "RouterOS DHCP option configuration command" + } + } + }, + + "notes": { + "title": "Important Notes", + "list": { + "vendorSpecific": "DHCP Option 43 is vendor-specific and must match the AP manufacturer's expected format", + "option60": "Some vendors require Option 60 (Vendor Class Identifier) to be set in addition to Option 43", + "reachability": "Ensure controller IPs are reachable from the AP management network", + "highAvailability": "For high availability, configure multiple controller IPs when supported", + "leaseRenewal": "Changes to DHCP options require AP to renew lease or reboot to take effect", + "testing": "Always test in a controlled environment before deploying to production networks" + } + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option60-builder.json b/src/lib/i18n/translations/en/tools/dhcp-option60-builder.json new file mode 100644 index 00000000..7ad52568 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option60-builder.json @@ -0,0 +1,140 @@ +{ + "title": "DHCP Option 60 - Vendor Class Identifier", + "subtitle": "Configure vendor-specific DHCP policies using Option 60 (Vendor Class Identifier) for device type detection and class-based configuration", + + "examples": { + "ciscoPhone": "Cisco IP Phones with TFTP", + "ciscoAp": "Cisco APs with Option 43", + "pxeClient": "PXE network boot", + "arubaAp": "Aruba wireless APs", + "tooltip": "Generate config for {vendor}" + }, + + "input": { + "title": "Vendor Class Configuration", + "preset": { + "label": "Vendor Preset" + }, + "custom": { + "label": "Custom Vendor Class", + "placeholder": "MyCustomVendorClass", + "help": "1-255 printable ASCII characters" + }, + "advancedOptions": "Advanced Options", + "subnet": { + "label": "Subnet (CIDR)", + "placeholder": "192.168.10.0/24", + "help": "Network address in CIDR notation" + }, + "poolStart": { + "label": "Pool Start", + "placeholder": "192.168.10.100", + "help": "First IP in matching pool" + }, + "poolEnd": { + "label": "Pool End", + "placeholder": "192.168.10.200", + "help": "Last IP in matching pool" + }, + "nonMatchingPoolStart": { + "label": "Non-Matching Pool Start", + "placeholder": "192.168.10.50", + "help": "First IP for non-matching clients" + }, + "nonMatchingPoolEnd": { + "label": "Non-Matching Pool End", + "placeholder": "192.168.10.99", + "help": "Last IP for non-matching clients" + }, + "serverIp": { + "label": { + "tftp": "TFTP Server IP", + "config": "Config File Server IP" + }, + "help": "IP address of the provisioning server" + }, + "bootFilename": { + "label": { + "default": "Boot Filename", + "config": "Config Filename" + }, + "placeholder": { + "pxe": "pxelinux.0", + "docsis": "modem.cfg", + "cisco": "SEPDefault.cnf.xml" + }, + "help": "Name of the configuration or boot file" + }, + "mikrotikServerName": { + "label": "MikroTik DHCP Server Name", + "placeholder": "dhcp1", + "help": "Name of DHCP server in MikroTik config" + }, + "leaseTime": { + "label": "Lease Time (dnsmasq)", + "placeholder": "24h", + "help": "DHCP lease time (e.g., 24h, 1h, 30m)" + } + }, + + "errors": { + "customRequired": "Custom vendor class identifier is required", + "invalidVendorClass": "Invalid vendor class identifier. Must be 1-255 printable ASCII characters.", + "invalidSubnet": "Invalid subnet CIDR notation (e.g., 192.168.10.0/24)", + "invalidPoolStart": "Invalid pool start IP address", + "invalidPoolEnd": "Invalid pool end IP address", + "invalidNonMatchingPoolStart": "Invalid non-matching pool start IP address", + "invalidNonMatchingPoolEnd": "Invalid non-matching pool end IP address", + "invalidServerIp": "Invalid server IP address", + "invalidBootFilename": "Invalid boot filename", + "invalidMikrotikServerName": "MikroTik server name cannot be empty", + "invalidLeaseTime": "Invalid lease time format (e.g., 24h, 1h, 30m)", + "failedToGenerate": "Failed to generate configuration" + }, + + "results": { + "title": "Generated Configurations", + "vendorClass": { + "title": "Vendor Class Identifier (Option 60)", + "useCase": "Use Case:" + }, + "formats": { + "isc": { + "title": "ISC DHCP Server", + "hint": "Add to /etc/dhcp/dhcpd.conf" + }, + "kea": { + "title": "Kea DHCP Server", + "hint": "Add to Kea configuration JSON" + }, + "windows": { + "title": "Windows DHCP Server", + "hint": "Run PowerShell commands as Administrator" + }, + "dnsmasq": { + "title": "dnsmasq", + "hint": "Add to /etc/dnsmasq.conf" + }, + "mikrotik": { + "title": "MikroTik RouterOS", + "hint": "RouterOS CLI commands" + } + } + }, + + "notes": { + "title": "Important Notes", + "list": { + "vendorClass": "DHCP Option 60 (Vendor Class Identifier) allows DHCP servers to provide different configurations based on client type", + "classPolicies": "Class-based policies enable separate IP pools and options for different device types", + "option43": "Wireless APs typically require both Option 60 and Option 43 for controller discovery", + "testing": "Test configurations in a lab environment before deploying to production networks", + "customize": "Adjust subnet addresses, pool ranges, and option values to match your network design" + } + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-option82-builder.json b/src/lib/i18n/translations/en/tools/dhcp-option82-builder.json new file mode 100644 index 00000000..3e5d85ba --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-option82-builder.json @@ -0,0 +1,122 @@ +{ + "title": "DHCP Option 82 Builder", + "subtitle": "Construct and parse DHCP Relay Agent Information (Option 82) with Circuit-ID, Remote-ID, and VLAN formats. Includes examples for relay ACLs and policies.", + + "modes": { + "build": "Build", + "parse": "Parse" + }, + + "formats": { + "ascii": "ASCII Text", + "hex": "Hexadecimal", + "vlanId": "VLAN ID", + "hostnamePort": "Hostname:Port" + }, + + "buildExamples": { + "vlan100": { + "label": "VLAN 100", + "description": "Circuit-ID as VLAN ID 100" + }, + "switchPort": { + "label": "Switch Port", + "description": "Circuit-ID as hostname:port" + }, + "customCircuit": { + "label": "Custom Circuit", + "description": "Circuit-ID as custom ASCII text" + }, + "switchHostname": { + "label": "Switch Hostname", + "description": "Remote-ID as hostname" + }, + "macAddress": { + "label": "MAC Address", + "description": "Remote-ID as MAC address" + }, + "agentId": { + "label": "Agent ID", + "description": "Remote-ID as relay agent identifier" + } + }, + + "parseExamples": { + "vlanHostname": { + "label": "VLAN + Hostname", + "description": "Circuit-ID (VLAN 100) + Remote-ID (sw1.example)" + }, + "switchPort": { + "label": "Switch Port", + "description": "Circuit-ID (Gi0/1) + Remote-ID (sw1.example)" + }, + "macAddress": { + "label": "MAC Address", + "description": "Remote-ID as MAC address (00:11:22:33:44:55)" + } + }, + + "build": { + "configurationTitle": "Configuration", + "suboption": { + "title": "Suboption {number}", + "typeLabel": "Suboption Type", + "circuitId": "Circuit-ID (Suboption 1)", + "remoteId": "Remote-ID (Suboption 2)", + "encodingFormat": "Encoding Format", + "valueLabel": "Value", + "placeholders": { + "vlanId": "100", + "hex": "001122334455", + "default": "Enter value" + } + }, + "addSuboption": "Add Suboption", + "results": { + "title": "Generated Option 82", + "hexEncoded": "Hex-Encoded Value", + "breakdown": "Breakdown", + "typeCode": "{type} (Code {code})", + "length": "Length: {length} bytes", + "valueLabel": "Value:", + "hexLabel": "Hex:", + "examples": { + "iscDhcpd": "ISC dhcpd Configuration Example", + "keaDhcp4": "Kea DHCPv4 Configuration Example", + "ciscoRelay": "Cisco Relay Agent Example" + } + } + }, + + "parse": { + "title": "Parse Option 82 Hex", + "hexEncoded": "Hex-Encoded Option 82", + "placeholder": "Enter hex string (e.g., 01064769302f31020b7377312e6578616d706c65)", + "button": "Parse", + "results": { + "title": "Parsed Option 82", + "totalLength": "Total Length:", + "suboptionsFound": "Suboptions Found:", + "suboptionsTitle": "Suboptions", + "typeCode": "{type} (Code {code})", + "length": "Length: {length} bytes", + "decodedValue": "Decoded Value:", + "hexValue": "Hex Value:", + "bytes": "{length} bytes", + "count": "{count}" + } + }, + + "errors": { + "title": "Validation Errors", + "valueRequired": "Suboption {number}: Value is required", + "vlanRange": "Suboption {number}: VLAN ID must be between 0 and 4095", + "invalidHex": "Suboption {number}: Invalid hex format", + "invalidHexInput": "Invalid hex input: only hexadecimal characters allowed" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-options-6-15.json b/src/lib/i18n/translations/en/tools/dhcp-options-6-15.json new file mode 100644 index 00000000..2f1d97d7 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-options-6-15.json @@ -0,0 +1,76 @@ +{ + "title": "DHCP Options 6 & 15 - DNS Servers and Domain", + "subtitle": "Option 6 specifies DNS servers for name resolution, while Option 15 provides the domain name for client hostname resolution. These options work together for complete DNS configuration.", + + "nav": { + "build": "Build Options", + "decode": "Decode Options" + }, + + "build": { + "title": "DNS Configuration", + "dnsServers": { + "legend": "DNS Servers (Option 6)", + "placeholder": "e.g., 8.8.8.8", + "addButton": "Add DNS Server", + "removeButton": "Remove" + }, + "domainName": { + "label": "Domain Name (Option 15)", + "placeholder": "e.g., example.com", + "hint": "Domain name for client hostname resolution" + }, + "validationErrors": "Validation Errors:" + }, + + "decode": { + "title": "Decode DNS Options", + "optionSelect": { + "legend": "Option to Decode", + "option6": "Option 6 - DNS Servers", + "option15": "Option 15 - Domain Name", + "option6Tooltip": "Decode Option 6 to extract DNS server addresses from hex", + "option15Tooltip": "Decode Option 15 to extract domain name from hex" + }, + "hexInput": { + "label": "Hex String", + "placeholderOption6": "e.g., 08080808 or 08 08 08 08 08 08 04 04", + "placeholderOption15": "e.g., 6578616d706c6503636f6d", + "hint": "Enter hex bytes (spaces optional)" + }, + "error": "Decode Error:" + }, + + "results": { + "title": "DHCP DNS Options", + "decodedTitle": "Decoded {option}", + "option6": { + "title": "Option 6 - DNS Servers", + "servers": "DNS Servers:", + "serverCount": "Server Count:", + "hexEncoded": "Hex Encoded:", + "wireFormat": "Wire Format:", + "totalLength": "Total Length:", + "bytes": "{length} bytes" + }, + "option15": { + "title": "Option 15 - Domain Name", + "domain": "Domain:", + "domainName": "Domain Name:", + "hexEncoded": "Hex Encoded:", + "wireFormat": "Wire Format:", + "totalLength": "Total Length:", + "bytes": "{length} bytes" + }, + "config": { + "title": "Configuration Examples", + "iscDhcpd": "ISC DHCPd", + "keaDhcp4": "Kea DHCPv4", + "dnsmasq": "dnsmasq" + }, + "copyButton": "Copy", + "copiedButton": "Copied", + "copyHexLabel": "Copy hex", + "copyWireLabel": "Copy wire format" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcp-snippets-generator.json b/src/lib/i18n/translations/en/tools/dhcp-snippets-generator.json new file mode 100644 index 00000000..50d98e05 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcp-snippets-generator.json @@ -0,0 +1,70 @@ +{ + "title": "DHCP Snippets Generator", + "subtitle": "Generate configuration snippets for ISC dhcpd and Kea DHCP servers with customizable subnets, pools, and options.", + + "targets": { + "iscDhcpd": "ISC dhcpd", + "keaDhcp4": "Kea DHCPv4", + "keaDhcp6": "Kea DHCPv6" + }, + + "configuration": { + "title": "Configuration", + "targetServers": "Target Servers", + "ipMode": "IP Mode", + "modes": { + "dhcp4": "DHCPv4 (IPv4)", + "dhcp6": "DHCPv6 (IPv6)" + }, + "subnet": { + "label": "Subnet (CIDR)", + "placeholderV6": "2001:db8::/64", + "placeholderV4": "192.168.1.0/24" + }, + "pools": { + "label": "Address Pools", + "startPlaceholder": "Start IP", + "endPlaceholder": "End IP", + "addPool": "Add Pool" + }, + "gateway": { + "label": "Gateway (Router)", + "placeholderV6": "fe80::1", + "placeholderV4": "192.168.1.1" + }, + "dns": { + "label": "DNS Servers (comma-separated)", + "placeholder": "8.8.8.8, 8.8.4.4" + }, + "domain": { + "label": "Domain Name", + "placeholder": "example.com" + }, + "leases": { + "defaultLabel": "Default Lease (seconds)", + "maxLabel": "Max Lease (seconds)", + "defaultPlaceholder": "86400", + "maxPlaceholder": "604800" + }, + "options": { + "useOptionNames": "Use option names (ISC)", + "prettyJson": "Pretty JSON (Kea)" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Generated Snippets", + "iscDhcpd": "ISC dhcpd.conf", + "keaDhcp4": "Kea DHCPv4 JSON", + "keaDhcp6": "Kea DHCPv6 JSON" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcpv6-dns-builder.json b/src/lib/i18n/translations/en/tools/dhcpv6-dns-builder.json new file mode 100644 index 00000000..5d1f0e0f --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcpv6-dns-builder.json @@ -0,0 +1,78 @@ +{ + "title": "DHCPv6 DNS Options (RFC 3646)", + "subtitle": "Configure DNS servers (Option 23) and search domains (Option 24) for DHCPv6 clients. Supports IPv6 DNS servers and multiple search domains.", + + "examples": { + "googleDNS": { + "label": "Google Public DNS", + "description": "Google Public DNS servers with example.com search domains" + }, + "cloudflareDNS": { + "label": "Cloudflare DNS", + "description": "Cloudflare 1.1.1.1 DNS with local search domain" + }, + "quad9DNS": { + "label": "Quad9 DNS", + "description": "Quad9 DNS with corporate search domains" + }, + "localNetwork": { + "label": "Local Network", + "description": "Local ULA DNS server with home.arpa domain" + } + }, + + "option23": { + "title": "Option 23: DNS Recursive Name Servers", + "helpText": "IPv6 addresses of DNS servers for client name resolution", + "serverLabel": "DNS Server {number}", + "placeholder": "2001:4860:4860::8888", + "removeLabel": "Remove DNS server", + "addButton": "Add DNS Server" + }, + + "option24": { + "title": "Option 24: Domain Search List", + "helpText": "DNS search domains for hostname resolution", + "domainLabel": "Search Domain {number}", + "placeholder": "example.com", + "removeLabel": "Remove search domain", + "addButton": "Add Search Domain" + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "option23Title": "Option 23: DNS Recursive Name Servers", + "option24Title": "Option 24: Domain Search List", + "totalLength": "Total Length:", + "lengthBytes": "{length} bytes", + "servers": "Servers:", + "serversCount": "{count}", + "domains": "Domains:", + "domainsCount": "{count}", + "dnsServersHeading": "DNS Servers", + "serverLabel": "Server {number}:", + "searchDomainsHeading": "Search Domains", + "domainLabel": "Domain {number}:", + "hexEncodedTitle": "Hex-Encoded (Compact)", + "wireFormatTitle": "Wire Format (Spaced)", + "breakdownTitle": "Domain Encoding Breakdown", + "configExampleTitle": "Configuration Example", + "keaDhcpv6Title": "Kea DHCPv6 Configuration" + }, + + "about": { + "title": "About RFC 3646", + "intro": "RFC 3646 defines DNS configuration options for DHCPv6, allowing IPv6 clients to automatically discover DNS servers and search domains.", + "option23Description": "DNS Recursive Name Server - List of IPv6 DNS server addresses (16 bytes each)", + "option24Description": "Domain Search List - DNS search domains encoded in DNS wire format (length-prefixed labels)", + "conclusion": "These options are essential for IPv6 network autoconfiguration, enabling clients to resolve hostnames without manual DNS configuration." + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dhcpv6-fqdn.json b/src/lib/i18n/translations/en/tools/dhcpv6-fqdn.json new file mode 100644 index 00000000..53d6722d --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dhcpv6-fqdn.json @@ -0,0 +1,69 @@ +{ + "title": "DHCPv6 Client FQDN Option (RFC 4704)", + "subtitle": "Configure the Client FQDN Option (Option 39) for DHCPv6, enabling dynamic DNS updates and hostname management for IPv6 clients.", + + "configuration": { + "fqdnTitle": "FQDN Configuration", + "fqdnHelpText": "Fully Qualified Domain Name for the DHCPv6 client", + "fqdnLabel": "Fully Qualified Domain Name (FQDN)", + "fqdnPlaceholder": "client.example.com", + + "flagsTitle": "DNS Update Flags", + "flagsHelpText": "Control how DNS updates are performed", + + "serverUpdate": { + "label": "Server Should Update DNS (S Flag)", + "help": "Server will perform AAAA and PTR record updates" + }, + "serverOverride": { + "label": "Server Override (O Flag)", + "help": "Server can override client's preferences" + }, + "clientUpdate": { + "label": "Client Should Update DNS (N Flag = 0)", + "help": "Client will perform its own DNS updates" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Option 39: Client FQDN", + "fqdn": "FQDN:", + "totalLength": "Total Length:", + "lengthBytes": "{length} bytes", + + "flagsBreakdown": "Flags Breakdown", + "sFlag": "S Flag", + "oFlag": "O Flag", + "nFlag": "N Flag", + "flagSet": "Set", + "flagNotSet": "Not Set", + + "flagsByte": "Flags Byte", + "hexEncoded": "Hex-Encoded (Compact)", + "wireFormat": "Wire Format (Spaced)", + "encodingBreakdown": "Encoding Breakdown", + "flags": "Flags:", + "fqdnLabel": "FQDN:", + + "configExample": "Configuration Example", + "keaDhcpv6": "Kea DHCPv6 Configuration" + }, + + "about": { + "title": "About RFC 4704", + "intro": "RFC 4704 defines the Client FQDN Option for DHCPv6, enabling clients and servers to negotiate dynamic DNS updates for IPv6 addresses.", + "sFlagDescription": "Server should perform DNS updates", + "oFlagDescription": "Server can override client preferences", + "nFlagDescription": "Client requests server to perform updates (client will NOT update)", + "conclusion": "The FQDN is encoded using DNS wire format with length-prefixed labels, enabling automated hostname registration in DNS for IPv6 networks." + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/dkim-key-generator.json b/src/lib/i18n/translations/en/tools/dkim-key-generator.json new file mode 100644 index 00000000..424ca1c6 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dkim-key-generator.json @@ -0,0 +1,93 @@ +{ + "title": "DKIM Key Generator", + "description": "Generate DKIM RSA keypairs with selectors and DNS TXT records for email authentication.", + + "config": { + "title": "Configuration", + "selectorLabel": "Selector:", + "selectorTooltip": "Unique identifier for this DKIM key (e.g., 'default', '202412', 'mailgun')", + "selectorPlaceholder": "default", + "domainLabel": "Domain:", + "domainTooltip": "Domain that will use this DKIM key for signing emails", + "domainPlaceholder": "example.com", + "keySizeLabel": "Key Size:", + "keySizeTooltip": "RSA key size in bits. 2048-bit recommended for security, 1024-bit for compatibility", + "keySizes": { + "1024": "1024-bit (Legacy)", + "2048": "2048-bit (Recommended)" + }, + "generateButton": "Generate DKIM Keys", + "generating": "Generating..." + }, + + "keys": { + "title": "Generated Keys", + "privateKey": { + "title": "Private Key", + "hideTooltip": "Hide private key", + "showTooltip": "Show private key", + "hideButton": "Hide", + "showButton": "Show", + "downloadTooltip": "Download private key as PEM file", + "downloadButton": "Download", + "downloaded": "Downloaded!", + "hidden": "Private key hidden for security", + "warning": "Keep this private key secure. Never share it publicly or store it in version control." + }, + "publicKey": { + "title": "Public Key", + "copyTooltip": "Copy public key to clipboard", + "copyButton": "Copy", + "copied": "Copied!", + "downloadTooltip": "Download public key as PEM file", + "downloadButton": "Download", + "downloaded": "Downloaded!" + } + }, + + "dns": { + "title": "DNS TXT Record", + "copyTooltip": "Copy DNS TXT record to clipboard", + "copyButton": "Copy", + "copied": "Copied!", + "exportTooltip": "Download DNS record as text file", + "exportButton": "Export", + "exported": "Downloaded!", + "zoneFileFormat": "Zone File Format:", + "dkimRecordValue": "DKIM Record Value:" + }, + + "implementation": { + "title": "Implementation Notes", + "selector": "Selector:", + "domain": "Domain:", + "keySize": "Key Size:", + "keySizeBits": "{size}-bit RSA", + "algorithm": "Algorithm:", + "algorithmValue": "RSA-SHA256 (rsa-sha256)", + "nextStepsTitle": "Next Steps:", + "steps": [ + "Add the DNS TXT record to your domain's DNS configuration", + "Configure your mail server with the private key", + "Set up DKIM signing for outgoing emails", + "Test DKIM signatures using online validation tools" + ] + }, + + "examples": { + "title": "Example Configurations", + "standard": { + "name": "Standard Setup" + }, + "monthly": { + "name": "Monthly Rotation" + }, + "serviceSpecific": { + "name": "Service-Specific" + }, + "selectorLabel": "Selector:", + "domainLabel": "Domain:", + "keySizeLabel": "Key Size:", + "keySizeBits": "{size}-bit" + } +} diff --git a/src/lib/i18n/translations/en/tools/dmarc-builder.json b/src/lib/i18n/translations/en/tools/dmarc-builder.json new file mode 100644 index 00000000..bc6bad95 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dmarc-builder.json @@ -0,0 +1,159 @@ +{ + "title": "DMARC Policy Builder", + "description": "Create DMARC policies with alignment options, reporting addresses, and failure handling configuration.", + + "domain": { + "title": "Domain Configuration", + "label": "Domain:", + "tooltip": "Domain that this DMARC policy will protect", + "placeholder": "example.com" + }, + + "policy": { + "title": "Policy Configuration", + "mainLabel": "Policy (p):", + "mainTooltip": "Action to take for emails that fail DMARC authentication", + "subdomainLabel": "Subdomain Policy (sp):", + "subdomainTooltip": "Policy for subdomains (inherits main policy if not set)", + + "types": { + "none": { + "label": "none - Monitor only", + "description": "Monitor only - no action taken on failed emails" + }, + "quarantine": { + "label": "quarantine - Send to spam", + "description": "Failed emails sent to spam/junk folder" + }, + "reject": { + "label": "reject - Block email", + "description": "Failed emails rejected at SMTP level" + } + }, + + "subdomainInherit": "Inherit from main policy", + + "percentageLabel": "Percentage (pct):", + "percentageTooltip": "Percentage of failing emails to apply policy to (useful for gradual deployment)" + }, + + "advanced": { + "title": "Advanced Options", + + "alignment": { + "title": "Authentication Alignment", + "dkimLabel": "DKIM Alignment (adkim):", + "dkimTooltip": "How strictly DKIM signature domain must match From domain", + "spfLabel": "SPF Alignment (aspf):", + "spfTooltip": "How strictly SPF domain must match From domain", + + "relaxed": { + "label": "r - Relaxed", + "description": "Relaxed - domain and subdomains match" + }, + "strict": { + "label": "s - Strict", + "description": "Strict - exact domain match only" + } + }, + + "reporting": { + "title": "Reporting Configuration", + "aggregateLabel": "Reporting Email (rua):", + "aggregateTooltip": "Email address to receive aggregate DMARC reports", + "aggregatePlaceholder": "dmarc@example.com", + "forensicLabel": "Forensic Email (ruf):", + "forensicTooltip": "Email address to receive forensic failure reports (detailed samples)", + "forensicPlaceholder": "forensic@example.com", + "intervalLabel": "Report Interval (ri):", + "intervalTooltip": "How often aggregate reports are sent (in seconds)", + + "intervals": { + "hourly": "1 hour", + "daily": "24 hours (daily)", + "weekly": "7 days (weekly)" + } + }, + + "failureOptions": { + "title": "Failure Reporting Options (fo):", + "tooltip": "When to generate forensic failure reports", + + "options": { + "0": "Generate reports if both SPF and DKIM fail", + "1": "Generate reports if either SPF or DKIM fail", + "d": "Generate reports if DKIM fails", + "s": "Generate reports if SPF fails" + } + } + }, + + "output": { + "title": "Generated DMARC Record", + "copyButton": "Copy", + "copyTooltip": "Copy DMARC record to clipboard", + "copied": "Copied!", + "exportButton": "Export", + "exportTooltip": "Download as zone file", + "downloaded": "Downloaded!", + "txtRecordLabel": "DNS TXT Record:" + }, + + "validation": { + "title": "Policy Validation", + "recordLengthLabel": "Record Length:", + "statusLabel": "Status:", + "valid": "Valid", + "invalid": "Invalid", + "success": "DMARC policy is valid and ready to deploy!", + + "errors": { + "domainRequired": "Domain is required", + "reportingEmail": "Reporting URI must be a valid email address", + "forensicEmail": "Forensic URI must be a valid email address", + "recordTooLong": "DMARC record too long ({length} chars). DNS TXT limit is 255." + }, + + "warnings": { + "domainNoTLD": "Domain should include TLD (e.g., .com, .org)", + "rejectNeedsReporting": "Consider adding reporting URI before using reject policy", + "noneWithPercentage": "Percentage should be 100% for monitoring-only policy", + "strictAlignment": "Strict alignment for both SPF and DKIM may cause legitimate emails to fail", + "recordLong": "DMARC record is long ({length} chars). Consider shortening." + } + }, + + "deployment": { + "title": "Deployment Guide", + "steps": [ + "Start with p=none to monitor current email authentication status", + "Analyze DMARC reports to identify legitimate vs malicious sources", + "Configure SPF and DKIM for all legitimate sending sources", + "Gradually increase to p=quarantine with low percentage (pct=25)", + "Monitor for false positives and adjust alignment if needed", + "Increase percentage gradually (50%, 75%, 100%)", + "Finally move to p=reject when confident in configuration" + ] + }, + + "examples": { + "title": "Example Policies", + + "monitor": { + "name": "Monitor Only", + "description": "Start monitoring without affecting email delivery" + }, + "quarantine": { + "name": "Quarantine Phase", + "description": "Move suspicious emails to spam folder" + }, + "fullProtection": { + "name": "Full Protection", + "description": "Reject all failing emails with forensics" + }, + + "policyLabel": "Policy:", + "percentageLabel": "Percentage:", + "reportsLabel": "Reports:" + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-aaaa-bulk.json b/src/lib/i18n/translations/en/tools/dns-aaaa-bulk.json new file mode 100644 index 00000000..a6bd3862 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-aaaa-bulk.json @@ -0,0 +1,52 @@ +{ + "title": "A/AAAA Bulk Generator", + "description": "Bulk create A and AAAA record sets from hostname and IP lists with TTL controls and zone file generation.", + + "examples": { + "title": "Quick Examples", + "webServers": "Web Servers", + "mailServers": "Mail Servers", + "ipv6Services": "IPv6 Services", + "hostsCount": "{count} hosts" + }, + + "input": { + "hostnames": { + "label": "Hostnames", + "tooltip": "Enter hostnames, one per line", + "placeholder": "www\napi\nmail\nftp" + }, + "ips": { + "label": "IP Addresses", + "tooltip": "Enter IP addresses (IPv4 or IPv6), one per line", + "placeholder": "192.168.1.10\n192.168.1.11\n2001:db8::1\n2001:db8::2" + }, + "ttl": { + "label": "TTL (seconds)", + "tooltip": "Time To Live in seconds" + }, + "zoneName": { + "label": "Zone Name", + "tooltip": "Domain name for the zone file", + "placeholder": "example.com" + }, + "generateZoneFile": "Generate zone file" + }, + + "results": { + "title": "Generated Records", + "copyRecordsButton": "Copy Records", + "downloadZoneButton": "Download Zone", + "tableHeaders": { + "name": "Name", + "ttl": "TTL", + "type": "Type", + "value": "Value" + } + }, + + "zoneFile": { + "title": "Zone File Preview", + "copyButton": "Copy Zone File" + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-cname-builder.json b/src/lib/i18n/translations/en/tools/dns-cname-builder.json new file mode 100644 index 00000000..5610a16c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-cname-builder.json @@ -0,0 +1,88 @@ +{ + "title": "CNAME Builder", + "description": "Build valid CNAME records with loop detection, self-target checks, and FQDN validation.", + + "mode": { + "bulkMode": "Bulk mode (multiple records)" + }, + + "examples": { + "title": "Quick Examples", + "webAliases": { + "label": "Web Aliases" + }, + "serviceRedirects": { + "label": "Service Redirects" + }, + "cdnConfiguration": { + "label": "CDN Configuration" + }, + "recordsCount": "{count} records" + }, + + "input": { + "aliases": { + "label": "Alias Names", + "tooltip": "Enter alias names, one per line", + "placeholder": "www\nblog\nmail\nftp" + }, + "targets": { + "label": "Target FQDNs", + "tooltip": "Enter target FQDNs, one per line. Must end with dot (.).", + "placeholder": "server1.example.com.\nserver2.example.com.\nmailserver.example.com.\nftpserver.example.com." + }, + "alias": { + "label": "Alias Name", + "tooltip": "Enter the alias name (left side of CNAME)", + "placeholder": "www" + }, + "target": { + "label": "Target FQDN", + "tooltip": "Enter the target FQDN (right side of CNAME). Must end with dot (.).", + "placeholder": "server1.example.com." + }, + "ttl": { + "label": "TTL (seconds)", + "tooltip": "Time To Live in seconds" + } + }, + + "bestPractices": { + "title": "CNAME Best Practices", + "rules": [ + "Target must be a Fully Qualified Domain Name (FQDN) ending with a dot", + "CNAME records cannot coexist with other record types", + "Avoid CNAME chains longer than 3-4 hops", + "Never point a CNAME to another CNAME if possible" + ] + }, + + "results": { + "title": "Generated CNAME Records", + "copyButton": "Copy Records", + "tableHeaders": { + "alias": "Alias", + "ttl": "TTL", + "type": "Type", + "target": "Target", + "status": "Status" + } + }, + + "validation": { + "title": "Validation Issues", + "status": { + "valid": "Valid", + "loop": "Loop Detected", + "selfTarget": "Self Target", + "invalidFormat": "Invalid Format", + "missingDot": "Missing FQDN Dot", + "unknown": "Unknown" + }, + "messages": { + "missingDot": "Target should end with '.' to be a proper FQDN", + "loop": "Creates a circular reference that will cause DNS resolution to fail", + "selfTarget": "Points to itself, which is not allowed" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-label-normalizer.json b/src/lib/i18n/translations/en/tools/dns-label-normalizer.json new file mode 100644 index 00000000..66fb538c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-label-normalizer.json @@ -0,0 +1,86 @@ +{ + "title": "DNS Label Normalizer", + "subtitle": "Normalize domain labels with case conversion, IDN detection, and homograph attack analysis.", + + "overview": { + "caseNormalization": { + "title": "Case Normalization", + "description": "Converts labels to lowercase following DNS case-insensitivity" + }, + "idnDetection": { + "title": "IDN Detection", + "description": "Identifies internationalized domain names and punycode encoding" + }, + "securityAnalysis": { + "title": "Security Analysis", + "description": "Detects homograph attacks and mixed script vulnerabilities" + } + }, + + "examples": { + "title": "Example Labels", + "caseNormalization": { + "label": "Case Normalization", + "value": "Example.COM\nWWW.GOOGLE.com", + "description": "Mixed case domain labels" + }, + "idnPunycode": { + "label": "IDN/Punycode", + "value": "москва.Ρ€Ρ„\nxn--80adxhks.xn--p1ai", + "description": "International domain names" + }, + "homographAttack": { + "label": "Homograph Attack", + "value": "googlΠ΅.com\nexΠ°mple.org", + "description": "Cyrillic characters mixed with Latin" + } + }, + + "input": { + "title": "Domain Labels", + "label": "Labels to Normalize", + "tooltip": "Enter domain labels separated by spaces, commas, or newlines", + "placeholder": "example.com\nxn--e1afmkfd.xn--p1ai\nmixed-script-Π΅xample.com" + }, + + "results": { + "title": "Normalization Results", + "labelNumber": "Label {number}", + "badges": { + "idn": "IDN", + "homoglyphs": "Homoglyphs", + "mixedScripts": "Mixed Scripts" + }, + "original": "Original:", + "normalized": "Normalized:", + "labelNormalized": "Label was normalized", + "noChanges": "No changes needed", + "scriptsDetected": "Scripts Detected ({count})", + "securityWarnings": "Security Warnings ({count})", + "errors": "Errors ({count})" + }, + + "education": { + "title": "About DNS Label Normalization", + "caseNormalization": { + "title": "Case Normalization", + "description": "DNS labels are case-insensitive. This tool converts all labels to lowercase for consistency and comparison.", + "example": "Example.COM β†’ example.com" + }, + "idnProcessing": { + "title": "IDN Processing", + "description": "Internationalized Domain Names use punycode encoding. This tool detects IDN labels and potential encoding issues.", + "example": "москва.Ρ€Ρ„ ↔ xn--80adxhks.xn--p1ai" + }, + "securityAnalysis": { + "title": "Security Analysis", + "description": "Mixed scripts in labels can indicate homograph attacks. This tool warns about potential security risks.", + "example": "googlΠ΅.com (Cyrillic 'Π΅')" + }, + "bestPractices": { + "title": "Best Practices", + "description": "Always normalize labels before comparison. Be cautious of mixed scripts and visually similar characters from different scripts.", + "example": "Normalize β†’ Compare β†’ Validate" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-mx-planner.json b/src/lib/i18n/translations/en/tools/dns-mx-planner.json new file mode 100644 index 00000000..0efc115e --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-mx-planner.json @@ -0,0 +1,83 @@ +{ + "title": "MX Record Planner", + "subtitle": "Plan MX record priorities with fallback guidance, best practices, and sample configurations for popular email providers.", + + "input": { + "domain": { + "label": "Domain", + "tooltip": "Domain name for the MX records" + }, + "ttl": { + "label": "Default TTL (seconds)", + "tooltip": "Default Time To Live in seconds for all MX records" + }, + "addRecordButton": "Add MX Record", + "priority": { + "label": "Priority", + "tooltip": "Lower numbers = higher priority" + }, + "mailserver": "Mail Server (FQDN)", + "role": { + "label": "Role", + "primary": "Primary", + "backup": "Backup", + "custom": "Custom" + } + }, + + "section": { + "mxRecords": "MX Records", + "sortOriginal": "Original Order", + "sortPriority": "Sort by Priority" + }, + + "examples": { + "title": "Quick Examples", + "basicSetup": "Basic Setup", + "googleWorkspace": "Google Workspace", + "microsoft365": "Microsoft 365", + "multiProvider": "Multi-Provider Setup", + "recordsCount": "{count} MX records" + }, + + "guidelines": { + "title": "Priority Guidelines", + "highest": "Highest priority, primary mail servers", + "high": "High priority, secondary mail servers", + "medium": "Medium priority, backup servers", + "low": "Low priority, fallback servers" + }, + + "bestPractices": { + "title": "MX Best Practices", + "redundancy": "Always have at least two MX records for redundancy", + "priorities": "Use different priority values to control mail flow", + "configuration": "Ensure all mail servers are properly configured", + "testing": "Test mail delivery to all configured servers", + "geographic": "Consider geographic distribution for better performance" + }, + + "validation": { + "emptyMailserver": "Mail server cannot be empty", + "missingDot": "Mail server should end with a dot (FQDN)", + "priorityRange": "Priority must be between 0 and 65535", + "duplicatePriority": "Duplicate priority values detected" + }, + + "results": { + "title": "Generated MX Records", + "copyButton": "Copy Zone Records", + "tableHeaders": { + "domain": "Domain", + "ttl": "TTL", + "type": "Type", + "priority": "Priority", + "mailServer": "Mail Server", + "status": "Status" + }, + "statusValid": "Valid", + "statusIssues": "Issues", + "configurationIssues": "Configuration Issues", + "priorityIssueFormat": "Priority {priority}: {issues}" + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-record-validator.json b/src/lib/i18n/translations/en/tools/dns-record-validator.json new file mode 100644 index 00000000..411f32f0 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-record-validator.json @@ -0,0 +1,122 @@ +{ + "title": "DNS Record Validator", + "subtitle": "Validate individual DNS resource record syntax for proper formatting and common issues", + + "overview": { + "syntaxValidation": { + "title": "Syntax Validation:", + "description": "Verify record values match RFC specifications for format and constraints." + }, + "errorDetection": { + "title": "Error Detection:", + "description": "Identify format errors, range violations, and protocol mismatches." + }, + "bestPractices": { + "title": "Best Practices:", + "description": "Get warnings about potential issues and optimization suggestions." + } + }, + + "examples": { + "title": "Quick Examples", + "a": "Basic web server A record", + "aaaa": "IPv6 web server record", + "cname": "Blog subdomain alias", + "mx": "Primary mail server", + "txt": "SPF policy record", + "srv": "HTTPS service record" + }, + + "recordTypes": { + "a": { + "label": "A (IPv4 Address)", + "description": "Maps domain to IPv4 address" + }, + "aaaa": { + "label": "AAAA (IPv6 Address)", + "description": "Maps domain to IPv6 address" + }, + "cname": { + "label": "CNAME (Canonical Name)", + "description": "Alias to another domain" + }, + "mx": { + "label": "MX (Mail Exchange)", + "description": "Mail server for domain" + }, + "txt": { + "label": "TXT (Text)", + "description": "Arbitrary text data" + }, + "srv": { + "label": "SRV (Service)", + "description": "Service location and port" + }, + "caa": { + "label": "CAA (Certificate Authority)", + "description": "Certificate authority authorization" + } + }, + + "input": { + "recordType": { + "label": "Record Type", + "tooltip": "Select the DNS record type to validate" + }, + "recordName": { + "label": "Record Name", + "tooltip": "The domain name for this DNS record", + "placeholder": "example.com" + }, + "recordValue": { + "label": "Record Value", + "tooltip": "The value/data for this DNS record", + "placeholderTxt": "Enter TXT record content...", + "placeholderA": "192.0.2.1", + "placeholderAAAA": "2001:db8::1", + "placeholderDefault": "Record value..." + }, + "priority": "Priority", + "service": "Service", + "protocol": "Protocol", + "weight": "Weight", + "port": "Port", + "flags": "Flags", + "tag": "Tag", + "ttl": { + "label": "TTL (seconds)", + "tooltip": "Time To Live in seconds (how long record should be cached)", + "placeholder": "3600" + } + }, + + "results": { + "validStatus": "Valid", + "invalidStatus": "Invalid", + "dnsRecord": "DNS Record", + "copyButton": "Copy Zone Line", + "zoneFileFormat": "Zone File Format:", + "errors": "Errors ({count})", + "warnings": "Warnings ({count})", + "normalizedValue": "Normalized Value" + }, + + "education": { + "commonRecordTypes": { + "title": "Common Record Types", + "description": "A/AAAA records map domains to IP addresses. CNAME creates aliases. MX directs email. TXT stores arbitrary data like SPF policies. SRV specifies service locations." + }, + "validationScope": { + "title": "Validation Scope", + "description": "This validator checks syntax, format, and common configuration issues. It doesn't verify that targets exist or are reachable - use DNS lookup tools for connectivity testing." + }, + "ttlGuidelines": { + "title": "TTL Guidelines", + "description": "Use shorter TTLs (300-3600s) for records that change frequently. Longer TTLs (3600-86400s) reduce DNS queries but slow propagation of changes. Balance based on your needs." + }, + "bestPractices": { + "title": "Best Practices", + "description": "Always use fully qualified domain names (ending with .) in record values. Validate SPF/DMARC policies carefully. Keep MX priorities consistent. Use descriptive TXT record formatting." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-srv-builder.json b/src/lib/i18n/translations/en/tools/dns-srv-builder.json new file mode 100644 index 00000000..008a2a75 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-srv-builder.json @@ -0,0 +1,112 @@ +{ + "title": "SRV Record Builder", + "description": "Compose SRV records with service discovery, protocol specification, priority/weight balancing, and target validation.", + + "input": { + "ttl": { + "label": "Default TTL (seconds)", + "tooltip": "Default Time To Live in seconds for all SRV records" + }, + "addRecordButton": "Add SRV Record", + "recordsTitle": "SRV Records", + "service": { + "label": "Service", + "tooltip": "The service name, typically starting with underscore (e.g., _http, _smtp)", + "customOption": "Custom", + "customPlaceholder": "_myservice" + }, + "protocol": { + "label": "Protocol", + "tooltip": "Transport protocol used by the service (TCP/UDP/TLS/SCTP)", + "tcp": "TCP", + "udp": "UDP", + "tls": "TLS", + "sctp": "SCTP" + }, + "domain": { + "label": "Domain", + "tooltip": "The domain name where this service is located", + "placeholder": "example.com" + }, + "priority": { + "label": "Priority", + "tooltip": "Lower numbers = higher priority" + }, + "weight": { + "label": "Weight", + "tooltip": "Load balancing weight for same priority" + }, + "port": { + "label": "Port", + "tooltip": "Port number where the service is listening (1-65535)" + }, + "target": { + "label": "Target (FQDN)", + "tooltip": "Fully Qualified Domain Name of the server hosting the service (must end with dot)", + "placeholder": "server.example.com." + } + }, + + "validation": { + "serviceEmpty": "Service name cannot be empty", + "serviceUnderscore": "Service name must start with underscore (_)", + "domainEmpty": "Domain name cannot be empty", + "priorityRange": "Priority must be between 0 and 65535", + "weightRange": "Weight must be between 0 and 65535", + "portRange": "Port must be between 1 and 65535", + "targetEmpty": "Target cannot be empty", + "targetFQDN": "Target should end with a dot (FQDN)" + }, + + "examples": { + "title": "Service Examples", + "webServices": "Web Services", + "mailServices": "Mail Services", + "sipServices": "SIP Services", + "xmppServices": "XMPP Services", + "recordsCount": "{count} SRV records" + }, + + "info": { + "title": "SRV Record Structure", + "format": "_service._protocol.domain. TTL IN SRV priority weight port target.", + "serviceLabel": "Service:", + "serviceDescription": "Must start with underscore (e.g., _http, _sip)", + "protocolLabel": "Protocol:", + "protocolDescription": "Usually tcp, udp, tls, or sctp", + "priorityLabel": "Priority:", + "priorityDescription": "Lower values = higher priority (0-65535)", + "weightLabel": "Weight:", + "weightDescription": "Load balancing within same priority (0-65535)", + "portLabel": "Port:", + "portDescription": "Service port number (1-65535)", + "targetLabel": "Target:", + "targetDescription": "FQDN of the server (must end with dot)" + }, + + "results": { + "title": "Generated SRV Records", + "copyButton": "Copy Records", + "tableHeaders": { + "service": "Service", + "serviceTooltip": "Service name and protocol", + "ttl": "TTL", + "ttlTooltip": "Time To Live - how long DNS resolvers should cache this record", + "type": "Type", + "typeTooltip": "DNS record type (always SRV for service records)", + "priority": "Priority", + "priorityTooltip": "Priority - lower values are preferred (0-65535)", + "weight": "Weight", + "weightTooltip": "Weight for load balancing among same priority records (0-65535)", + "port": "Port", + "portTooltip": "Port number where the service is available", + "target": "Target", + "targetTooltip": "Target server hostname (FQDN)", + "status": "Status", + "statusTooltip": "Validation status of this SRV record" + }, + "statusValid": "Valid", + "statusIssues": "Issues", + "validationSummaryTitle": "Configuration Issues" + } +} diff --git a/src/lib/i18n/translations/en/tools/dns-txt-escape.json b/src/lib/i18n/translations/en/tools/dns-txt-escape.json new file mode 100644 index 00000000..b2777b52 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dns-txt-escape.json @@ -0,0 +1,79 @@ +{ + "title": "TXT Record Escape Tool", + "description": "Safely escape and split TXT record strings into DNS-compatible chunks (≀255 characters each).", + + "input": { + "textToEscape": { + "label": "Text to Escape", + "tooltip": "The original text that will be escaped and split into chunks", + "placeholder": "Enter your raw text here..." + }, + "maxChunkLength": { + "label": "Max Chunk Length", + "tooltip": "Maximum length for each chunk (DNS TXT record limit is 255 characters)" + }, + "escapeOptions": { + "title": "Escape Options", + "tooltip": "Configure how the text should be escaped for DNS compatibility", + "escapeQuotes": { + "label": "Escape Quotes (\")", + "tooltip": "Escape double quote characters as \\\"" + }, + "escapeBackslashes": { + "label": "Escape Backslashes (\\)", + "tooltip": "Escape backslash characters as \\\\" + }, + "preserveSpaces": { + "label": "Preserve Spacing", + "tooltip": "Keep original whitespace formatting instead of normalizing spaces" + } + } + }, + + "validation": { + "emptyText": "Please enter text to escape", + "invalidChunkLength": "Chunk length must be between 1 and 255 characters", + "oversizedChunks": "{count} chunk(s) exceed the maximum length after escaping", + "manyChunks": "Text split into {count} chunks (consider splitting across multiple TXT records)", + "success": "Text successfully split into {count} chunk(s)" + }, + + "results": { + "chunksTitle": "Escaped Chunks ({count})", + "totalLengthTooltip": "Total length after escaping", + "totalLengthLabel": "{length} chars", + "chunkNumber": "Chunk {number}", + "copyChunkTooltip": "Copy this chunk to clipboard", + "dnsRecordTitle": "DNS Record Format", + "copyButton": "Copy", + "copyButtonTooltip": "Copy single-line DNS record format", + "exportButton": "Export", + "exportButtonTooltip": "Download as zone file", + "singleLineFormat": "Single Line Format:", + "zoneFileFormat": "Zone File Format:" + }, + + "examples": { + "title": "Example Texts", + "spf": { + "name": "SPF Record", + "description": "Sender Policy Framework record for email authentication", + "value": "v=spf1 include:_spf.google.com include:mailgun.org include:servers.mcsv.net ~all" + }, + "dkim": { + "name": "DKIM Key", + "description": "DomainKeys Identified Mail public key record", + "value": "k=rsa; t=s; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDGGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6+nQKQ5R7fPqqJLmPjGYGqwVF6" + }, + "domainVerification": { + "name": "Domain Verification", + "description": "Google domain ownership verification token", + "value": "google-site-verification=rXOxyZounnZasA8Z7oaD3c14JdjS9aKSWvsR1EbUSIQ" + }, + "longText": { + "name": "Long Text Sample", + "description": "Text that will need to be split into multiple chunks", + "value": "This is a very long text string that will definitely exceed the 255 character limit for DNS TXT records and will need to be properly escaped and split into multiple chunks. The escaping tool should handle this automatically and show you exactly how many chunks are created and what the final DNS record format will look like when you publish it to your DNS provider." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/dnskey-key-tag.json b/src/lib/i18n/translations/en/tools/dnskey-key-tag.json new file mode 100644 index 00000000..a7226e4c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/dnskey-key-tag.json @@ -0,0 +1,84 @@ +{ + "title": "DNSKEY Key Tag Calculator", + "description": "Compute the DNSKEY key tag from a DNSKEY RR (RFC 4034 algorithm) and display it alongside key metadata for DNSSEC validation purposes.", + + "examples": { + "title": "DNSKEY Examples", + "ksk": { + "title": "KSK Example (Algorithm 8 - RSASHA256)", + "description": "Key Signing Key with SEP flag set" + }, + "zsk": { + "title": "ZSK Example (Algorithm 13 - ECDSAP256SHA256)", + "description": "Zone Signing Key for data signing" + }, + "rdataOnly": { + "title": "RDATA Only Format", + "description": "DNSKEY record data without owner name" + } + }, + + "input": { + "label": "DNSKEY Record", + "tooltip": "Enter a DNSKEY record in standard format (e.g., 'example.org. 3600 IN DNSKEY 257 3 8 AwEAAag') to calculate its key tag", + "placeholder": "example.org. 3600 IN DNSKEY 257 3 8 AwEAAc...", + "exampleActive": "Using example data - modify to see your results" + }, + + "errors": { + "validationError": "Validation Error:", + "failedParse": "Failed to parse DNSKEY record" + }, + + "results": { + "title": "Key Tag Calculation", + "copyButton": "Copy Key Tag", + "keyTagLabel": "Key Tag", + "keyTagTooltip": "A 16-bit identifier calculated from the DNSKEY used to quickly identify which key was used to generate a signature" + }, + + "metadata": { + "title": "DNSKEY Metadata", + "keyType": { + "label": "Key Type", + "tooltip": "KSK (Key Signing Key) signs other keys and has the SEP flag set (257). ZSK (Zone Signing Key) signs zone data and has no SEP flag (256).", + "unknown": "Unknown" + }, + "flags": { + "label": "Flags", + "tooltip": "16-bit flags field. Bit 15 (SEP flag) indicates if this is a Key Signing Key. 257 = KSK, 256 = ZSK." + }, + "protocol": { + "label": "Protocol", + "tooltip": "Protocol field for DNSKEY records. Must always be 3 (DNSSEC)." + }, + "algorithm": { + "label": "Algorithm", + "tooltip": "Cryptographic algorithm used by this key. Common values: 8 (RSASHA256), 13 (ECDSA P-256), 15 (Ed25519).", + "unknown": "Unknown" + }, + "publicKey": { + "label": "Public Key (Base64)", + "tooltip": "The actual cryptographic public key data encoded in Base64 format. This is used for signature verification." + } + }, + + "education": { + "purpose": { + "title": "Key Tag Purpose", + "description": "The key tag is a short identifier used to quickly identify which DNSKEY was used to generate a signature. It's calculated using a checksum algorithm defined in RFC 4034 and helps optimize DNSSEC validation by avoiding the need to test every key." + }, + "keyTypes": { + "title": "Key Types", + "description": "KSK (Key Signing Key): Used to sign other keys (ZSKs). Has the SEP flag set (bit 15). ZSK (Zone Signing Key): Used to sign zone data. Does not have the SEP flag set." + }, + "algorithmSupport": { + "title": "Algorithm Support", + "description": "Supports all modern DNSSEC algorithms including RSASHA256 (8), RSASHA512 (10), ECDSA P-256 (13), ECDSA P-384 (14), and Ed25519 (15). Legacy algorithms like RSAMD5 are deprecated and should not be used." + }, + "validation": { + "title": "Validation Process", + "description": "The tool validates DNSKEY format, checks protocol compliance (must be 3), verifies algorithm support, and ensures proper base64 encoding of the public key before calculating the key tag." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/eui64.json b/src/lib/i18n/translations/en/tools/eui64.json new file mode 100644 index 00000000..f028ad2b --- /dev/null +++ b/src/lib/i18n/translations/en/tools/eui64.json @@ -0,0 +1,88 @@ +{ + "title": "EUI-64 Converter", + "description": "Convert between MAC addresses and IPv6 EUI-64 interface identifiers with automatic IPv6 address generation", + "input": { + "title": "Address Conversion", + "addresses": { + "label": "MAC Addresses or EUI-64 Identifiers", + "tooltip": "Enter MAC addresses (48-bit) or EUI-64 identifiers (64-bit)", + "placeholder": "00:1A:2B:3C:4D:5E\n02:1A:2B:FF:FE:3C:4D:5F\n08:00:27:12:34:56", + "help": "Enter MAC addresses (48-bit) or EUI-64 identifiers (64-bit) one per line. Various formats supported: xx:xx:xx:xx:xx:xx or xx-xx-xx-xx-xx-xx" + }, + "globalPrefix": { + "label": "IPv6 Global Prefix (Optional)", + "tooltip": "IPv6 network prefix for generating global addresses (e.g., 2001:db8::/64)", + "placeholder": "2001:db8::/64", + "help": "IPv6 prefix for generating global unicast addresses. Leave empty to use example prefix." + } + }, + "info": { + "title": "EUI-64 Information", + "description": "EUI-64 (Extended Unique Identifier 64-bit) is used to generate IPv6 interface identifiers from MAC addresses:", + "steps": { + "split": "Split MAC address: OUI (24 bits) + Device ID (24 bits)", + "insert": "Insert FFFE between OUI and Device ID", + "flip": "Flip the Universal/Local bit (bit 1) in the first octet", + "result": "Result: 64-bit interface identifier for IPv6" + } + }, + "processing": "Converting addresses...", + "results": { + "errors": { + "title": "Errors" + }, + "summary": { + "title": "Conversion Summary", + "totalInputs": "Total Inputs", + "valid": "Valid", + "invalid": "Invalid", + "macToEUI64": "MAC β†’ EUI-64", + "eui64ToMAC": "EUI-64 β†’ MAC" + }, + "conversions": { + "title": "Address Conversions" + } + }, + "actions": { + "exportCSV": "Export CSV", + "exportJSON": "Export JSON", + "copyInput": "Copy input", + "copyMAC": "Copy MAC address", + "copyEUI64": "Copy EUI-64 address", + "copyLinkLocal": "Copy link-local address", + "copyGlobal": "Copy global address" + }, + "csvHeaders": { + "input": "Input", + "type": "Type", + "macAddress": "MAC Address", + "eui64": "EUI-64", + "ipv6LinkLocal": "IPv6 Link-Local", + "ipv6Global": "IPv6 Global", + "universalLocal": "Universal/Local", + "unicastMulticast": "Unicast/Multicast", + "valid": "Valid", + "error": "Error" + }, + "conversion": { + "details": { + "title": "Conversion Details", + "macAddress": "MAC Address", + "eui64Address": "EUI-64 Address", + "ipv6LinkLocal": "IPv6 Link-Local", + "ipv6Global": "IPv6 Global", + "universalLocal": "Universal/Local", + "unicastMulticast": "Unicast/Multicast", + "bits": { + "title": "Bit Analysis", + "universal": "Universal", + "local": "Local", + "unicast": "Unicast", + "multicast": "Multicast" + } + } + }, + "errors": { + "unknownError": "Unknown error occurred during conversion" + } +} diff --git a/src/lib/i18n/translations/en/tools/free-space-finder.json b/src/lib/i18n/translations/en/tools/free-space-finder.json new file mode 100644 index 00000000..14abb6d1 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/free-space-finder.json @@ -0,0 +1,95 @@ +{ + "title": "Free Space Finder", + "description": "Find available IP address space within pools by analyzing allocated blocks and identifying gaps.", + "input": { + "pools": { + "label": "IP Address Pools", + "placeholder": "192.168.0.0/16\n10.0.0.0/8", + "help": "Enter CIDR blocks representing total available address space" + }, + "allocations": { + "label": "Allocated Blocks", + "placeholder": "192.168.1.0/24\n192.168.10.0/24\n10.0.0.0/16", + "help": "Enter already allocated subnets within the pools" + }, + "targetPrefix": { + "label": "Target Prefix Length (optional)", + "placeholder": "24", + "help": "Filter results to show only blocks of specific size" + }, + "actions": { + "clearFilter": "Clear Filter" + } + }, + "examples": { + "title": "Quick Examples", + "officeNetworkGaps": { + "label": "Office Network Gaps", + "description": "Find /24 gaps in corporate network" + }, + "largePoolAnalysis": { + "label": "Large Pool Analysis", + "description": "Analyze large address pool utilization" + }, + "homeNetworkSpace": { + "label": "Home Network Space", + "description": "Find available space in home network" + }, + "ipv6Planning": { + "label": "IPv6 Planning", + "description": "IPv6 address space analysis" + }, + "datacenterInventory": { + "label": "Datacenter Inventory", + "description": "Datacenter IP space audit" + } + }, + "actions": { + "findSpace": "Find Free Space", + "analyzing": "Analyzing...", + "copyAll": "Copy All Blocks", + "exportResults": "Export Results", + "copied": "Copied!" + }, + "results": { + "title": "Available Address Space", + "blocks": "Available Blocks", + "addresses": "Total Addresses", + "summary": { + "title": "Space Analysis Summary", + "totalBlocks": "Available Blocks", + "totalAddresses": "Total Free Addresses", + "largestBlock": "Largest Block", + "utilizationRate": "Utilization Rate" + }, + "blocks": { + "title": "Free Address Blocks", + "count": "({count})", + "block": "Block", + "size": "Size", + "addresses": "Addresses", + "copyTooltip": "Copy block" + }, + "noResults": "No free space found matching criteria", + "filtered": "Showing blocks with /{prefix} prefix" + }, + "visualization": { + "title": "Address Space Visualization", + "allocated": "Allocated", + "available": "Available", + "pool": "Pool Boundary" + }, + "errors": { + "title": "Analysis Errors", + "invalidPool": "Invalid pool specification", + "invalidAllocation": "Invalid allocation", + "analysisError": "Space analysis failed", + "noPoolsDefined": "No address pools defined" + }, + "stats": { + "poolUtilization": "Pool Utilization", + "fragmentationIndex": "Fragmentation Index", + "averageBlockSize": "Average Block Size", + "largestContiguousSpace": "Largest Contiguous Space" + } +} diff --git a/src/lib/i18n/translations/en/tools/freeform-tlv-builder.json b/src/lib/i18n/translations/en/tools/freeform-tlv-builder.json new file mode 100644 index 00000000..5d86649e --- /dev/null +++ b/src/lib/i18n/translations/en/tools/freeform-tlv-builder.json @@ -0,0 +1,122 @@ +{ + "title": "Freeform TLV Composer", + "subtitle": "Build custom DHCP options using Type-Length-Value encoding. Support for IPv4, IPv6, FQDN, strings, hex data, and numeric types with live hex preview.", + + "optionConfig": { + "title": "Option Configuration", + "optionCode": { + "label": "Option Code", + "placeholder": "224", + "hint": "DHCP option number (0-255, recommend 224-254 for custom)" + }, + "optionName": { + "label": "Option Name", + "placeholder": "e.g., Custom Server Option", + "hint": "Descriptive name for this option" + } + }, + + "dataItems": { + "title": "Data Items", + "hint": "Add multiple data items to build the option payload. Each item will be encoded sequentially.", + "item": "Item {number}", + "dataType": "Data Type", + "value": "Value", + "addButton": "Add Data Item", + + "dataTypes": { + "ipv4": { + "label": "IPv4 Address", + "description": "4 bytes, e.g., 192.168.1.1", + "placeholder": "192.168.1.1" + }, + "ipv6": { + "label": "IPv6 Address", + "description": "16 bytes, e.g., 2001:db8::1", + "placeholder": "2001:db8::1" + }, + "fqdn": { + "label": "Domain Name (FQDN)", + "description": "DNS wire format with length prefixes", + "placeholder": "example.com" + }, + "string": { + "label": "String (UTF-8)", + "description": "Text encoded as UTF-8 bytes", + "placeholder": "Enter text" + }, + "hex": { + "label": "Raw Hex", + "description": "Direct hex bytes", + "placeholder": "deadbeef or DE AD BE EF" + }, + "uint8": { + "label": "UInt8", + "description": "1 byte unsigned integer (0-255)", + "placeholder": "0-255" + }, + "uint16": { + "label": "UInt16", + "description": "2 byte unsigned integer (0-65535)", + "placeholder": "0-65535" + }, + "uint32": { + "label": "UInt32", + "description": "4 byte unsigned integer (0-4294967295)", + "placeholder": "0-4294967295" + }, + "boolean": { + "label": "Boolean", + "description": "1 byte (0 or 1)", + "placeholder": "0, 1, true, or false" + } + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Encoded Option", + "summary": { + "optionCode": "Option Code:", + "optionName": "Option Name:", + "dataLength": "Data Length:", + "items": "Items:", + "bytes": "{length} bytes" + }, + "hexEncoded": "Hex-Encoded (Compact)", + "wireFormat": "Wire Format (Spaced)", + "byteBreakdown": "Byte Breakdown", + "configExamples": "Configuration Examples", + "iscDhcpd": "ISC dhcpd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration" + }, + + "about": { + "title": "About TLV Encoding", + "intro": "Type-Length-Value (TLV) is a common encoding scheme used in DHCP options. This tool allows you to compose custom DHCP options by combining multiple data items of different types.", + "types": { + "ipv4Ipv6": "IPv4/IPv6:", + "ipv4Ipv6Desc": "Network addresses encoded as raw bytes", + "fqdn": "FQDN:", + "fqdnDesc": "Domain names in DNS wire format (length-prefixed labels)", + "string": "String:", + "stringDesc": "UTF-8 encoded text", + "uintTypes": "UInt8/16/32:", + "uintTypesDesc": "Unsigned integers of various sizes", + "boolean": "Boolean:", + "booleanDesc": "Single byte (0x00 or 0x01)", + "hex": "Hex:", + "hexDesc": "Raw hexadecimal bytes for custom data" + }, + "outro": "The generated hex output represents the option data only. DHCP servers will automatically add the option code and length fields when sending the option to clients." + }, + + "common": { + "required": "*", + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/gateway-option3.json b/src/lib/i18n/translations/en/tools/gateway-option3.json new file mode 100644 index 00000000..70e8dcb5 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/gateway-option3.json @@ -0,0 +1,87 @@ +{ + "title": "DHCP Option 3 - Router/Default Gateway", + "subtitle": "Build and decode default gateway configuration. Multiple gateways can be specified for redundancy or load balancing, listed in order of preference.", + + "nav": { + "build": "Build Option", + "decode": "Decode Option" + }, + + "examples": { + "decode": { + "singleGateway": { + "label": "Single Gateway", + "description": "192.168.1.1" + }, + "dualGateways": { + "label": "Dual Gateways", + "description": "192.168.1.1, 192.168.1.2" + }, + "googleDNS": { + "label": "Google DNS Primary", + "description": "8.8.8.8" + }, + "commonRouter": { + "label": "Common Home Router", + "description": "192.168.0.10" + } + } + }, + + "build": { + "title": "Gateway Configuration", + + "subnet": { + "label": "Subnet (Optional - for validation)", + "placeholder": "e.g., 192.168.1.0/24", + "hint": "If provided, gateways will be validated against this subnet" + }, + + "gateways": { + "label": "Gateway Addresses (in order of preference)", + "placeholder": "e.g., 192.168.1.1", + "addButton": "Add Gateway", + "removeButton": "Remove" + }, + + "errors": { + "title": "Validation Errors:" + } + }, + + "decode": { + "title": "Decode Option 3", + + "hexInput": { + "label": "Hex String", + "placeholder": "e.g., c0a80101 or c0 a8 01 01", + "hint": "Enter hex bytes (spaces optional)" + }, + + "error": { + "title": "Decode Error:" + } + }, + + "results": { + "buildTitle": "Option 3 - Router", + "decodeTitle": "Decoded Option 3", + + "gateways": "Gateways:", + "hexEncoded": "Hex Encoded:", + "wireFormat": "Wire Format:", + "totalLength": "Total Length:", + "lengthBytes": "{length} bytes", + "gatewayCount": "Gateway Count:", + + "configExamples": "Configuration Examples", + "iscDhcpd": "ISC DHCPd", + "keaDhcp4": "Kea DHCPv4", + "dnsmasq": "dnsmasq" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/iaid-calculator.json b/src/lib/i18n/translations/en/tools/iaid-calculator.json new file mode 100644 index 00000000..237a0c5a --- /dev/null +++ b/src/lib/i18n/translations/en/tools/iaid-calculator.json @@ -0,0 +1,80 @@ +{ + "title": "IAID Calculator", + "subtitle": "Calculate Identity Association Identifier (IAID) for DHCPv6 interfaces. Generate IAIDs from interface index, name, MAC address, or custom values with OS-specific conventions.", + + "config": { + "title": "IAID Configuration", + "hint": "Select method to generate Identity Association Identifier", + + "method": { + "label": "Generation Method", + "options": { + "interfaceIndex": "Interface Index", + "interfaceName": "Interface Name (hash)", + "macAddress": "MAC Address (hash)", + "custom": "Custom Value" + } + }, + + "interfaceIndex": { + "label": "Interface Index", + "placeholder": "e.g., 2 for eth0, 3 for wlan0", + "hint": "Network interface index (0-4294967295)" + }, + + "interfaceName": { + "label": "Interface Name", + "placeholder": "e.g., eth0, wlan0, enp3s0", + "hint": "Network interface name (will be hashed to generate IAID)" + }, + + "macAddress": { + "label": "MAC Address", + "placeholder": "00:0c:29:4f:a3:d2", + "hint": "Hardware address (last 4 bytes used for IAID)" + }, + + "customValue": { + "label": "Custom IAID Value", + "placeholder": "Enter value between 0 and 4294967295", + "hint": "32-bit unsigned integer (0-4294967295)" + } + }, + + "errors": { + "title": "Validation Errors" + }, + + "results": { + "title": "Calculated IAID", + + "summary": { + "method": "Method:", + "iaid": "IAID:" + }, + + "hex": "Hexadecimal", + "decimal": "Decimal", + "binary": "Binary", + + "osConventions": { + "title": "OS-Specific Conventions", + "hint": "How different operating systems typically generate IAIDs" + }, + + "configExamples": { + "keaDhcp6": "Kea DHCPv6 Configuration", + "iscDhcpd": "ISC DHCPd Configuration" + } + }, + + "namingGuide": { + "title": "Network Interface Naming Guide", + "hint": "Common interface naming conventions across different operating systems" + }, + + "common": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/idn-punycode-converter.json b/src/lib/i18n/translations/en/tools/idn-punycode-converter.json new file mode 100644 index 00000000..9cd9f0c5 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/idn-punycode-converter.json @@ -0,0 +1,78 @@ +{ + "title": "IDN Punycode Converter", + "description": "Convert between Unicode domain names and Punycode (ASCII-compatible encoding)", + "modes": { + "unicodeToPunycode": "Unicode β†’ Punycode", + "punycodeToUnicode": "Punycode β†’ Unicode" + }, + "examples": { + "title": "Example Domains", + "german": "German city domain", + "japanese": "Japanese domain", + "russian": "Russian domain", + "arabic": "Arabic domain", + "korean": "Korean domain", + "greek": "Greek domain" + }, + "conversion": { + "unicodeTitle": "Unicode β†’ Punycode Conversion", + "punycodeTitle": "Punycode β†’ Unicode Conversion", + "unicodeLabel": "Unicode Domain Name", + "punycodeLabel": "Punycode Domain Name", + "unicodeTooltip": "Enter the domain name you want to convert", + "unicodePlaceholder": "mΓΌnchen.de", + "punycodePlaceholder": "xn--mnchen-3ya.de", + "unicodeHelp": "Enter Unicode domain names with international characters", + "punycodeHelp": "Enter domain names containing xn-- punycode labels", + "unicodeResult": "Unicode Result", + "punycodeResult": "Punycode Result", + "swapTooltip": "Swap input and output", + "reverse": "Reverse", + "placeholder": "Enter a domain name to see the conversion result", + "error": "Error: Invalid input" + }, + "warnings": { + "title": "Notices", + "asciiOnly": "Input contains only ASCII characters - no conversion needed", + "noPunycode": "Input does not contain punycode domains (xn-- prefix)", + "tooLong": "Domain name exceeds maximum length of 253 characters" + }, + "actions": { + "copyResult": "Copy Result", + "copied": "Copied!", + "download": "Download", + "downloaded": "Downloaded!" + }, + "info": { + "aboutTitle": "About IDN and Punycode", + "aboutDescription": "Internationalized Domain Names (IDN) allow domain names to contain Unicode characters from various scripts and languages. Punycode is the ASCII-compatible encoding that represents Unicode domain labels, allowing non-ASCII domain names to work with existing DNS infrastructure.", + "howItWorksTitle": "How Punycode Works", + "howItWorksDescription": "Each Unicode label is encoded separately:", + "unicode": "Unicode:", + "encoded": "Encoded:", + "final": "Final:", + "xnPrefix": "The xn-- prefix identifies punycode labels", + "useCasesTitle": "Common Use Cases", + "useCases": { + "registration": "International domain registration", + "email": "Email address internationalization", + "dns": "DNS configuration", + "webDev": "Web application development", + "security": "Security analysis" + }, + "featuresTitle": "Supported Features", + "features": { + "compliant": "RFC 3492 compliant Punycode encoding/decoding", + "bidirectional": "Bidirectional conversion (Unicode ↔ Punycode)", + "multipleLabels": "Multiple domain labels support", + "mixed": "Mixed ASCII/Unicode domain handling" + }, + "securityTitle": "Security Considerations", + "security": { + "homograph": "Homograph attacks: visually similar characters from different scripts", + "validate": "Always validate and normalize IDN input in applications", + "mixedScript": "Consider implementing mixed-script detection", + "browserPolicy": "Be aware of browser IDN display policies" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/ip-converter.json b/src/lib/i18n/translations/en/tools/ip-converter.json new file mode 100644 index 00000000..999c2564 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ip-converter.json @@ -0,0 +1,135 @@ +{ + "title": "IP Address Converter", + "description": "Convert IP addresses between different number formats.", + + "input": { + "ipAddress": "IP Address", + "placeholder": "192.168.1.1" + }, + + "errors": { + "mustBeValidNumber": "Must be a valid number", + "decimalOutOfRange": "Must be between 0 and 4,294,967,295", + "invalidDecimal": "Invalid decimal value", + "invalidBinary": "Invalid binary format", + "invalidHex": "Invalid hexadecimal format", + "binaryOnlyDigits": "Only 0, 1, dots, and spaces allowed", + "binaryMust32Bits": "Must be exactly 32 binary digits (8 digits per octet)", + "binaryDottedFormat": "Must use dotted format: 8bits.8bits.8bits.8bits", + "binaryOctetLength": "Octet {octet} must be exactly 8 bits", + "hexOnlyDigits": "Only hex digits (0-9, A-F), dots, and x allowed", + "hexMust8Digits": "Must be exactly 8 hex digits (2 digits per octet)", + "hexDottedFormat": "Must use dotted format: 0xXX.0xXX.0xXX.0xXX", + "hexOctetLength": "Octet {octet} must be exactly 2 hex digits", + "hexInvalidDigits": "Octet {octet} contains invalid hex digits" + }, + + "formats": { + "binary": { + "title": "Binary Format", + "badge": "Binary (Base-2)", + "whatItIs": "Uses only digits 0 and 1, representing how computers store IP addresses at the hardware level", + "example": "192.168.1.1 = 11000000.10101000.00000001.00000001", + "usage": "Low-level networking, subnet calculations, understanding binary operations", + "howToRead": "Each octet is 8 bits. Binary 11000000 = 128+64 = 192 decimal" + }, + "decimal": { + "title": "Decimal Format", + "badge": "Decimal (Base-10)", + "whatItIs": "The entire IP as a single large number (0-4,294,967,295)", + "example": "192.168.1.1 = 3,232,235,777", + "usage": "Database storage, mathematical operations, IP range calculations", + "calculation": "(192Γ—256Β³) + (168Γ—256Β²) + (1Γ—256) + 1 = 3,232,235,777" + }, + "hexadecimal": { + "title": "Hexadecimal Format", + "badge": "Hexadecimal (Base-16)", + "whatItIs": "Uses digits 0-9 and letters A-F, common in programming and low-level operations", + "example": "192.168.1.1 = 0xC0.0xA8.0x01.0x01", + "usage": "Programming, system logs, network debugging, firmware configuration", + "conversion": "192 = C0 hex, 168 = A8 hex. Each hex digit represents 4 binary bits" + }, + "octal": { + "title": "Octal Format", + "badge": "Octal (Base-8)", + "whatItIs": "Uses digits 0-7, less common but still found in some configurations", + "example": "192.168.1.1 = 0300.0250.001.001", + "usage": "Legacy Unix configurations, file permissions, some network tools", + "note": "Leading zeros indicate octal format. 0300 octal = 192 decimal" + } + }, + + "ipClasses": { + "title": "IP Class Information", + "description": "IP address classes are historical categories that determine network size and usage patterns:", + "classA": { + "badge": "Class A", + "range": "1.0.0.0 to 126.255.255.255", + "defaultMask": "255.0.0.0 (/8)", + "networks": "126 networks, 16.7 million hosts each", + "usage": "Large organizations, ISPs, government networks" + }, + "classB": { + "badge": "Class B", + "range": "128.0.0.0 to 191.255.255.255", + "defaultMask": "255.255.0.0 (/16)", + "networks": "16,384 networks, 65,534 hosts each", + "usage": "Universities, medium-large organizations" + }, + "classC": { + "badge": "Class C", + "range": "192.0.0.0 to 223.255.255.255", + "defaultMask": "255.255.255.0 (/24)", + "networks": "2.1 million networks, 254 hosts each", + "usage": "Small businesses, home networks" + }, + "specialRanges": { + "title": "Special Ranges", + "classD": "Class D (224-239): Multicast addresses for group communication", + "classE": "Class E (240-255): Reserved for experimental use", + "privateNetworks": "Private Networks: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16", + "loopback": "Loopback: 127.0.0.0/8 (localhost addresses)" + } + }, + + "useCases": { + "title": "When to Use Each Format", + "networkAdmin": { + "title": "Network Administration", + "dottedDecimal": "Daily configuration and documentation", + "binary": "Subnet calculations and VLSM planning", + "hexadecimal": "Debugging network captures and logs" + }, + "programming": { + "title": "Programming & Development", + "decimal": "Database storage and IP range operations", + "hexadecimal": "Low-level socket programming", + "binary": "Bitwise operations and subnet masking" + }, + "troubleshooting": { + "title": "Troubleshooting & Analysis", + "binary": "Understanding subnet boundaries", + "hexadecimal": "Reading network packet captures", + "decimal": "Quick IP range calculations" + } + }, + + "explanations": { + "title": "Number Format Explanations", + "ipClassesTitle": "IP Address Classes", + "whatItIs": "What it is:", + "example": "Example:", + "usage": "Usage:", + "howToRead": "How to read:", + "calculation": "Calculation:", + "conversion": "Conversion:", + "note": "Note:", + "range": "Range:", + "defaultMask": "Default Mask:", + "networks": "Networks:", + "dottedDecimal": "Dotted Decimal:", + "decimal": "Decimal:", + "binary": "Binary:", + "hexadecimal": "Hexadecimal:" + } +} diff --git a/src/lib/i18n/translations/en/tools/ip-distance.json b/src/lib/i18n/translations/en/tools/ip-distance.json new file mode 100644 index 00000000..22da0d15 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ip-distance.json @@ -0,0 +1,69 @@ +{ + "title": "IP Address Distance Calculator", + "description": "Calculate the numerical distance between two IP addresses in the same address family.", + "input": { + "firstIP": { + "label": "First IP Address", + "placeholder": "192.168.1.1" + }, + "secondIP": { + "label": "Second IP Address", + "placeholder": "192.168.1.100" + }, + "help": "Enter two IP addresses of the same type (both IPv4 or both IPv6)" + }, + "examples": { + "title": "Quick Examples", + "sameSubnet": { + "label": "Same Subnet", + "description": "Adjacent addresses in same network" + }, + "differentSubnets": { + "label": "Different Subnets", + "description": "Addresses across subnet boundaries" + }, + "ipv6Distance": { + "label": "IPv6 Distance", + "description": "IPv6 address distance calculation" + }, + "largeDistance": { + "label": "Large Distance", + "description": "Addresses with significant separation" + } + }, + "results": { + "title": "Distance Calculation", + "distance": "Distance", + "addresses": "addresses apart", + "direction": { + "label": "Direction", + "forward": "Forward (first β†’ second)", + "backward": "Backward (second β†’ first)" + }, + "details": { + "title": "Calculation Details", + "firstAddress": "First Address", + "secondAddress": "Second Address", + "numericalDifference": "Numerical Difference", + "addressFamily": "Address Family" + } + }, + "actions": { + "calculate": "Calculate Distance", + "calculating": "Calculating...", + "swap": "Swap Addresses", + "copy": "Copy", + "copied": "Copied!" + }, + "errors": { + "title": "Calculation Error", + "invalidFirstIP": "Invalid first IP address", + "invalidSecondIP": "Invalid second IP address", + "mixedFamilies": "Both addresses must be same type (IPv4 or IPv6)", + "calculationFailed": "Distance calculation failed" + }, + "tooltips": { + "distance": "Number of IP addresses between the two inputs", + "swap": "Exchange the first and second IP addresses" + } +} diff --git a/src/lib/i18n/translations/en/tools/ip-enumerate.json b/src/lib/i18n/translations/en/tools/ip-enumerate.json new file mode 100644 index 00000000..d16310a7 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ip-enumerate.json @@ -0,0 +1,85 @@ +{ + "title": "IP Address Enumerator", + "description": "Generate all IP addresses in a CIDR block, range, or network with customizable display limits.", + "input": { + "label": "Network Input", + "placeholder": "192.168.1.0/28 or 10.0.0.1-10.0.0.50", + "help": "Enter CIDR block, IP range, or single address" + }, + "options": { + "maxDisplay": "Maximum addresses to display", + "maxDisplayTooltip": "Limit display for performance (max 10,000)", + "includeNetwork": "Include network address", + "includeNetworkTooltip": "Include the first address (network identifier)", + "includeBroadcast": "Include broadcast address", + "includeBroadcastTooltip": "Include the last address (broadcast)", + "network": "Network", + "broadcast": "Broadcast" + }, + "examples": { + "title": "Quick Examples", + "smallSubnet": { + "label": "Small Subnet /28", + "description": "16 addresses" + }, + "standardSubnet": { + "label": "Standard /24", + "description": "256 addresses" + }, + "ipRange": { + "label": "IP Range", + "description": "Custom range enumeration" + }, + "singleAddress": { + "label": "Single Address", + "description": "Just one address" + }, + "largeBlock": { + "label": "Large Block /16", + "description": "65,536 addresses (display limited)" + } + }, + "actions": { + "enumerate": "Enumerate Addresses", + "enumerating": "Enumerating...", + "generating": "Generating addresses...", + "copyAll": "Copy All", + "exportTxt": "Export TXT", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + "copied": "Copied!" + }, + "results": { + "title": "Enumerated Addresses", + "networkInfo": { + "title": "Network Information", + "type": "Type", + "network": "Network Address", + "broadcast": "Broadcast Address", + "firstUsable": "First Usable", + "lastUsable": "Last Usable", + "totalHosts": "Total Hosts" + }, + "addresses": { + "title": "IP Addresses", + "showing": "Showing {displayed} of {total} addresses", + "truncated": "Display limited to first {limit} addresses", + "copyTooltip": "Copy address" + } + }, + "errors": { + "title": "Error", + "invalidInput": "Invalid input format", + "tooManyAddresses": "Too many addresses to generate (max {max})", + "processingError": "Error processing input" + }, + "warnings": { + "largeGeneration": "Generating {count} addresses may take some time", + "performanceWarning": "Large networks may impact browser performance" + }, + + "safety": { + "title": "Performance Notice", + "message": "Large networks will be truncated to prevent browser slowdown" + } +} diff --git a/src/lib/i18n/translations/en/tools/ip-regex-generator.json b/src/lib/i18n/translations/en/tools/ip-regex-generator.json new file mode 100644 index 00000000..e3abc4a1 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ip-regex-generator.json @@ -0,0 +1,83 @@ +{ + "title": "IP Regex Generator", + "description": "Generate safe and reliable regular expressions for IPv4 and IPv6 address validation", + + "modes": { + "simple": "Simple Mode", + "advanced": "Advanced Mode" + }, + + "types": { + "title": "IP Address Type", + "ipv4Only": "IPv4 Only", + "ipv6Only": "IPv6 Only", + "both": "Both IPv4 & IPv6" + }, + + "options": { + "title": "Advanced Options" + }, + + "results": { + "title": "Generated Pattern", + "patternLabel": "Regular Expression", + "edit": "Edit", + "copy": "Copy", + "copied": "Copied!", + "testUrl": "Test on Regex101" + }, + + "editor": { + "title": "Edit Regular Expression", + "patternLabel": "Pattern:", + "patternPlaceholder": "Enter regex pattern...", + "flagsLabel": "Flags:", + "flagsPlaceholder": "g, i, m, etc.", + "apply": "Apply", + "cancel": "Cancel", + "previewLabel": "Preview:", + "validPattern": "Valid regex pattern", + "applyChanges": "Apply changes", + "cancelEditing": "Cancel editing", + "fixErrors": "Fix validation errors first" + }, + + "testCases": { + "title": "Test Cases", + "validTitle": "Valid Cases", + "invalidTitle": "Invalid Cases", + "addValid": "Add Valid", + "addInvalid": "Add Invalid", + "addValidPlaceholder": "Add new valid test case...", + "addInvalidPlaceholder": "Add new invalid test case...", + "editTestCases": "Edit Test Cases", + "applyTestChanges": "Apply Test Changes", + "cancelTestEditing": "Cancel Test Editing", + "matches": "matches", + "doesNotMatch": "does not match", + "all": "All", + "passed": "passed", + "of": "of" + }, + + "languageExamples": { + "title": "Language Examples", + "javascript": "JavaScript", + "python": "Python", + "java": "Java", + "csharp": "C#", + "php": "PHP", + "copy": "Copy" + }, + + "errors": { + "patternEmpty": "Pattern cannot be empty", + "invalidRegex": "Invalid regex" + }, + + "tooltips": { + "editPattern": "Edit the generated regex pattern", + "copyPattern": "Copy regex to clipboard", + "testRegex": "Test this regex on Regex101" + } +} diff --git a/src/lib/i18n/translations/en/tools/ip-validator.json b/src/lib/i18n/translations/en/tools/ip-validator.json new file mode 100644 index 00000000..07fdaa8b --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ip-validator.json @@ -0,0 +1,86 @@ +{ + "title": "IP Address Validator", + "description": "Validate IPv4 and IPv6 addresses with detailed error analysis and format checking", + "input": { + "label": "IP Address to Validate", + "placeholder": "Enter IPv4 or IPv6 address", + "help": "Enter any IP address format for comprehensive validation" + }, + "examples": { + "title": "Test Cases", + "validIPv4": "Valid IPv4", + "validIPv6": "Valid IPv6", + "ipv4LeadingZeros": "IPv4 with leading zeros", + "ipv4OctetTooLarge": "IPv4 octet too large", + "ipv6MultipleDoubleColon": "IPv6 with multiple ::", + "ipv6TooManyGroups": "IPv6 too many groups" + }, + "results": { + "valid": "Valid", + "invalid": "Invalid", + "details": { + "title": "Validation Details", + "type": "Address Type", + "normalizedForm": "Normalized Form", + "addressType": "Address Category", + "scope": "Scope", + "isPrivate": "Private Address", + "isReserved": "Reserved Address", + "compressedForm": "Compressed Form", + "embeddedIPv4": "Embedded IPv4", + "zoneId": "Zone Identifier", + "hasEmbeddedIPv4": "Contains IPv4" + }, + "errors": { + "title": "Validation Errors" + }, + "warnings": { + "title": "Warnings" + }, + "info": { + "title": "Additional Information" + } + }, + "errors": { + "ipv4": { + "mustContainDots": "IPv4 addresses must contain dots (.) to separate octets", + "exactlyFourOctets": "IPv4 addresses must have exactly 4 octets, found {count}", + "octetEmpty": "Octet {number} is empty", + "nonNumericCharacters": "Octet {number} contains non-numeric characters: {part}", + "leadingZeros": "Octet {number} has leading zeros: {part}", + "outOfRange": "Octet {number} is out of range (0-255): {value}", + "invalidFormat": "Invalid IPv4 format" + }, + "ipv6": { + "invalidCharacter": "Invalid character in IPv6 address: {char}", + "multipleDoubleColon": "IPv6 addresses can only contain one '::' sequence", + "tooManyGroups": "IPv6 addresses can have at most 8 groups, found {count}", + "emptyGroup": "Empty group found in IPv6 address", + "groupTooLong": "IPv6 group too long (max 4 hex digits): {group}", + "invalidHexadecimal": "Invalid hexadecimal in group: {group}", + "embeddedIPv4Error": "Invalid embedded IPv4 address: {error}", + "invalidFormat": "Invalid IPv6 format" + }, + "general": { + "unknownFormat": "Unknown IP address format", + "unknownError": "Unknown validation error occurred" + } + }, + "warnings": { + "ipv4": { + "privateAddress": "This is a private IP address", + "reservedAddress": "This is a reserved IP address" + }, + "ipv6": { + "linkLocal": "This is a link-local address", + "siteLocal": "This is a site-local address (deprecated)", + "embeddedIPv4": "Contains embedded IPv4 address" + } + }, + "actions": { + "validate": "Validate", + "clear": "Clear", + "copy": "Copy", + "copied": "Copied!" + } +} diff --git a/src/lib/i18n/translations/en/tools/ipv6-normalize.json b/src/lib/i18n/translations/en/tools/ipv6-normalize.json new file mode 100644 index 00000000..2ff44235 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ipv6-normalize.json @@ -0,0 +1,68 @@ +{ + "title": "IPv6 Address Normalizer", + "description": "Normalize IPv6 addresses to their canonical RFC 5952 format with compression and case standardization.", + "input": { + "label": "IPv6 Addresses", + "placeholder": "2001:0db8:0000:0000:0000:ff00:0042:8329\n2001:db8:0:0:1:0:0:1\n2001:0db8:0001:0000:0000:0ab9:C0A8:0102\n2001:db8::1\nfe80::1%eth0", + "help": "Enter IPv6 addresses (one per line) in any valid format. Supports compressed notation (::), mixed case, leading zeros, and zone IDs." + }, + "actions": { + "normalize": "Normalize Addresses", + "normalizing": "Normalizing...", + "copyAll": "Copy All", + "copyAllNormalized": "Copy All Normalized", + "exportTxt": "Export TXT", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + "copied": "Copied!" + }, + "errors": { + "title": "Errors", + "unknownError": "Unknown error" + }, + "summary": { + "title": "Normalization Summary", + "totalInputs": "Total Inputs", + "validInputs": "Valid", + "invalidInputs": "Invalid", + "alreadyNormalized": "Already Normalized", + "compressionApplied": "Compression Applied", + "leadingZerosRemoved": "Leading Zeros Removed", + "lowercaseApplied": "Lowercase Applied" + }, + "results": { + "title": "Normalized Results", + "count": "({count})", + "input": "Input", + "normalized": "Normalized", + "status": "Status", + "changes": "Changes Applied", + "valid": "Valid", + "invalid": "Invalid", + "copyTooltip": "Copy normalized address" + }, + "rfc": { + "title": "RFC 5952 Normalization", + "description": "This tool normalizes IPv6 addresses according to RFC 5952 guidelines:", + "rules": { + "lowercase": "Convert all hexadecimal digits to lowercase", + "leadingZeros": "Remove leading zeros from each 16-bit group", + "compression": "Use :: to compress the longest run of consecutive zero groups", + "singleZero": "Represent single zero groups as '0' not '0000'" + } + }, + "csvHeaders": { + "input": "Input", + "normalized": "Normalized", + "valid": "Valid", + "compressionApplied": "Compression Applied", + "leadingZerosRemoved": "Leading Zeros Removed", + "lowercaseApplied": "Lowercase Applied", + "error": "Error" + }, + "tooltips": { + "normalizeHelp": "Convert IPv6 addresses to their canonical RFC 5952 format", + "copyAllHelp": "Copy all normalized addresses to clipboard", + "exportHelp": "Export results in various formats" + } +} diff --git a/src/lib/i18n/translations/en/tools/ipv6-notation-converter.json b/src/lib/i18n/translations/en/tools/ipv6-notation-converter.json new file mode 100644 index 00000000..9e134f86 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ipv6-notation-converter.json @@ -0,0 +1,57 @@ +{ + "title": "IPv6 Notation Converter", + "description": "Convert between compressed and expanded IPv6 notation formats with validation.", + "modes": { + "expand": "Expand to Full Form", + "compress": "Compress to Short Form" + }, + "input": { + "label": "IPv6 Address", + "placeholder": "2001:db8::1", + "help": "Enter IPv6 address in any valid format", + "tooltipExpand": "Enter compressed IPv6 address to expand", + "tooltipCompress": "Enter expanded IPv6 address to compress" + }, + "output": { + "label": "Converted Address", + "expandedForm": "Expanded Form", + "compressedForm": "Compressed Form" + }, + "examples": { + "title": "Quick Examples", + "documentationPrefix": "Documentation Prefix", + "loopbackAddress": "Loopback Address", + "linkLocalAddress": "Link-Local Address", + "globalUnicast": "Global Unicast", + "ipv4MappedIPv6": "IPv4-mapped IPv6", + "multicastAddress": "Multicast Address" + }, + "actions": { + "convert": "Convert", + "copy": "Copy", + "copied": "Copied!" + }, + "errors": { + "title": "Conversion Error", + "enterAddress": "Please enter an IPv6 address", + "invalidFormat": "Invalid IPv6 address format", + "conversionFailed": "Conversion failed" + }, + "info": { + "title": "IPv6 Notation Formats", + "expanded": "Expanded: All 32 hexadecimal digits with leading zeros", + "compressed": "Compressed: Shortened using :: for consecutive zero groups", + "rules": "RFC 4291 defines the standard representation formats" + }, + "comparison": { + "input": "Input ({format})", + "output": "Output ({format})" + }, + "stats": { + "inputLength": "Input Length", + "outputLength": "Output Length", + "difference": "Difference", + "characters": "{count} characters", + "charactersChange": "{count} characters" + } +} diff --git a/src/lib/i18n/translations/en/tools/ipv6-subnet-calculator.json b/src/lib/i18n/translations/en/tools/ipv6-subnet-calculator.json new file mode 100644 index 00000000..03bdbae7 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ipv6-subnet-calculator.json @@ -0,0 +1,62 @@ +{ + "title": "IPv6 Subnet Calculator", + "description": "Calculate IPv6 subnet information with 128-bit addressing and modern network prefix notation.", + "sections": { + "networkConfiguration": "Network Configuration", + "commonNetworks": "Common IPv6 Networks", + "subnetInfo": "IPv6 Subnet Information", + "networkDetails": "Network Details", + "addressStructure": "IPv6 Address Structure", + "addressBreakdown": "128-bit Address Breakdown", + "calculationError": "Calculation Error" + }, + "form": { + "networkAddressLabel": "IPv6 Network Address", + "networkAddressPlaceholder": "2001:db8::/64", + "prefixLengthLabel": "Prefix Length", + "customPrefixLength": "Custom prefix length" + }, + "tooltips": { + "networkAddressHelp": "Enter IPv6 address with prefix (e.g., 2001:db8::/64) or address only", + "compressedNotation": "Compressed IPv6 notation using :: for consecutive zero groups", + "expandedNotation": "Full 128-bit IPv6 representation with all zero groups shown", + "subnetMask": "IPv6 subnet mask showing network portion (compressed format)", + "addressRange": "First and last assignable addresses in the subnet", + "assignableAddresses": "Number of addresses available for host assignment (excluding network/broadcast concepts)", + "reverseDns": "PTR record zone for reverse DNS lookups", + "binaryRepresentation": "128-bit binary representation showing network (1) and host (0) bits" + }, + "presets": { + "documentation48": "Documentation /48", + "standardSubnet64": "Standard Subnet /64", + "linkLocal64": "Link-Local /64", + "loopback128": "Loopback /128", + "googleDns48": "Google DNS /48", + "multicastAllNodes": "Multicast All Nodes" + }, + "results": { + "network": "Network", + "totalAddresses": "Total Addresses", + "networkCompressed": "Network Address (Compressed)", + "networkExpanded": "Network Address (Expanded)", + "subnetMask": "Subnet Mask", + "addressRange": "Address Range", + "assignableAddresses": "Assignable Addresses", + "reverseDnsZone": "Reverse DNS Zone", + "binaryPrefix": "Binary Prefix Representation" + }, + "actions": { + "hide": "Hide", + "show": "Show", + "binary": "Binary" + }, + "visualization": { + "networkPortion": "Network Portion", + "hostPortion": "Host Portion", + "bits": "bits", + "showingPortionsFor": "Showing network and host portions for" + }, + "format": { + "digits": "digits" + } +} diff --git a/src/lib/i18n/translations/en/tools/ipv6-teredo.json b/src/lib/i18n/translations/en/tools/ipv6-teredo.json new file mode 100644 index 00000000..223b6ed8 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ipv6-teredo.json @@ -0,0 +1,113 @@ +{ + "title": "IPv6 Teredo Parser", + "description": "Parse Teredo IPv6 addresses to extract server IPv4, flags, mapped port, and client IPv4", + + "overview": { + "tunneling": { + "title": "Teredo Tunneling:", + "description": "Allows IPv6 connectivity for hosts behind IPv4 NATs by encapsulating IPv6 packets in IPv4 UDP." + }, + "format": { + "title": "Address Format:", + "description": "where components are encoded and obfuscated." + }, + "obfuscation": { + "title": "Obfuscation:", + "description": "Client IP and port are XOR'ed to prevent some NATs from interfering with the tunnel." + } + }, + + "examples": { + "title": "Quick Examples", + "microsoftTeredo": "Microsoft Teredo", + "microsoftTeredoDesc": "Microsoft Teredo server example", + "compressedForm": "Compressed Form", + "compressedFormDesc": "Same address in compressed format", + "behindNATCone": "Behind NAT (Cone)", + "behindNATConeDesc": "Client behind cone NAT", + "directConnection": "Direct Connection", + "directConnectionDesc": "Direct connection without NAT" + }, + + "input": { + "label": "Teredo IPv6 Address", + "placeholder": "Enter IPv6 Teredo address...", + "help": "Enter a Teredo IPv6 address (starts with 2001::/32)" + }, + + "results": { + "validAddress": "Valid Teredo Address", + "invalidAddress": "Invalid Address" + }, + + "breakdown": { + "title": "Address Structure", + "prefix": { + "label": "Prefix", + "description": "Teredo identifier" + }, + "server": { + "label": "Server", + "description": "IPv4:" + }, + "flags": { + "label": "Flags" + }, + "port": { + "label": "Port", + "description": "Actual:" + }, + "client": { + "label": "Client", + "description": "IPv4:" + } + }, + + "components": { + "title": "Extracted Components", + "teredoServer": { + "title": "Teredo Server", + "description": "The Teredo relay server handling this tunnel" + }, + "clientIPv4": { + "title": "Client IPv4", + "description": "The client's public IPv4 address (may be NAT address)" + }, + "clientPort": { + "title": "Client Port", + "description": "The UDP port used by the client (may be NAT-mapped)" + }, + "natType": { + "title": "NAT Detection", + "description": "Indicates the type of NAT the client is behind" + } + }, + + "natTypes": { + "cone": "Cone NAT", + "symmetric": "Symmetric NAT", + "direct": "Direct connection (no NAT)" + }, + + "explanation": { + "title": "Parsing Steps", + "step1": "1. Teredo prefix: {prefix} (identifies this as a Teredo tunnel)", + "step2": "2. Server IPv4: {hex} β†’ {ipv4}", + "step3": "3. Flags: {hex} β†’ {flags}", + "step4": "4. Client port: {hex} XOR FFFF β†’ {port}", + "step5": "5. Client IPv4: {hex} XOR FFFFFFFF β†’ {ipv4}" + }, + + "errors": { + "invalidFormat": "Invalid IPv6 address format", + "notTeredo": "Not a valid Teredo address (must start with 2001::/32)", + "invalidComponents": "Invalid address components detected" + }, + + "actions": { + "parse": "Parse", + "clear": "Clear", + "copy": "Copy", + "copied": "Copied!" + } +} diff --git a/src/lib/i18n/translations/en/tools/ipv6-zone-id.json b/src/lib/i18n/translations/en/tools/ipv6-zone-id.json new file mode 100644 index 00000000..a003deea --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ipv6-zone-id.json @@ -0,0 +1,86 @@ +{ + "title": "IPv6 Zone ID Processor", + "description": "Process IPv6 addresses with zone identifiers for link-local and multicast address scoping.", + "input": { + "label": "IPv6 Addresses", + "placeholder": "fe80::1\nfe80::1%eth0\nfe80::1234:5678:90ab:cdef%wlan0\n::1\n2001:db8::1\nff02::1%eth0", + "help": "Enter IPv6 addresses with or without zone identifiers (%), one per line" + }, + "actions": { + "process": "Process Addresses", + "processing": "Processing...", + "copyAll": "Copy All", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + "copied": "Copied!" + }, + "errors": { + "title": "Errors", + "unknownError": "Unknown error" + }, + "summary": { + "title": "Processing Summary", + "totalInputs": "Total Inputs", + "validInputs": "Valid", + "invalidInputs": "Invalid", + "addressesWithZones": "With Zone IDs", + "addressesRequiringZones": "Requiring Zone IDs" + }, + "results": { + "title": "Zone ID Processing Results", + "count": "({count})", + "input": "Input", + "address": "Address", + "zoneId": "Zone ID", + "scope": "Scope", + "recommendation": "Recommendation", + "status": "Status", + "valid": "Valid", + "invalid": "Invalid", + "copyTooltip": "Copy processed address" + }, + "scope": { + "linkLocal": "Link-Local", + "siteLocal": "Site-Local", + "multicast": "Multicast", + "global": "Global", + "loopback": "Loopback", + "unspecified": "Unspecified" + }, + "recommendations": { + "zoneRequired": "Zone ID required for this scope", + "zoneOptional": "Zone ID optional", + "zoneNotNeeded": "Zone ID not needed", + "removeZone": "Consider removing zone ID", + "addZone": "Add zone ID for clarity" + }, + "csvHeaders": { + "input": "Input", + "address": "Address", + "zoneId": "Zone ID", + "scope": "Scope", + "recommendation": "Recommendation", + "valid": "Valid", + "error": "Error" + }, + "info": { + "title": "IPv6 Zone Identifiers", + "description": "Zone identifiers specify the network interface for link-local and multicast addresses", + "usage": "Format: address%zone (e.g., fe80::1%eth0)", + "whenRequired": { + "title": "When Zone IDs Are Required", + "linkLocal": { + "type": "Link-Local Addresses", + "description": "Required to specify which network interface" + }, + "multicast": { + "type": "Multicast Addresses", + "description": "Required for link-local and site-local multicast" + } + }, + "commonIdentifiers": { + "title": "Common Zone Identifiers", + "examples": "eth0, wlan0, lo0, 1, 2, 3" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/lease-time-option51.json b/src/lib/i18n/translations/en/tools/lease-time-option51.json new file mode 100644 index 00000000..288c6a06 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/lease-time-option51.json @@ -0,0 +1,100 @@ +{ + "title": "DHCP Option 51 - IP Address Lease Time", + "subtitle": "Option 51 specifies the lease time in seconds for the IP address assignment. T1 (renewal at 50%) and T2 (rebinding at 87.5%) timers are automatically calculated.", + + "nav": { + "build": "Build Option", + "decode": "Decode Option" + }, + + "examples": { + "decode": { + "oneHour": { + "label": "1 Hour", + "description": "3,600 seconds (0x00000e10)" + }, + "twentyFourHours": { + "label": "24 Hours", + "description": "86,400 seconds (0x00015180)" + }, + "sevenDays": { + "label": "7 Days", + "description": "604,800 seconds (0x00093a80)" + }, + "infinite": { + "label": "Infinite", + "description": "Infinite lease (0xffffffff)" + } + } + }, + + "build": { + "title": "Lease Time Configuration", + + "infinite": { + "label": "Infinite Lease (0xFFFFFFFF)", + "hint": "Permanent IP address assignment (may not be supported by all servers)" + }, + + "leaseSeconds": { + "label": "Lease Time (seconds)", + "hint": "= {time}" + }, + + "quickValues": { + "label": "Quick Values:", + "oneHour": "1h", + "fourHours": "4h", + "twentyFourHours": "24h", + "threeDays": "3d", + "sevenDays": "7d" + }, + + "errors": { + "warnings": "Warnings:", + "validationErrors": "Validation Errors:" + } + }, + + "decode": { + "title": "Decode Option 51", + + "hexInput": { + "label": "Hex String", + "placeholder": "e.g., 00015180 or 00 01 51 80", + "hint": "Enter 8 hex characters (4 bytes, spaces optional)" + }, + + "error": { + "title": "Decode Error:" + } + }, + + "results": { + "buildTitle": "Option 51 - Lease Time", + "decodeTitle": "Decoded Option 51", + + "leaseTime": "Lease Time:", + "hexEncoded": "Hex Encoded:", + "wireFormat": "Wire Format:", + "totalLength": "Total Length:", + "lengthBytes": "{length} bytes", + "seconds": "Seconds:", + "secondsValue": "{seconds}", + "infiniteLease": "Infinite Lease", + "t1Renewal": "T1 Renewal:", + "t1RenewalValue": "{time} (50% of lease)", + "t2Rebinding": "T2 Rebinding:", + "t2RebindingValue": "{time} (87.5% of lease)", + + "configExamples": "Configuration Examples", + "iscDhcpd": "ISC DHCPd", + "keaDhcp4": "Kea DHCPv4", + "dnsmasq": "dnsmasq" + }, + + "buttons": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/mac-address.json b/src/lib/i18n/translations/en/tools/mac-address.json new file mode 100644 index 00000000..271d678c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/mac-address.json @@ -0,0 +1,165 @@ +{ + "title": "MAC Address Converter & OUI Lookup", + "description": "Convert MAC addresses between different formats and identify the manufacturer using the Organizationally Unique Identifier (OUI)", + + "form": { + "singleMode": { + "title": "MAC Address", + "label": "Enter MAC Address", + "placeholder": "00:1A:2B:3C:4D:5E", + "tooltip": "Enter MAC address in any format: colon, hyphen, dot notation, or bare" + }, + "bulkMode": { + "title": "MAC Addresses", + "label": "Enter MAC Addresses", + "placeholder": "00:1A:2B:3C:4D:5E\n00-50-56-C0-00-08\n001A.2B3C.4D5E\n001b632b4567", + "tooltip": "Enter multiple MAC addresses, one per line" + }, + "lookup": "Lookup", + "lookingUp": "Looking up...", + "switchToSingle": "Switch to Single", + "switchToBulk": "Switch to Bulk", + "helpText": { + "single": "Supported formats: {formats}", + "bulk": "Enter MAC addresses one per line. Supported formats: {formats}", + "formats": "{colon}, {hyphen}, {cisco} (Cisco), {bare}" + } + }, + + "examples": { + "title": "Quick Examples", + "vendors": { + "telecomunicationTech": "Ukrainian telecom equipment (Odessa)", + "apple": "Apple device (Cupertino, CA)", + "raspberryPi": "Raspberry Pi (Cambridge, UK)", + "xensource": "Xen virtual machine (Palo Alto, CA)", + "icannIana": "IANA reserved addresses (special use)", + "pcEngines": "PC Engines embedded systems (Switzerland)", + "multicastBroadcast": "Multicast/Broadcast" + } + }, + + "results": { + "conversion": "Address Conversion", + "conversions": "Address Conversions", + "invalidAddress": "Invalid MAC Address", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + + "oui": { + "title": "OUI Information", + "fields": { + "oui": "OUI", + "manufacturer": "Manufacturer", + "country": "Country", + "blockType": "Block Type", + "blockSize": "Block Size", + "addressRange": "Address Range", + "registryStatus": "Registry Status", + "lastUpdated": "Last Updated", + "address": "Address" + }, + "values": { + "unknown": "Unknown", + "na": "N/A", + "private": "Private", + "public": "Public", + "addresses": "{count} addresses" + }, + "blockTypes": { + "maL": "Large block: 16.7 million addresses (24-bit prefix)", + "maM": "Medium block: 1 million addresses (28-bit prefix)", + "maS": "Small block: 4,096 addresses (36-bit prefix)", + "cid": "Company ID" + } + }, + + "details": { + "title": "Address Details", + "fields": { + "universalAddress": "Universal Address", + "locallyAdministered": "Locally Administered", + "unicast": "Unicast", + "multicastBroadcast": "Multicast/Broadcast" + } + }, + + "formats": { + "title": "Formats", + "types": { + "colonNotation": { + "label": "Colon Notation", + "tooltip": "Standard IEEE notation; most Linux, BSD, macOS use this" + }, + "hyphenNotation": { + "label": "Hyphen Notation", + "tooltip": "Common on Windows systems" + }, + "ciscoNotation": { + "label": "Cisco (Dot) Notation", + "tooltip": "Cisco IOS / NX-OS style" + }, + "bareUppercase": { + "label": "Bare (Uppercase)", + "tooltip": "Common in databases, APIs" + }, + "bareLowercase": { + "label": "Bare (Lowercase)", + "tooltip": "Common in scripts, JSON, etc." + }, + "eui64": { + "label": "EUI-64 (expanded form)", + "tooltip": "Used when converting MAC β†’ IPv6 Interface ID (adds FFFE in the middle, flips the U/L bit)" + }, + "ipv6Style": { + "label": "Dot-separated 2-byte groups", + "tooltip": "Occasionally seen in debugging or tools that mimic IPv6 notation" + }, + "spaceSeparated": { + "label": "Space-separated pairs", + "tooltip": "Sometimes seen in hex dumps or firmware logs" + }, + "decimalOctets": { + "label": "Decimal octets", + "tooltip": "Rare, but some diagnostic tools display MACs in decimal" + }, + "prefixedMac": { + "label": "Prefixed (MAC=)", + "tooltip": "Seen in configuration files or CLI outputs" + }, + "slashSeparated": { + "label": "Slash-separated", + "tooltip": "Seen in some telecom equipment or SNMP exports" + }, + "prefixedBare": { + "label": "Prefixed bare (MAC)", + "tooltip": "Appears in certain JSON/CSV exports or proprietary APIs" + }, + "prefixedAddr": { + "label": "Prefixed bare (addr)", + "tooltip": "Appears in certain JSON/CSV exports or proprietary APIs" + }, + "binary": { + "label": "Binary (8-bit groups)", + "tooltip": "Rare, but useful for bit-level inspection" + } + }, + "copyToClipboard": "Copy to clipboard" + }, + + "summary": { + "title": "Conversion Summary", + "stats": { + "total": "Total", + "valid": "Valid", + "invalid": "Invalid", + "withOui": "With OUI" + } + } + }, + + "education": { + "title": "Understanding MAC Addresses", + "quickTips": "Quick Tips" + } +} diff --git a/src/lib/i18n/translations/en/tools/naptr-builder.json b/src/lib/i18n/translations/en/tools/naptr-builder.json new file mode 100644 index 00000000..e36ddb16 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/naptr-builder.json @@ -0,0 +1,138 @@ +{ + "title": "NAPTR Record Builder", + "description": "Construct NAPTR (Naming Authority Pointer) records for dynamic delegation and service mapping", + + "examples": { + "title": "Quick Examples", + "sipService": "SIP Service", + "emailService": "Email Service", + "webService": "Web Service", + "srvDelegation": "SRV Delegation" + }, + + "flags": { + "uFlag": { + "label": "U - Terminal rule (URI)", + "description": "The Rule is terminal and the result is a URI" + }, + "sFlag": { + "label": "S - Terminal rule (SRV)", + "description": "The Rule is terminal and the result is for SRV lookup" + }, + "aFlag": { + "label": "A - Terminal rule (Address)", + "description": "The Rule is terminal and the result is an address record" + }, + "pFlag": { + "label": "P - Protocol specific", + "description": "Protocol-specific flags" + }, + "emptyFlag": { + "label": "Empty - Non-terminal", + "description": "The Rule is not terminal (continue processing)" + }, + "selectPlaceholder": "Select flag type" + }, + + "services": { + "sip": { + "label": "SIP Service", + "description": "Session Initiation Protocol" + }, + "email": { + "label": "Email Service", + "description": "Electronic mail service" + }, + "webHttp": { + "label": "HTTP Web Service", + "description": "Web service over HTTP" + }, + "webHttps": { + "label": "HTTPS Web Service", + "description": "Secure web service over HTTPS" + }, + "tel": { + "label": "Telephone Service", + "description": "Telephone number mapping" + }, + "fax": { + "label": "Fax Service", + "description": "Facsimile service" + }, + "h323": { + "label": "H.323 Service", + "description": "H.323 multimedia protocol" + }, + "im": { + "label": "Instant Messaging", + "description": "Instant messaging service" + }, + "showExamples": "Show service examples" + }, + + "input": { + "domainLabel": "Domain Name *", + "domainTooltip": "The domain name for this NAPTR record", + "domainDescription": "The domain name for this NAPTR record", + "domainPlaceholder": "example.com", + "orderLabel": "Order *", + "orderTooltip": "Processing order - lower values are processed first (0-65535)", + "orderDescription": "Processing order (0-65535)", + "preferenceLabel": "Preference *", + "preferenceTooltip": "Preference within the same order value (0-65535)", + "preferenceDescription": "Preference within order (0-65535)", + "flagsLabel": "Flags", + "flagsTooltip": "Control processing behavior - affects how the result is interpreted", + "serviceLabel": "Service", + "serviceTooltip": "Service identifier or protocol (e.g., E2U+sip for SIP services)", + "servicePlaceholder": "E2U+sip", + "regexpLabel": "Regular Expression", + "regexpTooltip": "Regular expression for pattern matching and substitution (format: !pattern!replacement!)", + "regexpPlaceholder": "!^.*$!sip:info@example.com!", + "regexpDescription": "Substitution expression (format: !pattern!replacement!)", + "replacementLabel": "Replacement", + "replacementTooltip": "Next domain to query, or '.' for terminal rules", + "replacementPlaceholder": ".", + "replacementDescription": "Domain name for next lookup, or \".\" for terminal rules" + }, + + "output": { + "title": "Generated NAPTR Record", + "placeholder": "Fill in the required fields to generate the NAPTR record", + "copyButton": "Copy Record", + "copied": "Copied!", + "downloadButton": "Download", + "downloaded": "Downloaded!" + }, + + "warnings": { + "title": "Configuration Warnings", + "uriFlag": "URI flag requires a valid substitution expression with delimiters", + "srvFlag": "SRV flag typically requires a replacement domain, not \".\"", + "nonTerminal": "Non-terminal rules should have a replacement domain for continued processing", + "regexpFormat": "Regular expressions should follow the format: !pattern!replacement!", + "orderPreference": "Order and Preference should typically be different values" + }, + + "info": { + "aboutTitle": "About NAPTR Records", + "aboutDescription": "NAPTR (Naming Authority Pointer) records provide a way to map domain names to URIs or other domain names through regular expression-based rewriting. They're commonly used in telecommunications for ENUM (E.164 Number Mapping) and SIP services, allowing dynamic delegation and service discovery.", + "fieldsTitle": "Field Descriptions", + "fields": { + "order": "Processing order (lower values first)", + "preference": "Preference within same order", + "flags": "Control processing behavior", + "service": "Service identifier or protocol", + "regexp": "Pattern matching and substitution", + "replacement": "Next domain to query" + }, + "useCasesTitle": "Common Use Cases", + "useCases": { + "enum": "ENUM telephone number mapping", + "sip": "SIP service discovery", + "delegation": "Dynamic delegation", + "protocol": "Protocol mapping", + "location": "Service location" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/network-visualizer.json b/src/lib/i18n/translations/en/tools/network-visualizer.json new file mode 100644 index 00000000..40449624 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/network-visualizer.json @@ -0,0 +1,43 @@ +{ + "title": "Network Visualization", + "sections": { + "networkVisualization": "Network Visualization", + "binaryBreakdown": "Subnet Mask Binary Breakdown", + "addressGrid": "Address Grid", + "networkEfficiency": "Network Efficiency" + }, + "addressTypes": { + "networkAddress": "Network Address", + "broadcastAddress": "Broadcast Address", + "usableHost": "Usable Host", + "usableHosts": "Usable Hosts" + }, + "ranges": { + "addressRange": "Address Range", + "totalAddresses": "{count} total addresses", + "usableHosts": "Usable Hosts ({count})" + }, + "legend": { + "network": "Network", + "usableHosts": "Usable Hosts ({count})", + "broadcast": "Broadcast" + }, + "binary": { + "networkBit": "Network bit (1)", + "hostBit": "Host bit (0)", + "bitPosition": "Position {position}", + "bitTooltip": "{type} - Position {position}", + "networkBits": "Network bits: {count}", + "hostBits": "Host bits: {count}", + "octetLabel": "(Octet {number})" + }, + "efficiency": { + "addressUtilization": "Address Utilization", + "wastedAddresses": "Wasted Addresses", + "totalCapacity": "Total Capacity", + "networkOverhead": "Network Overhead" + }, + "tooltips": { + "usableHostAddresses": "Usable Host Addresses" + } +} diff --git a/src/lib/i18n/translations/en/tools/next-available.json b/src/lib/i18n/translations/en/tools/next-available.json new file mode 100644 index 00000000..39c7fa6a --- /dev/null +++ b/src/lib/i18n/translations/en/tools/next-available.json @@ -0,0 +1,124 @@ +{ + "title": "Next Available Subnet Finder", + "description": "Find available subnet space in IP pools with intelligent allocation strategies and conflict detection.", + "input": { + "pools": { + "label": "Available IP Pools", + "placeholder": "192.168.0.0/16\n10.0.0.0/8", + "help": "Enter CIDR blocks representing available address pools" + }, + "allocations": { + "label": "Existing Allocations", + "placeholder": "192.168.1.0/24\n192.168.10.0/24\n10.0.0.0/16", + "help": "Enter already allocated subnets to avoid conflicts" + }, + "searchMode": { + "prefix": "Search by Prefix Length", + "hosts": "Search by Host Count" + }, + "desiredPrefix": { + "label": "Desired Prefix Length", + "tooltip": "CIDR prefix length for the new subnet" + }, + "desiredHosts": { + "label": "Desired Host Count", + "tooltip": "Number of hosts needed in the new subnet" + }, + "targetPrefix": { + "label": "Target Prefix Length" + }, + "policy": { + "label": "Allocation Policy", + "options": { + "firstFit": "First Fit", + "bestFit": "Best Fit" + } + }, + "maxCandidates": { + "label": "Maximum Candidates" + }, + "options": { + "ipv4UsableHosts": "Count only usable hosts (exclude network/broadcast)", + "maxCandidates": "Maximum candidates to return", + "usableHosts": { + "label": "Count Usable Hosts Only" + } + } + }, + "allocation": { + "policy": { + "label": "Allocation Policy", + "firstFit": "First Fit", + "firstFitDesc": "Use first available space that fits", + "bestFit": "Best Fit", + "bestFitDesc": "Minimize wasted space", + "worstFit": "Worst Fit", + "worstFitDesc": "Maximize remaining contiguous space" + } + }, + "searchCriteria": { + "title": "Search Criteria", + "byPrefix": { + "label": "By Prefix Length" + }, + "byHosts": { + "label": "By Host Count" + } + }, + + "examples": { + "title": "Quick Examples", + "officeSubnets": { + "label": "Office Subnets" + }, + "hostBasedSearch": { + "label": "Host-Based Search" + }, + "multiplePools": { + "label": "Multiple Pools" + }, + "ipv6Example": { + "label": "IPv6 Example" + } + }, + "actions": { + "findNext": "Find Next Available", + "searching": "Searching...", + "copyCandidate": "Copy", + "exportResults": "Export Results", + "copied": "Copied!" + }, + "results": { + "title": "Available Subnet Candidates", + "count": "({count})", + "noResults": "No available subnets found", + "candidate": "Candidate", + "subnet": "Subnet", + "size": "Size", + "utilization": "Pool Utilization", + "remainingSpace": "Remaining Space", + "pool": "Source Pool", + "copyTooltip": "Copy subnet" + }, + "visualization": { + "title": "Subnet Allocation Visualization", + "showVisualization": "Show allocation map", + "allocated": "Allocated", + "available": "Available", + "candidate": "Candidate" + }, + "errors": { + "title": "Search Errors", + "invalidPool": "Invalid IP pool", + "invalidAllocation": "Invalid allocation", + "noPoolSpace": "No available space in pools", + "searchFailed": "Subnet search failed" + }, + "summary": { + "title": "Search Summary", + "totalPools": "Total Pools", + "totalAllocations": "Existing Allocations", + "candidatesFound": "Candidates Found", + "searchCriteria": "Search Criteria" + } +} diff --git a/src/lib/i18n/translations/en/tools/nth-ip.json b/src/lib/i18n/translations/en/tools/nth-ip.json new file mode 100644 index 00000000..432b982a --- /dev/null +++ b/src/lib/i18n/translations/en/tools/nth-ip.json @@ -0,0 +1,90 @@ +{ + "title": "Nth IP Calculator", + "description": "Calculate the nth IP address from networks, ranges, or CIDR blocks using flexible indexing syntax.", + "examples": { + "title": "Quick Examples", + "tenthFromSubnet": { + "description": "Get 10th IP from a /24 subnet" + }, + "multipleRanges": { + "description": "Multiple range types with different indices" + }, + "ipv6Networks": { + "description": "IPv6 networks with various formats" + }, + "largeNetworks": { + "description": "Large networks with high indices" + }, + "firstLastIP": { + "description": "First and last IP using positive/negative indexing" + }, + "sequentialRanges": { + "description": "Sequential IP ranges with specific indices" + }, + "largeIPv6": { + "description": "IPv6 networks with large indices" + }, + "specialUse": { + "description": "Special use networks with specific indices" + } + }, + + "input": { + "title": "Network Configuration", + "label": "Networks and Indices", + "placeholder": "192.168.1.0/24 @ 10\n10.0.0.0-10.0.0.255 [50]", + "help": "Enter networks with indices using @, [], # or + syntax", + "globalOffset": { + "label": "Global Offset", + "placeholder": "0", + "help": "Base offset applied to all calculations" + } + }, + "actions": { + "calculate": "Calculate Nth IPs", + "calculating": "Calculating...", + "copyAll": "Copy All IPs", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + "copied": "Copied!" + }, + "errors": { + "title": "Errors" + }, + "summary": { + "title": "Calculation Summary", + "totalInputs": "Total Inputs", + "successfulCalculations": "Successful", + "failedCalculations": "Failed" + }, + "results": { + "title": "Nth IP Results", + "count": "({count})", + "network": "Network/Range", + "index": "Index", + "calculatedIP": "Calculated IP", + "position": "Position", + "totalAddresses": "Total", + "copyTooltip": "Copy IP address" + }, + "syntax": { + "title": "Supported Syntax", + "formats": [ + "network @ index (e.g., 192.168.1.0/24 @ 10)", + "network [index] (e.g., 10.0.0.0/8 [100])", + "network # index (e.g., 2001:db8::/64 # 50)", + "network + index (e.g., 172.16.0.0/16 + 1000)", + "network index (e.g., 192.168.0.0/24 5)" + ], + "indexing": "Positive indices start from 0, negative indices from -1 (last)" + }, + "csvHeaders": { + "network": "Network/Range", + "index": "Index", + "calculatedIP": "Calculated IP", + "position": "Position", + "totalAddresses": "Total Addresses", + "valid": "Valid", + "error": "Error" + } +} diff --git a/src/lib/i18n/translations/en/tools/prefix-delegation.json b/src/lib/i18n/translations/en/tools/prefix-delegation.json new file mode 100644 index 00000000..bf30018a --- /dev/null +++ b/src/lib/i18n/translations/en/tools/prefix-delegation.json @@ -0,0 +1,65 @@ +{ + "title": "DHCPv6 Prefix Delegation (IA_PD)", + "subtitle": "Build DHCPv6 IA_PD options for delegating IPv6 prefixes to requesting routers. Configure Identity Association for Prefix Delegation (Option 25) with IA Prefix options (Option 26) per RFC 8415.", + + "config": { + "title": "Prefix Delegation Configuration", + + "iaid": { + "label": "IAID (Identity Association ID)", + "hint": "Unique identifier for this IA_PD (0-4294967295)" + }, + + "t1": { + "label": "T1 Renewal Time (seconds)", + "placeholder": "Optional" + }, + + "t2": { + "label": "T2 Rebinding Time (seconds)", + "placeholder": "Optional" + }, + + "prefixes": { + "label": "Delegated Prefixes", + "prefixPlaceholder": "e.g., 2001:db8::/56", + "preferredPlaceholder": "Preferred (s)", + "validPlaceholder": "Valid (s)", + "addButton": "Add Prefix", + "removeButton": "Remove" + } + }, + + "errors": { + "title": "Validation Errors:" + }, + + "results": { + "title": "Option 25 - IA_PD", + + "iaid": "IAID:", + "t1Renewal": "T1 Renewal:", + "t2Rebinding": "T2 Rebinding:", + "fullHex": "Full Hex:", + "wireFormat": "Wire Format:", + "totalLength": "Total Length:", + "bytes": "{length} bytes", + + "prefixesSection": { + "title": "Delegated Prefixes (Option 26)", + "preferredLifetime": "Preferred Lifetime:", + "validLifetime": "Valid Lifetime:", + "prefixWireFormat": "Wire Format:" + }, + + "configSection": { + "title": "Configuration Example", + "keaDhcp6": "Kea DHCPv6" + } + }, + + "common": { + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/ptr-generator.json b/src/lib/i18n/translations/en/tools/ptr-generator.json new file mode 100644 index 00000000..560a1ae2 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ptr-generator.json @@ -0,0 +1,156 @@ +{ + "title": "Reverse PTR Generator", + "description": "Convert IP addresses and CIDR blocks to PTR record names and zone file examples", + + "overview": { + "reverseDNS": { + "title": "Reverse DNS", + "content": "PTR records provide reverse DNS lookups, mapping IP addresses back to hostnames" + }, + "zoneStructure": { + "title": "Zone Structure", + "content": "IPv4 uses .in-addr.arpa zones, IPv6 uses .ip6.arpa with nibble boundaries" + }, + "zoneFiles": { + "title": "Zone Files", + "content": "Generated records can be used directly in BIND9 and other DNS server configurations" + } + }, + "input": { + "label": "IP Address or CIDR Block", + "placeholder": "192.168.1.100 or 192.168.1.0/24", + "singleMode": "Single Address", + "cidrMode": "CIDR Block", + "help": "Enter an IP address for single PTR record or CIDR block for bulk generation", + "type": { + "label": "Input Type", + "singleIP": "Single IP Address", + "cidrBlock": "CIDR Block" + }, + "address": { + "labelSingle": "IP Address", + "placeholderSingle": "e.g., 192.168.1.100" + }, + "options": { + "generateZoneFiles": "Generate Zone Files", + "zoneFilesHint": "Include complete zone file format for DNS servers" + } + }, + "examples": { + "title": "Quick Examples", + "singleIPv4": { + "label": "Single IPv4", + "description": "Generate PTR for single IPv4 address" + }, + "singleIPv6": { + "label": "Single IPv6", + "description": "Generate PTR for single IPv6 address" + }, + "ipv4Subnet24": { + "label": "IPv4 /24 Subnet", + "description": "Generate PTRs for entire /24 subnet" + }, + "ipv4SmallBlock": { + "label": "IPv4 /28 Small Block", + "description": "Generate PTRs for /28 block (16 addresses)" + }, + "ipv6Network": { + "label": "IPv6 /64 Network", + "description": "Generate PTRs for IPv6 network block" + }, + "largeBlock": { + "label": "Large Block (/16)", + "description": "Demonstration of large block generation" + }, + "types": { + "singleIP": "Single IP Address", + "cidrBlock": "CIDR Block" + } + }, + "options": { + "showZoneFiles": "Show zone file format", + "showZoneFilesTooltip": "Display complete zone file content for DNS server configuration" + }, + "actions": { + "generate": "Generate PTR Records", + "generating": "Generating...", + "copyAll": "Copy All", + "exportTxt": "Export TXT", + "exportBind": "Export BIND", + "exportJson": "Export JSON", + "copied": "Copied!", + "copyZoneFile": "Copy Zone File" + }, + "results": { + "title": "PTR Records", + "summary": { + "totalPTRs": "Total PTR Records", + "ipv4": "IPv4 Records", + "zones": "DNS Zones" + }, + "records": { + "title": "PTR Record Details", + "ipAddress": "IP Address", + "ptrName": "PTR Record Name", + "type": "Type", + "zone": "Zone" + }, + "zoneFiles": { + "title": "Zone File Contents" + }, + "summary": { + "title": "Generation Summary", + "totalEntries": "Total Entries", + "ipv4Entries": "IPv4 Entries", + "ipv6Entries": "IPv6 Entries", + "uniqueZones": "Unique Zones" + }, + "entries": { + "title": "PTR Entries", + "count": "({count})", + "ipAddress": "IP Address", + "ptrRecord": "PTR Record", + "reverseZone": "Reverse Zone", + "type": "Type", + "copyTooltip": "Copy PTR record" + }, + "zoneFiles": { + "title": "Zone Files", + "count": "({count})", + "zoneName": "Zone: {name}", + "zoneType": "Type: {type}", + "entriesCount": "{count} entries", + "copyZoneTooltip": "Copy zone file content" + } + }, + "errors": { + "title": "Error", + "invalidInput": "Invalid input format", + "unsupportedFormat": "Unsupported IP format", + "tooManyEntries": "Too many entries (maximum {max})", + "processingError": "Error processing input" + }, + "warnings": { + "largeBlock": "Large CIDR block will generate many records", + "performanceNote": "Generation may take some time for large blocks" + }, + + "education": { + "whatArePTRRecords": { + "title": "What are PTR Records?", + "content": "PTR records provide reverse DNS lookups, mapping IP addresses back to domain names for verification and logging purposes" + }, + "zoneStructure": { + "title": "Zone Structure", + "content": "IPv4 reverse zones use .in-addr.arpa (e.g., 1.168.192.in-addr.arpa), IPv6 uses .ip6.arpa with nibble boundaries" + }, + "zoneDelegation": { + "title": "Zone Delegation", + "content": "Reverse DNS zones must be properly delegated by your ISP or hosting provider to function correctly" + }, + "bestPractices": { + "title": "Best Practices", + "content": "Always set up reverse DNS for mail servers, use descriptive hostnames, and ensure forward/reverse DNS match" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/pxe-profile-builder.json b/src/lib/i18n/translations/en/tools/pxe-profile-builder.json new file mode 100644 index 00000000..a096f06e --- /dev/null +++ b/src/lib/i18n/translations/en/tools/pxe-profile-builder.json @@ -0,0 +1,147 @@ +{ + "title": "PXE Profile Generator", + "subtitle": "Generate PXE boot profiles with automatic UEFI/BIOS detection using DHCP Options 93/94. Configure bootfiles for different architectures and generate dhcpd/Kea configuration snippets.", + + "profile": { + "title": "Profile Configuration", + + "name": { + "label": "Profile Name", + "placeholder": "e.g., Production PXE" + }, + + "tftpServer": { + "label": "TFTP Server (Option 66)", + "placeholder": "e.g., pxe.example.com or 192.168.1.10", + "hint": "Hostname or IP address of the TFTP server" + }, + + "architecture": { + "label": "Architecture Mode", + "hint": "Auto-detect uses Option 93 to serve different bootfiles based on client firmware", + "options": { + "auto": "Auto-detect", + "bios": "BIOS only", + "uefiX64": "UEFI x64", + "uefiX86": "UEFI x86", + "uefiArm64": "UEFI ARM64", + "uefiArm32": "UEFI ARM32" + } + } + }, + + "bootfiles": { + "title": "Bootfiles (Option 67)", + "hint": "Configure bootfile names for different client architectures. At least one bootfile is required.", + + "bios": { + "label": "BIOS Bootfile", + "placeholder": "e.g., pxelinux.0 or undionly.kpxe", + "hint": "For legacy BIOS systems (Arch Type 0x0000)" + }, + + "uefiX64": { + "label": "UEFI x64 Bootfile", + "placeholder": "e.g., bootx64.efi or ipxe-x64.efi", + "hint": "For UEFI x86-64 systems (Arch Type 0x0007)" + }, + + "uefiX86": { + "label": "UEFI x86 Bootfile", + "placeholder": "e.g., bootia32.efi", + "hint": "For UEFI IA32 systems (Arch Type 0x0006)" + }, + + "uefiArm64": { + "label": "UEFI ARM64 Bootfile", + "placeholder": "e.g., bootaa64.efi", + "hint": "For UEFI ARM 64-bit systems (Arch Type 0x000b)" + }, + + "uefiArm32": { + "label": "UEFI ARM32 Bootfile", + "placeholder": "e.g., bootarm.efi", + "hint": "For UEFI ARM 32-bit systems (Arch Type 0x000a)" + } + }, + + "network": { + "title": "Network Settings (Optional)", + "hint": "Customize network values for configuration examples below", + + "subnet": { + "label": "Subnet", + "placeholder": "192.168.1.0" + }, + + "netmask": { + "label": "Netmask", + "placeholder": "255.255.255.0" + }, + + "rangeStart": { + "label": "Range Start", + "placeholder": "192.168.1.100" + }, + + "rangeEnd": { + "label": "Range End", + "placeholder": "192.168.1.200" + }, + + "gateway": { + "label": "Gateway", + "placeholder": "192.168.1.1" + }, + + "dns": { + "label": "DNS Server", + "placeholder": "8.8.8.8" + } + }, + + "errors": { + "validation": "Validation Errors", + "network": "Network Settings Errors" + }, + + "results": { + "summary": { + "title": "Profile Summary", + "profileName": "Profile Name:", + "architecture": "Architecture Mode:", + "tftpServer": "TFTP Server:", + "biosBootfile": "BIOS Bootfile:", + "uefiX64Bootfile": "UEFI x64 Bootfile:", + "uefiX86Bootfile": "UEFI x86 Bootfile:", + "uefiArm64Bootfile": "UEFI ARM64 Bootfile:", + "uefiArm32Bootfile": "UEFI ARM32 Bootfile:" + }, + + "config": { + "title": "DHCP Server Configuration", + "iscDhcpd": "ISC dhcpd Configuration", + "keaDhcp4": "Kea DHCPv4 Configuration" + }, + + "archDetection": { + "title": "PXE Boot Architecture Detection", + "intro": "DHCP Option 93 (Client System Architecture Type) allows the DHCP server to detect the client's firmware type and serve the appropriate bootfile. Common architecture types:", + "types": { + "bios": "Intel x86PC (Legacy BIOS)", + "efiIa32": "EFI IA32 (32-bit UEFI)", + "efiBC": "EFI BC (64-bit UEFI, most common)", + "efiArm32": "EFI ARM 32-bit", + "efiArm64": "EFI ARM 64-bit" + }, + "autoDetectInfo": "When using auto-detect mode, the DHCP server will examine Option 93 in the client's DHCPDISCOVER message and respond with the appropriate bootfile for that architecture." + } + }, + + "common": { + "required": "*", + "recommended": "(recommended)", + "copy": "Copy", + "copied": "Copied" + } +} diff --git a/src/lib/i18n/translations/en/tools/random-ip.json b/src/lib/i18n/translations/en/tools/random-ip.json new file mode 100644 index 00000000..0713c5b1 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/random-ip.json @@ -0,0 +1,139 @@ +{ + "title": "Random IP Generator", + "subtitle": "Generate random IP addresses from networks and ranges with uniqueness control and seeded randomness", + + "examples": { + "title": "Quick Examples", + "titleTooltip": "Click to see example inputs", + "items": { + "basicCidr": { + "input": "192.168.1.0/24 x 5", + "description": "Generate 5 random IPs from a /24 subnet" + }, + "multipleFormats": { + "input": "10.0.0.0-10.0.0.255 * 3\n172.16.0.0/16 [8]", + "description": "Multiple formats: range and CIDR with different counts" + }, + "ipv6": { + "input": "2001:db8::/64 # 10\nfe80::/10 x 5", + "description": "IPv6 networks with various syntax formats" + }, + "largeCounts": { + "input": "192.168.0.0/16 100\n203.0.113.0/24 * 20", + "description": "Large generation counts from different networks" + }, + "specialUse": { + "input": "127.0.0.0/8\n::1/128 x 1\n169.254.0.0/16 [10]", + "description": "Special-use addresses: loopback and link-local" + }, + "testNetworks": { + "input": "198.51.100.0/24 * 15\n198.18.0.0/15 [25]", + "description": "Test networks for documentation and benchmarking" + } + } + }, + + "networkConfig": { + "title": "Network Configuration", + "networksLabel": "Networks and Counts", + "networksTooltip": "Enter networks with generation counts using various formats", + "networksPlaceholder": "192.168.1.0/24 x 10\n10.0.0.0-10.0.0.255 5\n172.16.0.0/16 * 3\n2001:db8::/64[15]", + "networksSpecifyTooltip": "Specify networks and generation counts", + "helpText": "Formats: network x count, network * count, network count, network#count, network[count]", + + "defaultCount": { + "label": "Default Count", + "tooltip": "Number of IPs to generate when count is not specified", + "placeholder": "5", + "valueTooltip": "Default generation count" + }, + + "unique": { + "label": "Unique IPs Only", + "tooltip": "Ensure all generated IPs are unique within each network" + }, + + "seed": { + "label": "Random Seed", + "tooltip": "Use the same seed for reproducible random results", + "placeholder": "Optional seed for reproducible results", + "valueTooltip": "Enter seed for reproducible randomness", + "generateTooltip": "Generate new random seed" + } + }, + + "loading": "Generating random IPs...", + + "errors": { + "title": "Errors" + }, + + "summary": { + "title": "Generation Summary", + "copyTooltip": "Copy summary to clipboard", + "totalNetworks": { + "label": "Total Networks", + "tooltip": "Total number of networks processed" + }, + "valid": { + "label": "Valid", + "tooltip": "Networks that were processed successfully" + }, + "invalid": { + "label": "Invalid", + "tooltip": "Networks that had processing errors" + }, + "totalIps": { + "label": "Total IPs", + "tooltip": "Total IP addresses generated across all networks" + }, + "uniqueIps": { + "label": "Unique IPs", + "tooltip": "Number of unique IP addresses generated" + } + }, + + "allIps": { + "title": "All Generated IPs", + "count": "({count})", + "copyAllTooltip": "Copy all generated IPs to clipboard", + "exportTxtTooltip": "Export as plain text file", + "exportCsvTooltip": "Export as CSV file", + "exportJsonTooltip": "Export as JSON file", + "ipCopyTooltip": "Click to copy IP address" + }, + + "generations": { + "title": "Network Generations", + "requested": "Requested:", + "generated": "Generated:", + "unique": "Unique:", + "uniqueYes": "Yes", + "uniqueNo": "No", + "seed": "Seed:", + "seedCopyTooltip": "Click to copy", + + "networkRange": { + "title": "Network Range", + "start": "Start:", + "end": "End:", + "total": "Total:", + "startCopyTooltip": "Click to copy", + "endCopyTooltip": "Click to copy" + }, + + "generatedIps": { + "title": "Generated IPs", + "count": "({count})" + } + }, + + "common": { + "copy": "Copy", + "copied": "Copied!", + "copyAll": "Copy All", + "txt": "TXT", + "csv": "CSV", + "json": "JSON" + } +} diff --git a/src/lib/i18n/translations/en/tools/reserved-ranges-reference.json b/src/lib/i18n/translations/en/tools/reserved-ranges-reference.json new file mode 100644 index 00000000..3a4f99f5 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/reserved-ranges-reference.json @@ -0,0 +1,65 @@ +{ + "title": "Reserved IP Ranges Reference", + "description": "Special-purpose IP address ranges defined by RFCs and their intended uses.", + "sections": { + "reservedRanges": "Reserved IP Ranges", + "explainer": "Understanding Reserved IP Ranges" + }, + "rangeInfo": { + "address": "Address Range", + "description": "Description", + "rfc": "RFC", + "tooltipFormat": "{description} - Defined in {rfc}" + }, + "notices": { + "privateNetwork": "Private Network: Not routed on the public Internet", + "linkLocal": "Link-Local: Used for automatic addressing", + "multicast": "Multicast: Used for group communication", + "reserved": "Reserved: Not assigned for general use" + }, + "explainer": { + "understanding": { + "title": "Understanding Reserved IP Ranges", + "content": "IP address ranges are reserved for special purposes to ensure proper Internet operation and avoid conflicts." + }, + "categories": { + "title": "Categories of Reserved Ranges", + "private": { + "title": "Private Networks (RFC 1918)", + "content": "Used in local networks, not routed on the Internet" + }, + "linkLocal": { + "title": "Link-Local (RFC 3927)", + "content": "Automatic IP assignment when DHCP is unavailable" + }, + "multicast": { + "title": "Multicast (RFC 3171)", + "content": "Group communication and streaming protocols" + }, + "special": { + "title": "Special-Use (Various RFCs)", + "content": "Documentation, testing, and protocol functions" + } + }, + "usage": { + "title": "Usage Guidelines", + "points": [ + "Private ranges are safe for internal use", + "Link-local addresses are automatically assigned", + "Multicast addresses require special handling", + "Documentation ranges are for examples only" + ] + }, + "planning": { + "title": "Network Planning", + "content": "Choose appropriate ranges based on your network size and routing requirements." + } + }, + "filter": { + "all": "All Ranges", + "private": "Private", + "linkLocal": "Link-Local", + "multicast": "Multicast", + "special": "Special-Use" + } +} diff --git a/src/lib/i18n/translations/en/tools/reverse-zone-generator.json b/src/lib/i18n/translations/en/tools/reverse-zone-generator.json new file mode 100644 index 00000000..9e761de9 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/reverse-zone-generator.json @@ -0,0 +1,123 @@ +{ + "title": "Reverse Zone Generator", + "description": "Generate complete reverse DNS zone files from CIDR blocks with customizable hostname templates", + + "overview": { + "fullZoneFiles": { + "title": "Full Zone Files:", + "description": "Complete DNS zone files with SOA, NS, and PTR records ready for deployment." + }, + "hostnameTemplates": { + "title": "Hostname Templates:", + "description": "Customize hostname patterns using placeholders like {ip} and {ip-dashes}." + }, + "zoneConfiguration": { + "title": "Zone Configuration:", + "description": "Configure name servers, contact email, TTL values, and domain settings." + } + }, + + "examples": { + "title": "Quick Examples", + "items": [ + { + "label": "IPv4 /24 Network", + "description": "Generate zone for full /24 subnet" + }, + { + "label": "IPv4 /28 Block", + "description": "Small block with custom naming" + }, + { + "label": "IPv6 /64 Network", + "description": "IPv6 reverse zone generation" + }, + { + "label": "Corporate Network", + "description": "Corporate naming convention" + } + ] + }, + + "form": { + "cidrBlock": { + "label": "CIDR Block", + "placeholder": "192.168.1.0/24 or 2001:db8::/64", + "tooltip": "Enter a CIDR block to generate reverse zones for" + }, + "hostnameTemplate": { + "label": "Hostname Template", + "placeholder": "host-[ip-dashes].example.com.", + "tooltip": "Use placeholders like {ip}, {ip-dashes} to customize hostnames" + }, + "nameServers": { + "label": "Name Servers", + "placeholder": "ns1.example.com\nns2.example.com", + "tooltip": "One name server per line, automatically adds trailing dots" + }, + "contactEmail": { + "label": "Contact Email", + "placeholder": "hostmaster.example.com.", + "tooltip": "DNS zone contact email address" + }, + "defaultTTL": { + "label": "Default TTL (seconds)", + "placeholder": "86400", + "tooltip": "Default TTL for zone records in seconds" + }, + "zoneConfiguration": "Zone Configuration" + }, + + "templateHelp": { + "title": "Available Placeholders:", + "placeholders": [ + { + "placeholder": "{ip}", + "description": "Original IP address (192.168.1.100)" + }, + { + "placeholder": "{ip-dashes}", + "description": "IP with dashes (192-168-1-100)" + }, + { + "placeholder": "{domain}", + "description": "Base domain from settings" + } + ] + }, + + "results": { + "title": "Generated Zone Files", + "summary": { + "zoneFiles": "Zone Files", + "ptrRecords": "PTR Records" + }, + "recordCount": "{count} records", + "copyZoneFile": "Copy Zone File", + "error": { + "title": "Generation Error", + "validFormats": "Valid formats:", + "ipv4CIDR": "IPv4 CIDR: 192.168.1.0/24, 10.0.0.0/16", + "ipv6CIDR": "IPv6 CIDR: 2001:db8::/64, fe80::/10" + } + }, + + "education": { + "zoneFileStructure": { + "title": "Zone File Structure", + "content": "Generated zone files include proper SOA records with serial numbers, refresh/retry/expire timers, and NS records for delegation. All PTR records are automatically generated based on your template." + }, + "hostnameTemplates": { + "title": "Hostname Templates", + "content": "Use placeholders to create consistent naming patterns. {ip-dashes} is popular for creating hostnames like host-192-168-1-100.example.com from IP addresses." + }, + "zoneDelegation": { + "title": "Zone Delegation", + "content": "The generated zones need to be properly delegated by your ISP or DNS provider. Ensure your name servers are configured to serve these zones and are reachable from the internet." + }, + "bestPractices": { + "title": "Best Practices", + "content": "Keep TTL values reasonable (3600-86400 seconds). Use descriptive hostnames that help with network troubleshooting. Ensure forward DNS (A/AAAA) records exist for consistency." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/reverse-zones-calculator.json b/src/lib/i18n/translations/en/tools/reverse-zones-calculator.json new file mode 100644 index 00000000..2b3d9a6b --- /dev/null +++ b/src/lib/i18n/translations/en/tools/reverse-zones-calculator.json @@ -0,0 +1,94 @@ +{ + "title": "Reverse Zones Calculator", + "description": "Calculate the minimal set of reverse DNS zones needed to delegate a CIDR block", + + "overview": { + "zoneBoundaries": { + "title": "Zone Boundaries", + "description": "IPv4 uses octet boundaries (/8, /16, /24) and IPv6 uses nibble boundaries (4-bit increments)." + }, + "delegation": { + "title": "Delegation", + "description": "DNS zones must be properly delegated by upstream providers at natural boundaries." + }, + "optimization": { + "title": "Optimization", + "description": "Calculate the minimal number of zones needed to avoid unnecessary complexity." + } + }, + + "examples": { + "title": "Quick Examples", + "ipv4_24": { + "label": "IPv4 /24 Network", + "description": "Single class C zone delegation" + }, + "ipv4_16": { + "label": "IPv4 /16 Network", + "description": "Class B with multiple /24 zones" + }, + "ipv4_20": { + "label": "IPv4 /20 Block", + "description": "16 class C zones needed" + }, + "ipv4_28": { + "label": "IPv4 /28 Subnet", + "description": "Small subnet within /24 zone" + }, + "ipv6_64": { + "label": "IPv6 /64 Network", + "description": "IPv6 nibble boundary delegation" + }, + "ipv6_48": { + "label": "IPv6 /48 Prefix", + "description": "IPv6 /48 delegation zone" + } + }, + + "input": { + "label": "CIDR Network", + "placeholder": "e.g., 192.168.1.0/24 or 2001:db8::/48", + "help": "Enter the network you need reverse DNS zones for" + }, + + "buttons": { + "calculate": "Calculate Zones", + "clear": "Clear" + }, + + "results": { + "title": "Reverse Zone Analysis", + "analysis": { + "title": "Zone Analysis", + "totalZones": "Total Zones", + "ipv4Zones": "IPv4 Zones", + "ipv6Zones": "IPv6 Zones", + "delegationType": "Delegation Type" + }, + "zones": { + "title": "Required Reverse Zones", + "zone": "Zone", + "type": "Type", + "delegation": "Delegation", + "addresses": "Addresses" + }, + "configuration": { + "title": "Configuration Examples", + "bindConfig": "BIND9 Configuration", + "delegationCommands": "Zone File Setup Commands" + } + }, + + "delegationTypes": { + "classC": "Class C (/24)", + "custom": "Custom", + "multipleZones": "Multiple zones", + "ipv6Nibble": "IPv6 nibble boundary" + }, + + "error": { + "title": "Calculation Error", + "noZones": "No reverse zones could be calculated for this CIDR", + "invalidInput": "Invalid CIDR format" + } +} diff --git a/src/lib/i18n/translations/en/tools/rp-builder.json b/src/lib/i18n/translations/en/tools/rp-builder.json new file mode 100644 index 00000000..b7cc39c5 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/rp-builder.json @@ -0,0 +1,120 @@ +{ + "title": "RP Record Builder", + "subtitle": "Create RP (Responsible Person) records to specify administrative contacts for your domains", + "examples": { + "title": "Common Role Examples", + "roles": { + "systemAdmin": { + "name": "System Administrator", + "description": "Primary system administrator contact" + }, + "webmaster": { + "name": "Webmaster", + "description": "Website administrator contact" + }, + "security": { + "name": "Security Contact", + "description": "Security incident response contact" + }, + "dnsAdmin": { + "name": "DNS Administrator", + "description": "DNS zone administrator" + } + } + }, + "form": { + "domain": { + "label": "Domain Name", + "placeholder": "example.com", + "help": "The domain name for this RP record", + "tooltip": "The domain name for which this RP record will be created" + }, + "converter": { + "title": "Email to Domain Name Converter", + "placeholder": "admin@example.com", + "button": "Convert", + "help": "Enter an email to automatically convert to domain name format" + }, + "mailbox": { + "label": "Mailbox Domain Name", + "placeholder": "admin.example.com.", + "help": "Domain name encoding the email address (use \".\" for no contact)", + "tooltip": "Domain name encoding the email address. Use '.' for no contact specified.", + "emailPreview": "Email:" + }, + "txt": { + "label": "TXT Domain Name", + "placeholder": "admin-info.example.com.", + "help": "Domain name where TXT record with contact info can be found (use \".\" for none)", + "tooltip": "Domain name where TXT record with additional contact information can be found. Use '.' for no additional info." + } + }, + "output": { + "rpRecord": "Generated RP Record", + "txtRecord": "Suggested TXT Record", + "txtHelp": "This TXT record should be created at the specified domain", + "placeholder": "Fill in the required fields to generate the RP record", + "invalidFormat": "Invalid format" + }, + "buttons": { + "copy": "Copy Records", + "copied": "Copied!", + "download": "Download", + "downloaded": "Downloaded!" + }, + "alerts": { + "info": { + "title": "Information", + "noMailbox": "Using \".\" for mailbox means no mailbox is specified", + "noTxt": "Using \".\" for TXT means no additional text information is provided", + "txtRecord": "Remember to create the TXT record at {txtDname} with contact information" + }, + "warnings": { + "title": "Configuration Warnings", + "mailboxFqdn": "Mailbox domain name should be a fully qualified domain name", + "txtFqdn": "TXT domain name should be a fully qualified domain name or \".\"", + "mailboxDot": "Domain names in RP records should end with a dot (.) for absolute names", + "txtDot": "TXT domain name should end with a dot (.) for absolute names" + } + }, + "info": { + "about": { + "title": "About RP Records", + "description": "RP (Responsible Person) records identify the responsible person for a domain or host. They specify both a mailbox (encoded as a domain name) and optionally point to a TXT record with additional contact information. This allows automated discovery of administrative contacts." + }, + "encoding": { + "title": "Email Encoding", + "description": "Email addresses are encoded as domain names:", + "examples": { + "simple": { + "email": "admin@example.com", + "encoded": "admin.example.com." + }, + "complex": { + "email": "user.name@example.com", + "encoded": "user\\.name.example.com." + } + }, + "note": "Dots in the local part are escaped with backslashes" + }, + "useCases": { + "title": "Common Use Cases", + "items": { + "zone": "Zone administrator contact", + "server": "Server administrator contact", + "security": "Security incident response", + "automated": "Automated contact discovery", + "compliance": "Compliance requirements" + } + }, + "bestPractices": { + "title": "Best Practices", + "items": { + "fqdn": "Always use fully qualified domain names ending with a dot", + "txtRecords": "Create corresponding TXT records with detailed contact information", + "upToDate": "Keep contact information up to date and monitored", + "rolesBased": "Consider creating role-based contacts rather than personal ones" + } + } + } +} diff --git a/src/lib/i18n/translations/en/tools/rrsig-planner.json b/src/lib/i18n/translations/en/tools/rrsig-planner.json new file mode 100644 index 00000000..240c746f --- /dev/null +++ b/src/lib/i18n/translations/en/tools/rrsig-planner.json @@ -0,0 +1,98 @@ +{ + "title": "RRSIG Planner", + "description": "Suggest RRSIG validity windows (inception/expiration) based on TTLs and desired overlap, with renewal lead-time guidance for automated DNSSEC signature management.", + + "form": { + "ttl": { + "label": "TTL (seconds)", + "error": "TTL must be between 1 and 86400 seconds" + }, + "overlap": { + "label": "Desired Overlap (hours)", + "error": "Overlap must be between 1 and 168 hours" + }, + "leadTime": { + "label": "Renewal Lead Time (hours)", + "error": "Lead time must be between 1 and 168 hours" + }, + "clockSkew": { + "label": "Clock Skew (hours)", + "error": "Clock skew must be between 0 and 24 hours" + }, + "validityDays": { + "label": "Signature Validity (days)", + "error": "Validity must be between 1 and 365 days" + } + }, + + "warnings": { + "title": "Timing Warnings:" + }, + + "windows": { + "current": { + "title": "Current Signature Window", + "copy": "Copy" + }, + "next": { + "title": "Next Signature Window", + "copySchedule": "Copy Full Schedule" + }, + "timing": { + "inception": "Inception (Start Time)", + "expiration": "Expiration (End Time)", + "renewal": "Renewal Time", + "nextInception": "Next Inception", + "nextExpiration": "Next Expiration", + "followingRenewal": "Following Renewal", + "renewalNote": "Generate next signatures before this time" + }, + "metrics": { + "validityPeriod": "Validity Period", + "leadTime": "Lead Time", + "overlapPeriod": "Overlap Period" + } + }, + + "guidelines": { + "title": "Implementation Guidelines", + "automation": { + "title": "Automation Schedule:", + "monitor": "Monitor renewal times continuously", + "generate": "Generate new signatures {leadTime} before expiration", + "maintain": "Maintain {overlap} overlap period", + "account": "Account for {clockSkew}h clock skew tolerance" + }, + "bestPractices": { + "title": "Best Practices:", + "test": "Test signature generation before deployment", + "monitor": "Monitor DNSSEC validation after updates", + "backup": "Keep backup signatures for rollback", + "log": "Log all signature generation events" + } + }, + + "education": { + "timing": { + "title": "RRSIG Timing", + "content": "RRSIG records have inception and expiration timestamps that define when the signature is valid. Proper timing ensures continuous DNSSEC validation during key transitions." + }, + "overlap": { + "title": "Overlap Strategy", + "content": "Overlapping signature validity periods prevent validation failures during rollover. New signatures should be generated before old ones expire." + }, + "clockSkew": { + "title": "Clock Skew Tolerance", + "content": "Account for time differences between authoritative servers and validators. Start signatures slightly in the past to accommodate clock skew." + }, + "automation": { + "title": "Automation Benefits", + "content": "Automated RRSIG generation reduces manual errors and ensures consistent timing. Plan renewal schedules based on TTL values and operational requirements." + } + }, + + "copyTemplates": { + "single": "RRSIG Timing Window:\nInception: {inception} ({inceptionTimestamp})\nExpiration: {expiration} ({expirationTimestamp})\nRenewal Time: {renewal}", + "schedule": "RRSIG Planning Schedule:\n\nCurrent Window:\nInception: {currentInception} ({currentInceptionTimestamp})\nExpiration: {currentExpiration} ({currentExpirationTimestamp})\nRenewal Time: {currentRenewal}\n\nNext Window:\nInception: {nextInception} ({nextInceptionTimestamp})\nExpiration: {nextExpiration} ({nextExpirationTimestamp})\nRenewal Time: {nextRenewal}" + } +} diff --git a/src/lib/i18n/translations/en/tools/spf-builder.json b/src/lib/i18n/translations/en/tools/spf-builder.json new file mode 100644 index 00000000..8bc199b2 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/spf-builder.json @@ -0,0 +1,124 @@ +{ + "title": "SPF Policy Builder", + "description": "Craft SPF (Sender Policy Framework) policies with mechanisms, qualifiers, and validation.", + + "mechanisms": { + "title": "SPF Mechanisms", + "tooltip": "Configure SPF mechanisms that define which servers can send email", + "addButton": "Add Custom", + "removeTooltip": "Remove this mechanism", + + "types": { + "all": { + "description": "Matches all IPs (should be last)" + }, + "include": { + "description": "Include another domains SPF record", + "placeholder": "_spf.google.com" + }, + "a": { + "description": "Match A/AAAA records of domain", + "placeholder": "domain.com (optional)" + }, + "mx": { + "description": "Match MX records of domain", + "placeholder": "domain.com (optional)" + }, + "ptr": { + "description": "Match PTR records (discouraged)", + "placeholder": "domain.com (optional)" + }, + "ip4": { + "description": "Match specific IPv4 address/range", + "placeholder": "203.0.113.1 or 203.0.113.0/24" + }, + "ip6": { + "description": "Match specific IPv6 address/range", + "placeholder": "2001:db8::1 or 2001:db8::/32" + }, + "exists": { + "description": "Check if domain exists", + "placeholder": "check.example.com" + } + }, + + "qualifiers": { + "pass": "+ Pass", + "fail": "- Fail", + "softFail": "~ SoftFail", + "neutral": "? Neutral" + } + }, + + "modifiers": { + "title": "SPF Modifiers", + "tooltip": "Optional SPF modifiers for advanced configuration", + + "redirect": { + "placeholder": "fallback.example.com" + }, + "exp": { + "placeholder": "explain.example.com" + } + }, + + "output": { + "title": "Generated SPF Record", + "copyButton": "Copy", + "copyTooltip": "Copy SPF record to clipboard", + "copied": "Copied!", + "exportButton": "Export", + "exportTooltip": "Download as zone file", + "downloaded": "Downloaded!", + "zoneFileFormat": "Zone File Format:" + }, + + "validation": { + "title": "Policy Validation", + "dnsLookupsLabel": "DNS Lookups:", + "recordLengthLabel": "Record Length:", + "statusLabel": "Status:", + "validStatus": "Valid", + "invalidStatus": "Invalid", + "successMessage": "SPF policy is valid and ready to use!", + + "errors": { + "noMechanisms": "At least one mechanism must be enabled", + "tooManyLookups": "Too many DNS lookups ({count}). SPF limit is 10.", + "mechanismRequiresDomain": "{type} mechanism requires a domain value", + "mechanismRequiresIP": "{type} mechanism requires an IP address", + "invalidIPv4": "Invalid IPv4 address/range: {value}", + "invalidIPv6": "Invalid IPv6 address: {value}", + "recordTooLong": "SPF record too long ({length} chars). DNS TXT limit is 255." + }, + + "warnings": { + "highLookupCount": "High DNS lookup count ({count}). Consider consolidating.", + "allShouldBeLast": "'all' mechanism should typically be last", + "ptrDiscouraged": "PTR mechanism is discouraged (slow and unreliable)", + "recordLong": "SPF record is long ({length} chars). Consider shortening.", + "redirectWithMechanisms": "redirect modifier should not be used with mechanisms" + } + }, + + "examples": { + "title": "Example Policies", + + "basic": { + "name": "Basic Email Provider", + "description": "Simple SPF policy for Google Workspace" + }, + "multiple": { + "name": "Multiple Providers", + "description": "SPF policy for multiple email services" + }, + "serverProvider": { + "name": "Server + Provider", + "description": "Dedicated server with email provider fallback" + }, + "strict": { + "name": "Strict Policy", + "description": "Restrictive SPF policy with hard fail" + } + } +} diff --git a/src/lib/i18n/translations/en/tools/subnet-calculator.json b/src/lib/i18n/translations/en/tools/subnet-calculator.json new file mode 100644 index 00000000..40211458 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/subnet-calculator.json @@ -0,0 +1,110 @@ +{ + "title": "Subnet Calculator", + "description": "Calculate IPv4 subnet information including network address, broadcast address, and usable host range.", + "input": { + "cidr_label": "CIDR Network", + "cidr_placeholder": "192.168.1.0/24" + }, + "sections": { + "network_info": "Network Information", + "host_info": "Host Information", + "binary_representation": "Binary Representation" + }, + "fields": { + "network_address": "Network Address", + "broadcast_address": "Broadcast Address", + "subnet_mask": "Subnet Mask", + "wildcard_mask": "Wildcard Mask", + "total_hosts": "Total Hosts", + "usable_hosts": "Usable Hosts", + "first_host": "First Host", + "last_host": "Last Host" + }, + "tooltips": { + "network_address": "The first IP address in a subnet, used to identify the network itself", + "broadcast_address": "The last IP address in a subnet, used to send messages to all devices", + "subnet_mask": "Defines which portion of IP address represents network vs host", + "wildcard_mask": "Inverse of subnet mask - used in ACLs", + "total_hosts": "All IP addresses in this subnet", + "usable_hosts": "IPs available for devices (excludes network/broadcast)", + "first_host": "First IP address available for devices", + "last_host": "Last IP address available for devices", + "network_binary": "Network address in binary format", + "mask_binary": "Subnet mask in binary format", + "broadcast_binary": "Broadcast address in binary format" + }, + "actions": { + "copy_network": "Copy network address to clipboard", + "copy_broadcast": "Copy broadcast address to clipboard", + "copy_network_aria": "Copy network address", + "copy_broadcast_aria": "Copy broadcast address", + "copied": "Copied!", + "calculating": "Calculating subnet..." + }, + "binary": { + "network_label": "Network:", + "mask_label": "Mask:", + "broadcast_label": "Broadcast:" + }, + "explainer": { + "title": "Understanding Subnetting", + "network_address": { + "title": "Network Address", + "description": "The first IP address in a subnet, used to identify the network itself. Hosts cannot be assigned this address as it represents the entire network segment." + }, + "broadcast_address": { + "title": "Broadcast Address", + "description": "The last IP address in a subnet, used to send messages to all devices on the network. When a packet is sent to this address, it reaches every host in the subnet." + }, + "subnet_mask": { + "title": "Subnet Mask", + "description": "Defines which portion of an IP address represents the network and which represents the host. A mask of /24 means the first 24 bits identify the network." + }, + "wildcard_mask": { + "title": "Wildcard Mask", + "description": "The inverse of a subnet mask, used in access control lists. Where the subnet mask has 1s, the wildcard has 0s, and vice versa." + }, + "usable_hosts": { + "title": "Usable Hosts", + "description": "The number of IP addresses available for devices. Always 2 less than total addresses because network and broadcast addresses are reserved." + }, + "cidr_notation": { + "title": "CIDR Notation", + "description": "Classless Inter-Domain Routing notation (e.g., /24) indicates how many bits are used for the network portion. Higher numbers mean smaller subnets with fewer hosts." + } + }, + "explainer": { + "title": "Understanding Subnetting", + "network_address": { + "title": "Network Address", + "description": "The first IP address in a subnet, used to identify the network itself. Hosts cannot be assigned this address as it represents the entire network segment." + }, + "broadcast_address": { + "title": "Broadcast Address", + "description": "The last IP address in a subnet, used to send messages to all devices on the network. When a packet is sent to this address, it reaches every host in the subnet." + }, + "subnet_mask": { + "title": "Subnet Mask", + "description": "Defines which portion of an IP address represents the network and which represents the host. A mask of /24 means the first 24 bits identify the network." + }, + "wildcard_mask": { + "title": "Wildcard Mask", + "description": "The inverse of a subnet mask, used in access control lists. Where the subnet mask has 1s, the wildcard has 0s, and vice versa." + }, + "usable_hosts": { + "title": "Usable Hosts", + "description": "The number of IP addresses available for devices. Always 2 less than total addresses because network and broadcast addresses are reserved." + }, + "cidr_notation": { + "title": "CIDR Notation", + "description": "Classless Inter-Domain Routing notation (e.g., /24) indicates how many bits are used for the network portion. Higher numbers mean smaller subnets with fewer hosts." + } + }, + "tips": { + "title": "πŸ’‘ Pro Tips", + "plan_growth": "Plan for Growth: Choose subnet sizes that accommodate future expansion", + "binary_understanding": "Binary Understanding: Learning binary helps understand how subnetting works", + "common_sizes": "Common Sizes: /24 (254 hosts), /25 (126 hosts), /26 (62 hosts), /30 (2 hosts for point-to-point)", + "private_networks": "Private Networks: Use RFC 1918 addresses (10.x.x.x, 172.16-31.x.x, 192.168.x.x) for internal networks" + } +} diff --git a/src/lib/i18n/translations/en/tools/subnet-planner.json b/src/lib/i18n/translations/en/tools/subnet-planner.json new file mode 100644 index 00000000..fdfe54c3 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/subnet-planner.json @@ -0,0 +1,118 @@ +{ + "title": "Subnet Planner", + "description": "Plan and design subnet allocation for networks with optimal address space utilization.", + "input": { + "parentNetwork": { + "label": "Parent Network", + "placeholder": "192.168.0.0/16", + "help": "The main network to subdivide" + }, + "requirements": { + "title": "Subnet Requirements", + "addSubnet": "Add Subnet", + "name": "Subnet Name", + "namePlaceholder": "e.g., Sales Department", + "hostsNeeded": "Hosts Needed", + "description": "Description", + "descriptionPlaceholder": "Optional description" + } + }, + "planning": { + "strategy": { + "title": "Planning Strategy", + "efficient": "Efficient Allocation", + "efficientDesc": "Minimize wasted address space", + "aligned": "Aligned Boundaries", + "alignedDesc": "Use clean subnet boundaries" + }, + "growthFactor": { + "label": "Growth Factor", + "tooltip": "Reserve additional space for future expansion" + } + }, + "results": { + "title": "Subnet Plan", + "summary": { + "title": "Planning Summary", + "totalSubnets": "Total Subnets", + "addressesUsed": "Addresses Used", + "addressesWasted": "Addresses Wasted", + "efficiency": "Efficiency" + }, + "plan": { + "title": "Subnet Allocation Plan", + "subnet": "Subnet", + "network": "Network", + "hosts": "Hosts", + "size": "Size", + "utilization": "Utilization" + } + }, + "actions": { + "plan": "Generate Plan", + "planning": "Planning...", + "export": "Export Plan", + "copy": "Copy", + "copied": "Copied!" + }, + "strategy": { + "title": "Allocation Strategy", + "fitBest": { + "label": "Best Fit", + "description": "Minimize address space waste" + }, + "preserveOrder": { + "label": "Preserve Order", + "description": "Allocate in the order specified" + }, + "usableHosts": { + "label": "Optimize for Usable Hosts", + "description": "Account for network and broadcast addresses" + } + }, + "parentNetwork": { + "label": "Parent Network", + "placeholder": "e.g., 192.168.0.0/16" + }, + "requirements": { + "title": "Subnet Requirements", + "addSubnet": "Add Subnet", + "clearAll": "Clear All", + "emptyState": "No subnet requirements defined. Add subnets to begin planning." + }, + "examples": { + "title": "Example Scenarios", + "officeNetwork": { + "label": "Small Office Network", + "subnets": { + "sales": "Sales Department (25 hosts)", + "engineering": "Engineering Team (15 hosts)", + "servers": "Server VLAN (10 hosts)" + } + }, + "largeCorporate": { + "label": "Large Corporate Network", + "subnets": { + "hq": "Headquarters (500 hosts)", + "branchOffice": "Branch Office (100 hosts)", + "dmz": "DMZ Servers (50 hosts)", + "management": "Management Network (20 hosts)" + } + }, + "dataCenter": { + "label": "Data Center Network", + "subnets": { + "webServers": "Web Server Farm (200 hosts)", + "databaseCluster": "Database Cluster (50 hosts)", + "loadBalancers": "Load Balancers (10 hosts)", + "monitoring": "Monitoring Systems (30 hosts)" + } + } + }, + "errors": { + "title": "Planning Error", + "invalidNetwork": "Invalid parent network", + "insufficientSpace": "Insufficient address space", + "planningFailed": "Subnet planning failed" + } +} diff --git a/src/lib/i18n/translations/en/tools/supernet-calculator.json b/src/lib/i18n/translations/en/tools/supernet-calculator.json new file mode 100644 index 00000000..73898579 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/supernet-calculator.json @@ -0,0 +1,144 @@ +{ + "title": "Supernet Calculator", + "description": "Aggregate multiple networks into a single supernet for route summarization and efficient routing table management.", + + "examples": { + "title": "Quick Examples", + "networks": "{count} Networks", + "moreNetworks": "+ {count} more", + "contiguous": { + "label": "Contiguous Networks", + "description": "Adjacent subnets that aggregate efficiently" + }, + "home": { + "label": "Home Network", + "description": "Typical residential setup with multiple VLANs" + }, + "homelab": { + "label": "Homelab Setup", + "description": "Self-hosted services and virtualization lab" + }, + "datacenter": { + "label": "Data Center Networks", + "description": "Well-planned contiguous allocation for servers" + }, + "campus": { + "label": "Campus Network", + "description": "University or enterprise campus subnets" + }, + "scattered": { + "label": "Scattered Networks", + "description": "Non-adjacent networks with limited aggregation" + } + }, + + "input": { + "title": "Input Networks", + "addNetwork": "Add Network", + "cidr": "CIDR", + "descriptionPlaceholder": "Description (optional)" + }, + + "analysis": { + "title": "Aggregation Analysis", + "efficiency": "Aggregation Efficiency", + "efficiencyTooltip": "How efficiently the networks can be aggregated - higher is better", + "canAggregate": "Can Aggregate", + "limitedAggregation": "Limited Aggregation", + "recommendations": "Recommendations" + }, + + "summary": { + "title": "Supernet Summary", + "address": "Supernet Address", + "addressTooltip": "The aggregated network address that encompasses all input networks", + "totalHosts": "Total Hosts", + "totalHostsTooltip": "Total number of host addresses available in the supernet" + }, + + "benefits": { + "title": "Route Aggregation Benefits", + "originalRoutes": "Original Routes", + "originalRoutesTooltip": "Number of individual routes before aggregation", + "aggregatedRoutes": "Aggregated Routes", + "aggregatedRoutesTooltip": "Number of routes after supernet aggregation", + "routesSaved": "Routes Saved", + "routesSavedTooltip": "Number of routes eliminated through aggregation", + "reduction": "Reduction", + "reductionTooltip": "Percentage reduction in routing table size" + }, + + "details": { + "title": "Supernet Details", + "networkAddress": "Network Address", + "networkAddressTooltip": "The first IP address in the supernet that identifies the network itself", + "subnetMask": "Subnet Mask", + "subnetMaskTooltip": "Defines which portion of the IP address represents the network vs host bits", + "wildcardMask": "Wildcard Mask", + "wildcardMaskTooltip": "Inverse of subnet mask, used in access control lists and routing protocols", + "addressRange": "Address Range", + "addressRangeTooltip": "First and last usable IP addresses in the supernet (excluding network and broadcast)", + "binaryMask": "Binary Subnet Mask", + "binaryMaskTooltip": "Binary representation of the subnet mask showing network (1) and host (0) bits" + }, + + "visualization": { + "title": "Network Visualization", + "heading": "Input Networks vs Supernet", + "description": "Visual representation of how individual networks are aggregated into a supernet", + "inputNetworks": "Input Networks", + "aggregatesTo": "Aggregates to", + "supernet": "Supernet", + "hosts": "{count} hosts" + }, + + "error": { + "title": "Calculation Error" + }, + + "page": { + "aboutTitle": "About Supernetting", + "aboutDescription": "Supernetting (also called route aggregation or CIDR block aggregation) is the process of combining multiple smaller networks into a single larger network. This technique is essential for:", + "benefits": { + "reducedRoutingTables": { + "title": "Reduced Routing Tables", + "description": "Fewer routes mean faster lookups and reduced memory usage in routers" + }, + "improvedScalability": { + "title": "Improved Scalability", + "description": "Internet routing scales better with aggregated routes instead of individual subnets" + }, + "betterPerformance": { + "title": "Better Performance", + "description": "Reduced route advertisements and faster convergence in routing protocols" + }, + "easierManagement": { + "title": "Easier Management", + "description": "Simplified network policies and access control lists" + } + }, + "useCases": { + "title": "When to Use Supernetting", + "ispRouteAggregation": { + "title": "ISP Route Aggregation", + "description": "Combining customer routes for BGP advertisements" + }, + "enterpriseNetworks": { + "title": "Enterprise Networks", + "description": "Summarizing branch office networks at headquarters" + }, + "dataCenters": { + "title": "Data Centers", + "description": "Aggregating server farm subnets for external advertisement" + }, + "networkRedesign": { + "title": "Network Redesign", + "description": "Optimizing existing IP allocations for better summarization" + } + }, + "proTip": { + "title": "Pro Tip", + "description": "For optimal supernetting, design your IP allocation strategy from the beginning. Contiguous, power-of-2 sized networks aggregate much more efficiently than scattered allocations." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/svcb-https-builder.json b/src/lib/i18n/translations/en/tools/svcb-https-builder.json new file mode 100644 index 00000000..c1944b54 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/svcb-https-builder.json @@ -0,0 +1,133 @@ +{ + "title": "SVCB/HTTPS Builder", + "description": "Build SVCB and HTTPS resource records with service parameters for enhanced service discovery and connection optimization.", + + "sections": { + "serviceConfiguration": "Service Configuration", + "serviceParameters": "Service Parameters", + "usageNotes": "Usage Notes" + }, + + "form": { + "domain": { + "label": "Domain:", + "placeholder": "example.com", + "tooltip": "Domain name for the SVCB/HTTPS record" + }, + "recordType": { + "label": "Record Type:", + "tooltip": "Record type: HTTPS for HTTP services, SVCB for general services" + }, + "priority": { + "label": "Priority:", + "placeholder": "1", + "tooltip": "Priority: 0 for alias mode, >0 for service mode" + }, + "targetName": { + "label": "Target Name:", + "placeholder": ". (same domain)", + "tooltip": "Target domain name or '.' for same domain" + } + }, + + "parameters": { + "descriptions": { + "mandatory": "Mandatory parameters that must be understood by the client", + "alpn": "Application-Layer Protocol Negotiation identifiers (e.g., h2, h3)", + "no-default-alpn": "Indicates that no default ALPN should be assumed", + "port": "Alternative port number for the service", + "ipv4hint": "IPv4 address hints to avoid additional DNS lookups", + "ech": "Encrypted Client Hello configuration", + "ipv6hint": "IPv6 address hints to avoid additional DNS lookups" + }, + "placeholders": { + "alpn": "h2,h3", + "port": "443", + "ipv4hint": "203.0.113.1,203.0.113.2", + "ipv6hint": "2001:db8::1,2001:db8::2", + "ech": "base64-encoded-config", + "mandatory": "1,3", + "default": "value" + } + }, + + "results": { + "generatedRecord": "Generated {recordType} Record", + "recordBreakdown": "Record Breakdown:", + "breakdown": { + "type": "Type:", + "priority": "Priority:", + "target": "Target:", + "parameters": "Parameters:", + "aliasMode": "Alias Mode", + "serviceMode": "Service Mode" + }, + "actions": { + "copy": { + "button": "Copy", + "copied": "Copied!", + "tooltip": "Copy record to clipboard" + }, + "export": { + "button": "Export", + "downloaded": "Downloaded!", + "tooltip": "Download as zone file" + } + } + }, + + "validation": { + "title": "Validation", + "status": { + "label": "Status:", + "valid": "Valid", + "invalid": "Invalid" + }, + "success": "{recordType} record is valid and ready to deploy!", + "errors": { + "domainRequired": "Domain is required", + "domainTld": "Domain should include TLD (e.g., .com, .org)", + "priorityRange": "Priority must be between 0 and 65535", + "priorityZeroTarget": "Priority 0 should typically use \".\" as target (alias mode)", + "targetFqdn": "Target name should be a FQDN or \".\" for same domain", + "portRange": "Port must be a number between 1 and 65535", + "alpnRequired": "ALPN parameter requires at least one protocol identifier", + "invalidIPv4": "Invalid IPv4 address in ipv4hint: {ip}", + "invalidIPv6": "Invalid IPv6 address in ipv6hint: {ip}", + "alpnConflict": "Using both alpn and no-default-alpn may cause conflicts", + "httpsPortRecommendation": "HTTPS records typically benefit from port parameter" + } + }, + + "examples": { + "title": "Example Configurations", + "items": [ + { + "name": "HTTPS with HTTP/2", + "description": "Basic HTTPS service with HTTP/2 support" + }, + { + "name": "CDN Endpoint", + "description": "HTTPS service pointing to CDN with IP hints" + }, + { + "name": "Alternative Service", + "description": "Alternative HTTPS service on different port" + } + ], + "config": { + "type": "Type:", + "priority": "Priority:", + "target": "Target:", + "params": "Params:" + } + }, + + "usageNotes": [ + "Priority 0 creates an alias record (AliasMode), priority >0 creates a service record (ServiceMode)", + "Use \".\" as target name to indicate the same domain as the owner name", + "ALPN values should match the protocols actually supported by the service", + "IP hints can improve connection performance by avoiding additional DNS lookups", + "ECH parameter enables Encrypted Client Hello for enhanced privacy" + ] +} diff --git a/src/lib/i18n/translations/en/tools/tlsa-generator.json b/src/lib/i18n/translations/en/tools/tlsa-generator.json new file mode 100644 index 00000000..55a148a8 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/tlsa-generator.json @@ -0,0 +1,175 @@ +{ + "title": "TLSA Generator", + "subtitle": "Create TLSA (DNS-based Authentication of Named Entities) records for certificate pinning and DANE implementation.", + "service": { + "title": "Service Configuration", + "domain": { + "label": "Domain:", + "placeholder": "example.com", + "tooltip": "Domain name for the TLSA record" + }, + "port": { + "label": "Port:", + "placeholder": "443", + "tooltip": "Port number for the service (e.g., 443 for HTTPS, 25 for SMTP)" + }, + "protocol": { + "label": "Protocol:", + "tooltip": "Protocol type (tcp or udp)", + "options": { + "tcp": "TCP", + "udp": "UDP" + } + } + }, + "parameters": { + "title": "TLSA Parameters", + "usage": { + "label": "Certificate Usage:", + "tooltip": "Certificate usage - how the certificate should be used for authentication", + "options": { + "0": "0 - CA Constraint", + "1": "1 - Service Certificate Constraint", + "2": "2 - Trust Anchor Assertion", + "3": "3 - Domain-Issued Certificate" + }, + "descriptions": { + "0": "CA Constraint - Certificate must be issued by the CA represented in the TLSA record", + "1": "Service Certificate Constraint - Certificate must match the one in the TLSA record", + "2": "Trust Anchor Assertion - Certificate must chain to the CA in the TLSA record", + "3": "Domain-Issued Certificate - Certificate must match the one specified (most common)" + } + }, + "selector": { + "label": "Selector:", + "tooltip": "Which part of the certificate to use", + "options": { + "0": "0 - Full Certificate", + "1": "1 - Subject Public Key Info" + }, + "descriptions": { + "0": "Full Certificate - Use the entire certificate", + "1": "Subject Public Key Info - Use only the public key portion (recommended)" + } + }, + "matchingType": { + "label": "Matching Type:", + "tooltip": "How to process the certificate data", + "options": { + "0": "0 - Exact Match", + "1": "1 - SHA-256 Hash", + "2": "2 - SHA-512 Hash" + }, + "descriptions": { + "0": "Exact Match - Use the certificate/key data as-is (not recommended)", + "1": "SHA-256 Hash - Use SHA-256 hash of the certificate/key (recommended)", + "2": "SHA-512 Hash - Use SHA-512 hash of the certificate/key" + } + } + }, + "certificate": { + "title": "Certificate Data", + "inputType": { + "certificate": "Certificate/Public Key (PEM)", + "hash": "Hash Value" + }, + "certificateInput": { + "label": "Certificate/Public Key:", + "placeholder": "-----BEGIN CERTIFICATE-----\nMIIFXzCCA0egAwIBAgIJAKZ5QeHxw...\n-----END CERTIFICATE-----", + "tooltip": "Paste the PEM-encoded certificate or public key" + }, + "hashInput": { + "label": "Hash Value:", + "tooltip": "Enter the {type} hash value", + "placeholders": { + "sha256": "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab", + "sha512": "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "exact": "Certificate or key data" + } + }, + "generateHash": { + "button": "Generate Hash", + "tooltip": "Generate hash from certificate (demo mode)" + } + }, + "output": { + "title": "Generated TLSA Record", + "copy": { + "button": "Copy", + "copied": "Copied!", + "tooltip": "Copy TLSA record to clipboard" + }, + "export": { + "button": "Export", + "downloaded": "Downloaded!", + "tooltip": "Download as zone file" + } + }, + "examples": { + "title": "Example Configurations", + "items": { + "https": { + "name": "HTTPS Certificate Pin", + "description": "Pin a specific certificate for HTTPS" + }, + "smtp": { + "name": "SMTP TLS Certificate", + "description": "DANE for email server TLS" + }, + "ca": { + "name": "CA Trust Anchor", + "description": "Trust anchor for certificate authority" + } + }, + "tooltip": "Load example configuration", + "selected": "Selected" + }, + "validation": { + "errors": { + "domainRequired": "Domain is required", + "portRange": "Port must be between 1 and 65535", + "certificateRequired": "Certificate data is required", + "hashRequired": "Hash value is required", + "hashFormat": "Hash must contain only hexadecimal characters" + }, + "warnings": { + "domainTld": "Domain should include TLD (e.g., .com, .org)", + "certificateFormat": "Certificate should be in PEM format", + "sha256Length": "SHA-256 hash should be exactly 64 hexadecimal characters", + "sha512Length": "SHA-512 hash should be exactly 128 hexadecimal characters", + "caManagement": "Usage types 0 and 2 require careful CA certificate management", + "fullCertificate": "Full certificate selector (0) is less flexible than SPKI selector (1)", + "exactMatch": "Exact match (0) is not recommended - use SHA-256 (1) or SHA-512 (2)" + }, + "title": { + "errors": "Validation Errors", + "warnings": "Warnings" + } + }, + "security": { + "title": "Security Best Practices", + "tips": { + "usageType": "Use usage type 3 (Domain-Issued Certificate) for most scenarios", + "selector": "Prefer selector 1 (SPKI) over selector 0 (full certificate) for flexibility", + "matching": "Use SHA-256 (1) or SHA-512 (2) matching types, avoid exact match (0)", + "multiple": "Pin multiple certificates to avoid service disruption during certificate rotation", + "testing": "Test TLSA records with DANE validation tools before deployment" + } + }, + "info": { + "about": { + "title": "About TLSA Records", + "description": "TLSA records provide DNS-based certificate constraints for TLS connections. They enable DANE (DNS-based Authentication of Named Entities), allowing domain owners to specify which certificates should be trusted for their services, independent of traditional Certificate Authorities." + }, + "usage": { + "title": "Common Use Cases", + "items": { + "pinning": "Certificate pinning for enhanced security", + "dane": "DANE implementation for email servers", + "ca": "Custom CA trust anchors", + "mitm": "Protection against man-in-the-middle attacks", + "compliance": "Meeting security compliance requirements" + } + } + } +} diff --git a/src/lib/i18n/translations/en/tools/ttl-calculator.json b/src/lib/i18n/translations/en/tools/ttl-calculator.json new file mode 100644 index 00000000..a2d43091 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ttl-calculator.json @@ -0,0 +1,117 @@ +{ + "title": "TTL Calculator", + "description": "Humanize DNS TTL values and compute cache expiry times from now or specific dates", + + "overview": { + "humanization": { + "title": "TTL Humanization:", + "content": "Convert seconds to human-readable formats like '1 hour' or '2 days 3 hours'" + }, + "cacheExpiry": { + "title": "Cache Expiry:", + "content": "Calculate when DNS records will expire from resolver caches" + }, + "guidelines": { + "title": "TTL Guidelines:", + "content": "Get recommendations based on record stability and update frequency" + } + }, + + "examples": { + "commonValues": { + "title": "Common TTL Values" + }, + "useCases": { + "title": "TTL by Use Case", + "scenarios": { + "loadBalancer": "Load Balancer IP", + "webServer": "Web Server A Record", + "mxRecord": "MX Record", + "nsRecord": "NS Record" + }, + "descriptions": { + "loadBalancer": "Short TTL for quick failover capability", + "webServer": "Standard TTL for web services", + "mxRecord": "Stable mail server configuration", + "nsRecord": "Authoritative name servers rarely change" + } + } + }, + + "input": { + "label": "TTL Value", + "placeholder": "Enter TTL value in seconds", + "tooltip": "Enter TTL value in seconds", + "customDateLabel": "Calculate expiry from custom date/time" + }, + + "results": { + "title": "TTL Analysis", + "copyTTL": "Copy TTL", + "humanReadable": "Human Readable", + "secondsLabel": "seconds", + "cacheExpiry": "Cache Expiry Times", + "fromNow": "From Now", + "fromCustomDate": "From Custom Date", + "summary": "Summary", + "guidelinesTitle": "TTL Guidelines by Category", + "recommendations": "Recommendations", + "details": "TTL Details" + }, + + "errors": { + "invalidValue": "Invalid TTL value", + "mustBePositive": "TTL must be a positive number", + "tooLarge": "TTL value is too large" + }, + + "units": { + "seconds": "seconds", + "minutes": "minutes", + "hours": "hours", + "days": "days", + "weeks": "weeks" + }, + + "guidelines": { + "veryShort": { + "label": "Very Short (< 5 min)", + "description": "High DNS load, instant propagation" + }, + "short": { + "label": "Short (5 min - 1 hr)", + "description": "Frequent changes, good for testing" + }, + "medium": { + "label": "Medium (1 hr - 1 day)", + "description": "Balanced performance and flexibility" + }, + "long": { + "label": "Long (1 day - 1 week)", + "description": "Stable records, reduced DNS queries" + }, + "veryLong": { + "label": "Very Long (> 1 week)", + "description": "Infrastructure, rarely changes" + } + }, + + "education": { + "tradeoffs": { + "title": "TTL Trade-offs", + "content": "Lower TTLs allow faster propagation of DNS changes but increase DNS query load. Higher TTLs reduce DNS traffic but slow down change propagation. Balance based on your needs." + }, + "cacheBehavior": { + "title": "Cache Behavior", + "content": "DNS resolvers cache records for the TTL duration. Once expired, they must query authoritative servers again. Some resolvers may cache slightly longer or shorter than the exact TTL." + }, + "changePlanning": { + "title": "Change Planning", + "content": "Before making DNS changes, consider lowering TTLs in advance. This reduces the time users see old records. After changes stabilize, you can increase TTLs again." + }, + "monitoringImpact": { + "title": "Monitoring Impact", + "content": "Monitor DNS query volumes when changing TTLs. Very short TTLs can significantly increase load on authoritative servers and may impact DNS provider costs." + } + } +} diff --git a/src/lib/i18n/translations/en/tools/ula-generator.json b/src/lib/i18n/translations/en/tools/ula-generator.json new file mode 100644 index 00000000..0c47d042 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/ula-generator.json @@ -0,0 +1,71 @@ +{ + "title": "ULA Generator", + "description": "Generate RFC 4193 Unique Local Addresses with cryptographically secure Global IDs.", + "form": { + "count": { + "label": "Number of ULAs to generate (1-100):", + "placeholder": "1" + }, + "subnetIds": { + "label": "Subnet IDs (optional, comma/newline separated):", + "placeholder": "0001, 0002, 0003 or leave empty for random generation", + "helpText": "If provided, must be 1-4 hex digits. Leave empty for random generation." + } + }, + "buttons": { + "generate": "Generate ULA Addresses", + "generating": "Generating...", + "copyTooltip": "Copy network address" + }, + "results": { + "summary": { + "title": "Generation Summary", + "stats": { + "totalRequested": "Total Requested:", + "successfullyGenerated": "Successfully Generated:", + "failed": "Failed:" + } + }, + "errors": { + "title": "Errors" + }, + "addresses": { + "title": "Generated ULA Addresses", + "ulaNumber": "ULA #", + "network": "Network:", + "prefix": "Prefix:", + "error": "- Error" + }, + "components": { + "title": "Address Components", + "ulaPrefix": "ULA Prefix:", + "globalId": "Global ID:", + "subnetId": "Subnet ID:" + }, + "details": { + "title": "Generation Details", + "algorithm": "Algorithm:", + "timestamp": "Timestamp:", + "entropy": "Entropy:" + } + }, + "parser": { + "title": "ULA Address Parser", + "description": "Parse and analyze existing ULA addresses to extract their components.", + "form": { + "address": { + "label": "ULA Address:", + "placeholder": "fd12:3456:789a:0001::/64" + } + }, + "results": { + "title": "Parsed Components", + "components": { + "ulaPrefix": "ULA Prefix:", + "globalId": "Global ID:", + "subnetId": "Subnet ID:", + "interfaceId": "Interface ID:" + } + } + } +} diff --git a/src/lib/i18n/translations/en/tools/vlsm-calculator.json b/src/lib/i18n/translations/en/tools/vlsm-calculator.json new file mode 100644 index 00000000..20fb5e0c --- /dev/null +++ b/src/lib/i18n/translations/en/tools/vlsm-calculator.json @@ -0,0 +1,127 @@ +{ + "title": "VLSM Calculator", + "description": "Design efficient subnets with Variable Length Subnet Masking for optimal address space utilization.", + "networkConfig": { + "title": "Network Configuration", + "networkAddress": { + "label": "Network Address", + "placeholder": "192.168.1.0" + }, + "cidrNotation": { + "label": "CIDR Notation" + } + }, + "subnetRequirements": { + "title": "Subnet Requirements", + "addSubnet": "Add Subnet", + "subnetName": { + "placeholder": "Subnet name" + }, + "hostsNeeded": "Hosts needed:", + "description": { + "placeholder": "Description (optional)" + } + }, + "summary": { + "title": "VLSM Summary", + "totalSubnets": "Total Subnets", + "hostsRequested": "Hosts Requested", + "hostsProvided": "Hosts Provided", + "wastedHosts": "Wasted Hosts", + "efficiency": "Efficiency", + "remainingAddresses": "Remaining Addresses" + }, + "table": { + "title": "Subnet Allocation Table", + "columns": { + "subnet": "Subnet", + "network": "Network", + "hosts": "Hosts", + "mask": "Mask", + "efficiency": "Efficiency", + "actions": "Actions" + }, + "hostsNeeded": "{count} needed", + "hostsProvided": "{count} provided", + "hostsWasted": "{count} wasted" + }, + "details": { + "networkAddress": { + "label": "Network Address", + "tooltip": "First IP address in the subnet - identifies the network" + }, + "broadcastAddress": { + "label": "Broadcast Address", + "tooltip": "Last IP address in the subnet - sends to all hosts" + }, + "firstUsableHost": { + "label": "First Usable Host", + "tooltip": "First IP address available for host assignment" + }, + "lastUsableHost": { + "label": "Last Usable Host", + "tooltip": "Last IP address available for host assignment" + }, + "subnetMask": { + "label": "Subnet Mask", + "tooltip": "Defines which portion of IP represents network vs host" + }, + "wildcardMask": { + "label": "Wildcard Mask", + "tooltip": "Inverse of subnet mask - used in access control lists" + }, + "binaryMask": { + "label": "Binary Mask", + "tooltip": "Binary representation of the subnet mask" + }, + "hostBits": { + "label": "Host Bits", + "tooltip": "Number of bits available for host addressing", + "value": "{count} bits" + } + }, + "details": { + "networkAddress": { + "label": "Network Address", + "tooltip": "First IP address in the subnet - identifies the network" + }, + "broadcastAddress": { + "label": "Broadcast Address", + "tooltip": "Last IP address in the subnet - sends to all hosts" + }, + "firstUsableHost": { + "label": "First Usable Host", + "tooltip": "First IP address available for host assignment" + }, + "lastUsableHost": { + "label": "Last Usable Host", + "tooltip": "Last IP address available for host assignment" + }, + "subnetMask": { + "label": "Subnet Mask", + "tooltip": "Defines which portion of IP represents network vs host" + }, + "wildcardMask": { + "label": "Wildcard Mask", + "tooltip": "Inverse of subnet mask - used in access control lists" + }, + "binaryMask": { + "label": "Binary Mask", + "tooltip": "Binary representation of the subnet mask" + }, + "hostBits": { + "label": "Host Bits", + "tooltip": "Number of bits available for host addressing", + "value": "{count} bits" + } + }, + "actions": { + "expandDetails": "Expand subnet details", + "collapseDetails": "Collapse subnet details", + "copyNetworkInfo": "Copy network info" + }, + "error": { + "title": "Calculation Error" + }, + "defaultSubnetName": "Subnet {number}" +} diff --git a/src/lib/i18n/translations/en/tools/wildcard-mask.json b/src/lib/i18n/translations/en/tools/wildcard-mask.json new file mode 100644 index 00000000..18305783 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/wildcard-mask.json @@ -0,0 +1,165 @@ +{ + "title": "Wildcard Mask Converter", + "subtitle": "Convert between CIDR notation, subnet masks, and wildcard masks with ACL rule generation", + + "examples": { + "title": "Quick Examples", + "items": { + "basicCidr": { + "label": "Basic CIDR to Wildcard", + "preview": "Conversion only" + }, + "subnetMask": { + "label": "Subnet Mask Format", + "preview": "Conversion only" + }, + "wildcardInput": { + "label": "Wildcard Mask Input", + "preview": "Conversion only" + }, + "mixedFormats": { + "label": "Mixed Formats", + "preview": "Conversion only" + }, + "ciscoAcl": { + "label": "Cisco ACL Generation", + "preview": "With ACL" + }, + "complexAcl": { + "label": "Complex Network ACLs", + "preview": "With ACL" + } + } + }, + + "networkInputs": { + "title": "Network Inputs", + "titleTooltip": "Enter networks in various formats for wildcard mask conversion", + "label": "IP Addresses, CIDRs, or Ranges", + "labelTooltip": "Enter networks in CIDR, subnet mask, or wildcard mask format", + "placeholder": "192.168.1.0/24\n10.0.0.0 255.255.255.0\n172.16.0.0 0.0.255.255", + "help": "Enter one per line: CIDR (192.168.1.0/24), network + subnet mask (10.0.0.0 255.255.255.0), or network + wildcard mask (172.16.0.0 0.0.255.255)" + }, + + "aclOptions": { + "title": "ACL Options", + "titleTooltip": "Configure access control list rule generation for network devices", + "generateLabel": "Generate ACL Rules", + "generateTooltip": "Generate access control list rules for network devices", + + "action": { + "label": "Action", + "tooltip": "Whether to permit or deny traffic matching this rule", + "permit": "Permit", + "deny": "Deny" + }, + + "protocol": { + "label": "Protocol", + "tooltip": "Network protocol (ip, tcp, udp, icmp, etc.)", + "placeholder": "ip" + }, + + "destination": { + "label": "Destination", + "tooltip": "Destination network or 'any' for all destinations", + "placeholder": "any" + } + }, + + "loading": "Converting masks...", + + "errors": { + "title": "Errors" + }, + + "summary": { + "title": "Conversion Summary", + "titleTooltip": "Overview of wildcard mask conversion results", + "totalInputs": { + "label": "Total Inputs", + "tooltip": "Total number of network inputs processed" + }, + "valid": { + "label": "Valid", + "tooltip": "Successfully converted network inputs" + }, + "invalid": { + "label": "Invalid", + "tooltip": "Network inputs that could not be converted" + } + }, + + "conversions": { + "title": "Mask Conversions", + "titleTooltip": "Detailed conversion results for each network input", + "exportCsv": "Export CSV", + "exportJson": "Export JSON", + + "status": { + "valid": "Valid", + "invalid": "Invalid" + }, + + "details": { + "cidr": { + "label": "CIDR:", + "tooltip": "Classless Inter-Domain Routing notation" + }, + "subnetMask": { + "label": "Subnet Mask:", + "tooltip": "Standard subnet mask in dotted decimal notation" + }, + "wildcardMask": { + "label": "Wildcard Mask:", + "tooltip": "Inverse subnet mask used in Cisco ACLs and OSPF" + }, + "network": { + "label": "Network:", + "tooltip": "First address in the network range" + }, + "broadcast": { + "label": "Broadcast:", + "tooltip": "Last address in the network range" + }, + "hostBits": { + "label": "Host Bits:", + "tooltip": "Number of bits available for host addresses" + }, + "usableHosts": { + "label": "Usable Hosts:", + "tooltip": "Total assignable host addresses (excluding network and broadcast)" + } + }, + + "copyTooltip": "Copy to clipboard" + }, + + "aclRules": { + "title": "Generated ACL Rules", + "titleTooltip": "Access control list rules generated for network devices", + + "cisco": { + "title": "Cisco ACL", + "tooltip": "Cisco IOS access control list format", + "copyTooltip": "Copy all Cisco ACL rules to clipboard" + }, + + "juniper": { + "title": "Juniper ACL", + "tooltip": "Juniper JunOS firewall filter format", + "copyTooltip": "Copy all Juniper ACL rules to clipboard" + }, + + "generic": { + "title": "Generic ACL", + "tooltip": "Generic access control list format", + "copyTooltip": "Copy all generic ACL rules to clipboard" + } + }, + + "common": { + "copy": "Copy", + "copied": "Copied!" + } +} diff --git a/src/lib/i18n/translations/en/tools/zone-stats.json b/src/lib/i18n/translations/en/tools/zone-stats.json new file mode 100644 index 00000000..28db5cb6 --- /dev/null +++ b/src/lib/i18n/translations/en/tools/zone-stats.json @@ -0,0 +1,128 @@ +{ + "title": "DNS Zone Statistics", + "description": "Analyze zone file structure, record distribution, and configuration health", + + "overview": { + "recordAnalysis": { + "title": "Record Analysis:", + "content": "Count and categorize all DNS records by type and TTL." + }, + "sizeMetrics": { + "title": "Size Metrics:", + "content": "Identify largest records and analyze name length distribution." + }, + "healthChecks": { + "title": "Health Checks:", + "content": "Validate zone structure and identify potential issues." + } + }, + + "examples": { + "title": "Zone Analysis Examples", + "simple": { + "name": "Simple Zone", + "description": "Basic zone with common record types" + }, + "complex": { + "name": "Complex Zone", + "description": "Comprehensive zone with diverse record types and TTLs" + }, + "large": { + "name": "Large Organization", + "description": "Large organization with multiple services and locations" + } + }, + + "input": { + "label": "Zone File Content", + "tooltip": "Paste your DNS zone file for comprehensive statistical analysis" + }, + + "results": { + "title": "Zone Analysis Report", + "copy": "Copy Report", + "copied": "Copied!", + "stats": { + "totalRecords": "Total Records", + "recordTypes": "Record Types", + "uniqueTtls": "Unique TTLs", + "avgNameLength": "Avg Name Length" + }, + "recordDistribution": { + "title": "Record Type Distribution", + "records": "record", + "recordsPlural": "records" + }, + "ttlDistribution": { + "title": "TTL Distribution", + "labels": { + "veryShort": "Very Short", + "short": "Short", + "medium": "Medium", + "long": "Long" + } + }, + "nameAnalysis": { + "title": "Name Length Analysis", + "shortestName": "Shortest Name", + "longestName": "Longest Name", + "averageLength": "Average Length", + "chars": "chars" + }, + "largestRecord": { + "title": "Largest Record", + "bytes": "bytes" + }, + "healthChecks": { + "title": "Zone Health Checks", + "soaPresent": "SOA Record Present", + "nsPresent": "NS Records Present", + "noDuplicates": "No Duplicate Records", + "duplicateRecords": "Duplicate Record", + "duplicateRecordsPlural": "Duplicate Records", + "noOrphanedGlue": "No Orphaned Glue Records", + "orphanedGlue": "Orphaned Glue Record", + "orphanedGluePlural": "Orphaned Glue Records" + } + }, + + "copyTemplate": { + "title": "DNS Zone Statistics Report", + "separator": "========================", + "totalRecords": "Total Records: {count}", + "recordsByType": "Records by Type:", + "ttlDistribution": "TTL Distribution:", + "ttlEntry": "{ttl}s: {count} record{plural}", + "nameStats": "Name Statistics:", + "shortestName": "Shortest name: {length} characters", + "longestName": "Longest name: {length} characters", + "averageLength": "Average length: {length} characters", + "largestRecord": "Largest Record: {size} bytes", + "zoneHealth": "Zone Health:", + "hasSoa": "Has SOA: {status}", + "hasNs": "Has NS records: {status}", + "duplicates": "Duplicate records: {count}", + "orphanedGlue": "Orphaned glue: {count}", + "yes": "Yes", + "no": "No" + }, + + "education": { + "statistics": { + "title": "Zone Statistics", + "content": "Zone statistics help understand DNS structure, identify optimization opportunities, and spot potential issues. Analyze record distribution, TTL patterns, and naming conventions for better zone management." + }, + "ttlStrategy": { + "title": "TTL Strategy", + "content": "TTL distribution reveals caching patterns. Short TTLs enable quick changes but increase DNS load. Long TTLs reduce queries but slow propagation. Balance based on change frequency and traffic patterns." + }, + "recordAnalysis": { + "title": "Record Analysis", + "content": "Record type distribution shows zone complexity. Heavy A/AAAA records suggest web services, many MX records indicate mail infrastructure, and diverse types show comprehensive DNS usage." + }, + "healthMonitoring": { + "title": "Health Monitoring", + "content": "Regular zone analysis catches configuration drift, identifies duplicates, and ensures essential records exist. Use statistics to track zone growth and optimize DNS performance over time." + } + } +} diff --git a/src/lib/stores/language.ts b/src/lib/stores/language.ts new file mode 100644 index 00000000..19fe8df3 --- /dev/null +++ b/src/lib/stores/language.ts @@ -0,0 +1,223 @@ +/** + * Language Store + * Reactive store for managing current language and translations + */ + +import { writable, derived, get } from 'svelte/store'; +import { browser } from '$app/environment'; +import { i18n, type TranslationObject, type InterpolationParams } from '$lib/i18n'; +import { detectLanguage, setStoredLanguage, buildLocalizedPath } from '$lib/i18n/lang-detector'; +import { DEFAULT_LANGUAGE, type Language, SUPPORTED_LANGUAGES, LANGUAGE_CODES } from '$lib/i18n/supported-languages'; + +// Import all English translations eagerly (synchronously at build time) +// This prevents flash of keys while maintaining code simplicity +const enTranslations = import.meta.glob<Record<string, any>>('../i18n/translations/en/**/*.json', { + eager: true, + import: 'default', +}); + +/* Current locale store */ +export const locale = writable<string>(DEFAULT_LANGUAGE); + +/* Loaded translations store */ +export const translations = writable<Record<string, TranslationObject>>({}); + +/* Available languages */ +export const languages = writable<Language[]>(SUPPORTED_LANGUAGES); + +/* Track loaded namespaces to avoid re-loading */ +const loadedNamespaces = new Set<string>(); + +/** + * Convert file path to namespace key + * e.g., '../i18n/translations/en/tools/ip-converter.json' -> 'tools/ip-converter' + */ +function pathToNamespace(path: string): string { + return path.replace('../i18n/translations/en/', '').replace('.json', ''); +} + +/** + * Initialize English translations immediately / synchronously + */ +function initializeEnglishTranslations() { + const enTranslationsObject: TranslationObject = {}; + + // Process all eagerly-loaded English translations + for (const [path, module] of Object.entries(enTranslations)) { + const namespace = pathToNamespace(path); + const translationData = module as Record<string, any>; + + // Add to i18n manager + i18n.addNamespace(DEFAULT_LANGUAGE, namespace, translationData); + + // Track as loaded + loadedNamespaces.add(`${DEFAULT_LANGUAGE}:${namespace}`); + + // Add to translations object + enTranslationsObject[namespace] = translationData; + } + + // Update store once with all translations + translations.set({ + [DEFAULT_LANGUAGE]: enTranslationsObject, + }); +} + +// Initialize English immediately on module load +initializeEnglishTranslations(); + +/** + * Initialize language from detection + */ +export function initLanguage(pathname?: string): void { + const detected = detectLanguage(pathname); + + // Validate and fallback to English if invalid + const validLang = LANGUAGE_CODES.includes(detected) ? detected : DEFAULT_LANGUAGE; + + locale.set(validLang); + i18n.setLocale(validLang); + + // Load namespaces for non-English languages + if (validLang !== DEFAULT_LANGUAGE) { + loadNamespaces(validLang, ['common', 'nav', 'settings', 'tools']); + } +} + +/** + * Change language + */ +export function setLanguage(lang: string): void { + // Validate language code + if (!LANGUAGE_CODES.includes(lang)) { + console.warn(`[i18n] Invalid language code: ${lang}, falling back to ${DEFAULT_LANGUAGE}`); + lang = DEFAULT_LANGUAGE; + } + + locale.set(lang); + i18n.setLocale(lang); + setStoredLanguage(lang); + + // Pre-load common namespaces for the new language + if (lang !== DEFAULT_LANGUAGE) { + loadNamespaces(lang, ['common', 'nav', 'settings', 'tools']); + } +} + +/** + * Load translations for a namespace + */ +export async function loadTranslations(lang: string, namespace: string): Promise<void> { + const key = `${lang}:${namespace}`; + + // Skip if already loaded + if (loadedNamespaces.has(key)) { + return; + } + + // English is pre-loaded, just mark as loaded + if (lang === DEFAULT_LANGUAGE) { + loadedNamespaces.add(key); + return; + } + + try { + // Dynamic import for non-English languages only + // Using switch to avoid bundler warnings about en/* being both static and dynamic + let module; + switch (lang) { + case 'de': + module = await import(`../i18n/translations/de/${namespace}.json`); + break; + case 'es': + module = await import(`../i18n/translations/es/${namespace}.json`); + break; + case 'fr': + module = await import(`../i18n/translations/fr/${namespace}.json`); + break; + default: + throw new Error(`Unsupported language: ${lang}`); + } + + i18n.addNamespace(lang, namespace, module.default || module); + + // Mark as loaded + loadedNamespaces.add(key); + + // Update translations store for reactivity + translations.update((current) => ({ + ...current, + [lang]: { + ...current[lang], + [namespace]: module.default || module, + }, + })); + } catch (error) { + console.warn(`[i18n] Failed to load ${lang}/${namespace}, English fallback will be used`, error); + + // Mark English namespace as fallback + const enKey = `${DEFAULT_LANGUAGE}:${namespace}`; + if (!loadedNamespaces.has(enKey)) { + loadedNamespaces.add(enKey); + } + } +} + +/** + * Load multiple namespaces at once + */ +export async function loadNamespaces(lang: string, namespaces: string[]): Promise<void> { + await Promise.all(namespaces.map((ns) => loadTranslations(lang, ns))); +} + +/** + * Reactive translation function + * Usage in components: $t('key.path', { param: value }) + * + * ALWAYS falls back to English if translation is missing + * NEVER shows translation keys to users + */ +export const t = derived([locale, translations], ([_$locale, _$translations]) => { + return (key: string, params?: InterpolationParams): string => { + return i18n.t(key, params); + }; +}); + +/** + * Reactive raw translation function + * Returns raw translation values (arrays, objects) without string conversion + * Used by content files that need structured data + */ +export const tRaw = derived([locale, translations], ([_$locale, _$translations]) => { + return (key: string): any => { + return i18n.getRaw(key); + }; +}); + +/** + * Get localized path for current language + */ +export function localizedPath(path: string): string { + const currentLocale = get(locale); + return buildLocalizedPath(currentLocale, path); +} + +/** + * Navigate to localized path + * Helper for goto(localizedNavigate('/settings')) + */ +export function localizedNavigate(path: string): string { + return localizedPath(path); +} + +/** + * Subscribe to locale changes + */ +export function onLocaleChange(callback: (lang: string) => void): () => void { + return locale.subscribe(callback); +} + +// Auto-initialize on client +if (browser) { + initLanguage(window.location.pathname); +} diff --git a/src/params/lang.ts b/src/params/lang.ts new file mode 100644 index 00000000..e8b7f0dc --- /dev/null +++ b/src/params/lang.ts @@ -0,0 +1,20 @@ +/** + * Route Parameter Matcher for Language Codes + * Validates language codes in routes like /[lang=lang]/settings + */ + +import { LANGUAGE_CODES, DEFAULT_LANGUAGE } from '$lib/i18n/supported-languages'; + +/** + * Match function for SvelteKit parameter matching + * Only matches non-English language codes (English has no prefix) + */ +export function match(param: string): boolean { + // English doesn't use URL prefix + if (param === DEFAULT_LANGUAGE) { + return false; + } + + // Check if it's a supported language + return LANGUAGE_CODES.includes(param); +} diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 1ebff0fa..1b5e6cb2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -24,6 +24,7 @@ import { ALL_PAGES } from '$lib/constants/nav'; import { initializeOfflineSupport } from '$lib/stores/offline'; import { bookmarks } from '$lib/stores/bookmarks'; + import { initLanguage, loadNamespaces, locale } from '$lib/stores/language'; import { ANALYTICS_ENABLED, ANALYTICS_DOMAIN, ANALYTICS_DSN } from '$lib/config/customizable-settings'; import Header from '$lib/components/furniture/Header.svelte'; @@ -132,6 +133,13 @@ } onMount(() => { + // Initialize language first + initLanguage($page.url.pathname); + + // Load base translations asynchronously + const currentLocale = $locale; + loadNamespaces(currentLocale, ['common', 'nav']); + theme.init(); toolUsage.init(); accessibility.init(); diff --git a/src/routes/[lang]/+layout.svelte b/src/routes/[lang]/+layout.svelte new file mode 100644 index 00000000..01eafd7a --- /dev/null +++ b/src/routes/[lang]/+layout.svelte @@ -0,0 +1,21 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + import { page } from '$app/stores'; + import { initLanguage, loadNamespaces } from '$lib/stores/language'; + + let { children } = $props(); + + onMount(async () => { + // Initialize language from URL parameter + const langParam = $page.params.lang; + if (langParam) { + initLanguage(`/${langParam}${$page.url.pathname.replace(`/${langParam}`, '')}`); + + // Load base translations for the language + await loadNamespaces(langParam, ['common', 'nav', 'settings']); + } + }); +</script> + +<!-- Render children (the actual page content) --> +{@render children?.()} diff --git a/src/routes/[lang]/about/(sections)/api/+page.svelte b/src/routes/[lang]/about/(sections)/api/+page.svelte new file mode 100644 index 00000000..97f75f03 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/api/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import ApiSection from '$lib/components/page-specific/about/ApiSection.svelte'; +</script> + +<ApiSection /> diff --git a/src/routes/[lang]/about/(sections)/attributions/+page.svelte b/src/routes/[lang]/about/(sections)/attributions/+page.svelte new file mode 100644 index 00000000..58e704f6 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/attributions/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import AttributionsSection from '$lib/components/page-specific/about/AttributionsSection.svelte'; +</script> + +<AttributionsSection /> diff --git a/src/routes/[lang]/about/(sections)/author/+page.svelte b/src/routes/[lang]/about/(sections)/author/+page.svelte new file mode 100644 index 00000000..afea63d9 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/author/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import AuthorSection from '$lib/components/page-specific/about/AuthorSection.svelte'; +</script> + +<AuthorSection longMode={true} /> diff --git a/src/routes/[lang]/about/(sections)/building/+page.svelte b/src/routes/[lang]/about/(sections)/building/+page.svelte new file mode 100644 index 00000000..15c98688 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/building/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import BuildingSection from '$lib/components/page-specific/about/BuildingSection.svelte'; +</script> + +<BuildingSection /> diff --git a/src/routes/[lang]/about/(sections)/deploying/+page.svelte b/src/routes/[lang]/about/(sections)/deploying/+page.svelte new file mode 100644 index 00000000..28444182 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/deploying/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import DeployingSection from '$lib/components/page-specific/about/DeployingSection.svelte'; +</script> + +<DeployingSection /> diff --git a/src/routes/[lang]/about/(sections)/self-hosting/+page.svelte b/src/routes/[lang]/about/(sections)/self-hosting/+page.svelte new file mode 100644 index 00000000..3d442ad8 --- /dev/null +++ b/src/routes/[lang]/about/(sections)/self-hosting/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import SelfHostingSection from '$lib/components/page-specific/about/SelfHostingSection.svelte'; +</script> + +<SelfHostingSection /> diff --git a/src/routes/[lang]/about/(sections)/support/+page.svelte b/src/routes/[lang]/about/(sections)/support/+page.svelte new file mode 100644 index 00000000..240d270c --- /dev/null +++ b/src/routes/[lang]/about/(sections)/support/+page.svelte @@ -0,0 +1,5 @@ +<script lang="ts"> + import SupportSection from '$lib/components/page-specific/about/SupportSection.svelte'; +</script> + +<SupportSection /> diff --git a/src/routes/[lang]/about/+layout.svelte b/src/routes/[lang]/about/+layout.svelte new file mode 100644 index 00000000..225e1c3d --- /dev/null +++ b/src/routes/[lang]/about/+layout.svelte @@ -0,0 +1,23 @@ +<script> + import { page } from '$app/stores'; + import '../../../styles/page-specific/about-page.scss'; + + let { children } = $props(); + + const isLicensePage = $derived(($page.url?.pathname ?? '/').includes('/license')); +</script> + +<main class="card about-content" class:license-page={isLicensePage}> + {@render children()} +</main> + +<style lang="scss"> + .about-content { + max-width: 1200px; + margin: 0 auto; + + &.license-page { + width: fit-content; + } + } +</style> diff --git a/src/routes/[lang]/about/+page.svelte b/src/routes/[lang]/about/+page.svelte new file mode 100644 index 00000000..abec032d --- /dev/null +++ b/src/routes/[lang]/about/+page.svelte @@ -0,0 +1,78 @@ +<script lang="ts"> + import '../../../styles/pages.scss'; + import { author, site } from '$lib/constants/site'; + + // Import section components + import ApiSection from '$lib/components/page-specific/about/ApiSection.svelte'; + import SelfHostingSection from '$lib/components/page-specific/about/SelfHostingSection.svelte'; + import TipsSection from '$lib/components/page-specific/about/TipsSection.svelte'; + import BuildingSection from '$lib/components/page-specific/about/BuildingSection.svelte'; + import AuthorSection from '$lib/components/page-specific/about/AuthorSection.svelte'; + import AttributionsSection from '$lib/components/page-specific/about/AttributionsSection.svelte'; + import LicenseSection from '$lib/components/page-specific/about/LicenseSection.svelte'; +</script> + +<!-- Section 0: Hero --> +<div class="hero"> + <h2>About {site.title}</h2> + <p class="lead"> + Networking Toolbox is a collection of free, open-source networking utilities designed to simplify common + network-related tasks for system administrators and network engineers. + </p> +</div> +<!-- Links #1: GitHub --> +<!-- Links #1: Live Demo, DockerHub, CodeBerg Mirror, Sponsor, More Apps --> +<section class="contents"> + <div> + <h3>About</h3> + <ul> + <li><a href="/about/api">API</a></li> + <li><a href="/about/deploying">Self-Hosting</a></li> + <li><a href="/about/building">Developing</a></li> + <li><a href="/about/support">Get Support</a></li> + <li><a href="/about/attributions">Attributions</a></li> + <li><a href="/about/author">About Author</a></li> + <li><a href="/about/legal/license">License</a></li> + </ul> + </div> + <div> + <h3>External links</h3> + <ul> + <li><a href={site.repo}>Source on GitHub</a></li> + <li><a href={site.mirror}>CodeBerg mirror</a></li> + <li><a href={site.docker}>DockerHub</a></li> + <li><a href={author.portfolio}>More apps...</a></li> + </ul> + </div> +</section> + +<TipsSection /> +<ApiSection /> +<SelfHostingSection /> +<BuildingSection /> +<AttributionsSection /> +<AuthorSection /> +<LicenseSection /> + +<style lang="scss"> + .hero { + margin-bottom: var(--spacing-xl); + h2 { + font-size: var(--font-size-2xl); + margin-bottom: var(--spacing-lg); + } + .lead { + font-size: var(--font-size-md); + color: var(--text-secondary); + } + } + + .contents { + display: grid; + grid-template-columns: 1fr 1fr; + gap: var(--spacing-2xl); + @media (max-width: 700px) { + grid-template-columns: 1fr; + } + } +</style> diff --git a/src/routes/[lang]/about/legal/+layout.svelte b/src/routes/[lang]/about/legal/+layout.svelte new file mode 100644 index 00000000..c5f7892e --- /dev/null +++ b/src/routes/[lang]/about/legal/+layout.svelte @@ -0,0 +1,91 @@ +<script lang="ts"> + import '../../../../styles/pages.scss'; +</script> + +<div class="legal-page"> + <slot /> +</div> + +<!-- svelte-ignore css-unused-selector --> +<style lang="scss"> + .legal-page { + .hero { + margin-bottom: var(--spacing-xl); + h2 { + font-size: var(--font-size-2xl); + margin-bottom: var(--spacing-lg); + } + .lead { + font-size: var(--font-size-md); + color: var(--text-secondary); + } + } + + section { + margin-bottom: var(--spacing-2xl); + + h3 { + font-size: var(--font-size-xl); + margin-bottom: var(--spacing-md); + color: var(--text-primary); + } + + h4 { + font-size: var(--font-size-lg); + margin-top: var(--spacing-lg); + margin-bottom: var(--spacing-sm); + color: var(--text-primary); + } + + p { + line-height: 1.7; + color: var(--text-secondary); + margin-bottom: var(--spacing-md); + } + + ul { + margin: var(--spacing-md) 0; + padding-left: var(--spacing-xl); + + li { + line-height: 1.7; + color: var(--text-secondary); + margin-bottom: var(--spacing-sm); + + strong { + color: var(--text-primary); + } + } + } + + a { + color: var(--accent); + text-decoration: none; + transition: opacity 0.2s ease; + + &:hover { + opacity: 0.8; + text-decoration: underline; + } + } + } + + .updated { + font-size: var(--font-size-sm); + font-style: italic; + color: var(--text-muted); + margin-top: var(--spacing-lg); + } + + .contact { + padding: var(--spacing-lg); + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--accent), transparent 95%), + color-mix(in srgb, var(--accent), transparent 98%) + ); + border-radius: var(--border-radius); + border-left: 4px solid var(--accent); + } + } +</style> diff --git a/src/routes/[lang]/about/legal/+page.svelte b/src/routes/[lang]/about/legal/+page.svelte new file mode 100644 index 00000000..7e1ebd5e --- /dev/null +++ b/src/routes/[lang]/about/legal/+page.svelte @@ -0,0 +1,176 @@ +<script lang="ts"> + import '../../../../styles/pages.scss'; + import { site } from '$lib/constants/site'; + import { legalPages } from '$lib/constants/nav'; + import Icon from '$lib/components/global/Icon.svelte'; +</script> + +<svelte:head> + <title>Legal | Networking Toolbox + + + + + + +
+

Legal Information

+

Boring (but important) stuff

+
+ + + +
+

Summary

+

+ Networking Toolbox is open source software licensed under the MIT License. We respect your privacy and process most + data locally in your browser. For more details, please review the individual documents above. +

+
+ + diff --git a/src/routes/[lang]/about/legal/accessibility/+page.svelte b/src/routes/[lang]/about/legal/accessibility/+page.svelte new file mode 100644 index 00000000..92910168 --- /dev/null +++ b/src/routes/[lang]/about/legal/accessibility/+page.svelte @@ -0,0 +1,78 @@ + + + + Accessibility Policy | Networking Toolbox + + + + + + +
+

Accessibility Policy

+

Making networking tools accessible to everyone.

+
+ +{#each sections as section (section.title)} +
+

{section.title}

+

{section.content}

+ + {#if section.list} +
    + {#each section.list as item, index (index)} +
  • {item}
  • + {/each} +
+ {/if} + + {#if section.link} +

+ {section.link.text} +

+ {/if} +
+{/each} diff --git a/src/routes/[lang]/about/legal/community/+page.svelte b/src/routes/[lang]/about/legal/community/+page.svelte new file mode 100644 index 00000000..1a2017dc --- /dev/null +++ b/src/routes/[lang]/about/legal/community/+page.svelte @@ -0,0 +1,51 @@ + + + + Community Guidelines | Networking Toolbox + + + + + + +
+

Community Guidelines

+

Building a welcoming and inclusive community.

+
+ +
+

Code of Conduct

+

+ Networking Toolbox follows the + Contributor Covenant Code of Conduct. +

+

+ We are committed to providing a welcoming and inclusive environment for everyone. This applies to all project spaces + including GitHub issues, pull requests, discussions, and any other community interactions. +

+
+ +
+

Expected Behavior

+
    +
  • Be respectful and considerate in your communication
  • +
  • Welcome newcomers and help them get started
  • +
  • Focus on constructive feedback and collaboration
  • +
  • Respect differing viewpoints and experiences
  • +
  • Accept responsibility and apologize when mistakes are made
  • +
+
+ +
+

Reporting Issues

+

+ If you experience or witness unacceptable behavior, please report it by opening an issue on + GitHub + or contacting the project maintainer directly. +

+

All reports will be handled with discretion and confidentiality.

+
diff --git a/src/routes/[lang]/about/legal/cookies/+page.svelte b/src/routes/[lang]/about/legal/cookies/+page.svelte new file mode 100644 index 00000000..0ee548a1 --- /dev/null +++ b/src/routes/[lang]/about/legal/cookies/+page.svelte @@ -0,0 +1,39 @@ + + + + Cookie Policy | Networking Toolbox + + + + + + +
+

Cookie Policy

+

We don't use cookies.

+
+ +
+

No Cookies

+

Networking Toolbox does not use cookies of any kind. We don't set, read, or store any cookies in your browser.

+
+ +
+

What We Use Instead

+

+ For storing your preferences (theme, layout, bookmarks), we use browser localStorage. Unlike cookies, localStorage + data: +

+
    +
  • Never gets sent to our servers
  • +
  • Stays entirely in your browser
  • +
  • Isn't subject to cookie consent laws
  • +
  • Can be cleared anytime in your browser settings
  • +
+

+ Learn more about what we store in our + Privacy Policy. +

+
diff --git a/src/routes/[lang]/about/legal/license/+page.svelte b/src/routes/[lang]/about/legal/license/+page.svelte new file mode 100644 index 00000000..9a641445 --- /dev/null +++ b/src/routes/[lang]/about/legal/license/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/about/legal/privacy/+page.svelte b/src/routes/[lang]/about/legal/privacy/+page.svelte new file mode 100644 index 00000000..4b3ba0ae --- /dev/null +++ b/src/routes/[lang]/about/legal/privacy/+page.svelte @@ -0,0 +1,129 @@ + + + + {$t('pages.legal.privacy.title')} + + + + + + +
+

{$t('pages.legal.privacy.hero.title')}

+

+ {$t('pages.legal.privacy.hero.lead')} +

+ +

{$t('pages.legal.privacy.principles.title')}

+
    + {#each $t('pages.legal.privacy.principles.items') as item, index (index)} +
  • {item}
  • + {/each} +
+
+ +
+

{$t('pages.legal.privacy.overview.title')}

+

+ {$t('pages.legal.privacy.overview.description')} +

+
+ +
+

{$t('pages.legal.privacy.dataProcessing.title')}

+ +

{$t('pages.legal.privacy.dataProcessing.clientSide.title')}

+

+ {$t('pages.legal.privacy.dataProcessing.clientSide.description')} +

+ +

{$t('pages.legal.privacy.dataProcessing.serverSide.title')}

+

+ {$t('pages.legal.privacy.dataProcessing.serverSide.description')} +

+
    + {#each $t('pages.legal.privacy.dataProcessing.serverSide.items') as item, index (index)} +
  • {item}
  • + {/each} +
+
+ +
+

{$t('pages.legal.privacy.localStorage.title')}

+

+ {$t('pages.legal.privacy.localStorage.description')} +

+
    + {#each $t('pages.legal.privacy.localStorage.items') as item, index (index)} +
  • {item}
  • + {/each} +
+

{$t('pages.legal.privacy.localStorage.note')}

+
+ +
+

{$t('pages.legal.privacy.analytics.title')}

+

+ {$t('pages.legal.privacy.analytics.description')} +

+
    + {#each $t('pages.legal.privacy.analytics.features') as feature, index (index)} +
  • {feature}
  • + {/each} +
+

{$t('pages.legal.privacy.analytics.note')}

+
+ +
+

{$t('pages.legal.privacy.thirdParty.title')}

+

{$t('pages.legal.privacy.thirdParty.description')}

+
    + {#each $t('pages.legal.privacy.thirdParty.services') as service, index (index)} +
  • {service}
  • + {/each} +
+

{$t('pages.legal.privacy.thirdParty.note')}

+
+ +
+

{$t('pages.legal.privacy.selfHosting.title')}

+

+ {$t('pages.legal.privacy.selfHosting.description')} +

+
+ +
+

{$t('pages.legal.privacy.openSource.title')}

+

{$t('pages.legal.privacy.openSource.description')}

+ +
+ +
+

{$t('pages.legal.privacy.changes.title')}

+

+ {$t('pages.legal.privacy.changes.description')} +

+

{$t('pages.legal.privacy.changes.lastUpdated')}

+
+ +
+

{$t('pages.legal.privacy.contact.title')}

+

+ {$t('pages.legal.privacy.contact.description')} +

+
diff --git a/src/routes/[lang]/about/legal/security/+page.svelte b/src/routes/[lang]/about/legal/security/+page.svelte new file mode 100644 index 00000000..0a623942 --- /dev/null +++ b/src/routes/[lang]/about/legal/security/+page.svelte @@ -0,0 +1,50 @@ + + + + Security Policy | Networking Toolbox + + + + + + +
+

Security Policy

+

How we handle security and vulnerability disclosures.

+
+ +
+

Reporting Vulnerabilities

+

+ If you discover a security vulnerability, please report it by emailing the maintainer directly on + security at as93 dot net. +

+
+ +
+

Security Practices

+
    +
  • All connections are fully encrypted (HTTPS/TLS)
  • +
  • Your data is encrypted locally or with keys only you control
  • +
  • We follow the principle of least privilege β€” only what’s needed has access
  • +
  • We use strong security headers and proven, modern encryption
  • +
  • We never trust or expose unvalidated data
  • +
  • Secrets are securely stored and never committed to code
  • +
  • All code is open source and regularly reviewed
  • +
  • Dependencies are audited and kept up to date
  • +
  • We log safely β€” no personal or sensitive data is ever recorded
  • +
  • We respond quickly to any reported security issues
  • +
+
+ +
+

Supported Versions

+

We support security updates for the latest release only. Self-hosted instances should update regularly.

+
+ +
+

Response Time

+

We aim to acknowledge security reports within 48 hours and provide updates on resolution progress.

+
diff --git a/src/routes/[lang]/bookmarks/+page.svelte b/src/routes/[lang]/bookmarks/+page.svelte new file mode 100644 index 00000000..ce1b0c71 --- /dev/null +++ b/src/routes/[lang]/bookmarks/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/cidr/+page.svelte b/src/routes/[lang]/cidr/+page.svelte new file mode 100644 index 00000000..152f681f --- /dev/null +++ b/src/routes/[lang]/cidr/+page.svelte @@ -0,0 +1,515 @@ + + +
+ + + + + +
+

Essential CIDR Concepts

+
+ {#each cidrContent.coreConcepts as concept, index (index)} +
+
+ +

{concept.title}

+
+

{concept.description}

+ {concept.example} +
+ {/each} +
+
+ + +
+

What is CIDR?

+
+
+ {#each cidrContent.aboutSection.content as paragraph, i (i)} +

+ {#if i === 0} + CIDR (Classless Inter-Domain Routing) + {paragraph.replace('CIDR (Classless Inter-Domain Routing) ', '')} + {:else} + {paragraph} + {/if} +

+ {/each} +
+
+

Why CIDR Matters

+ {#each cidrContent.aboutSection.advantages as advantage, index (index)} +
+ {advantage.title}: + {advantage.description} +
+ {/each} +
+
+
+ + +
+

Common CIDR Block Sizes

+
+ + + + + + + + + + + {#each cidrContent.commonSizes as size, index (index)} + + + + + + + {/each} + +
CIDRSubnet MaskHostsCommon Use
{size.cidr}{size.mask}{size.hosts}{size.use}
+
+

+ * /31 networks use special point-to-point addressing (RFC 3021) where both addresses are usable without + network/broadcast addresses. +

+
+ + +
+

How CIDR Works

+
+ {#each cidrContent.howItWorks as item, index (index)} +
+

{item.title}

+

{item.content}

+ + {#if item.example.type === 'bit'} +
+
{item.example.networkBits}
+
{item.example.hostBits}
+
+ {item.example.networkLabel} + {item.example.hostLabel} +
+
+ {:else if item.example.type === 'summary'} +
+ {#if item.example.before} +
+ {item.example.before.title}
+ {#each item.example.before.content as line, index (index)} + {line}
+ {/each} +
+ {/if} +
β†’
+ {#if item.example.after} +
+ {item.example.after.title}
+ {#each item.example.after.content as line, index (index)} + {line}
+ {/each} +
+ {/if} +
+ {:else if item.example.type === 'tip'} +
+ {item.example.title} + {item.example.content} +
+ {/if} +
+ {/each} +
+
+ + +
+

Quick Reference

+
+ {#each cidrContent.quickReference as card, index (index)} +
+

{card.title}

+
    + {#each card.items as item, index (index)} +
  • {item}
  • + {/each} +
+
+ {/each} +
+
+
+ + diff --git a/src/routes/[lang]/cidr/alignment/+page.svelte b/src/routes/[lang]/cidr/alignment/+page.svelte new file mode 100644 index 00000000..70fed57c --- /dev/null +++ b/src/routes/[lang]/cidr/alignment/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/cidr/allocator/+page.svelte b/src/routes/[lang]/cidr/allocator/+page.svelte new file mode 100644 index 00000000..67a9e13f --- /dev/null +++ b/src/routes/[lang]/cidr/allocator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/cidr/compare/+page.svelte b/src/routes/[lang]/cidr/compare/+page.svelte new file mode 100644 index 00000000..5530e344 --- /dev/null +++ b/src/routes/[lang]/cidr/compare/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/cidr/deaggregate/+page.svelte b/src/routes/[lang]/cidr/deaggregate/+page.svelte new file mode 100644 index 00000000..b7c503b3 --- /dev/null +++ b/src/routes/[lang]/cidr/deaggregate/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/cidr/gaps/+page.svelte b/src/routes/[lang]/cidr/gaps/+page.svelte new file mode 100644 index 00000000..3906c1db --- /dev/null +++ b/src/routes/[lang]/cidr/gaps/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/cidr/mask-converter/+layout.svelte b/src/routes/[lang]/cidr/mask-converter/+layout.svelte new file mode 100644 index 00000000..836eb41f --- /dev/null +++ b/src/routes/[lang]/cidr/mask-converter/+layout.svelte @@ -0,0 +1,524 @@ + + + +
+
+
+

{title}

+

{description}

+
+
+ + + +
+
+ + + + + +
+

Subnet Information

+
+ +
+ Network Bits + {$cidr} +
+
+ +
+ Host Bits + {32 - $cidr} +
+
+ +
+ Usable Hosts + + {get(subnetInfo).hosts.toLocaleString()} + +
+
+
+
+ + +
+

Common Subnets

+
+ {#each COMMON_SUBNETS as subnet, index (index)} + + + + {/each} +
+
+ + +
+

Understanding CIDR and Subnet Masks

+ +
+
+

CIDR Notation

+

+ CIDR (Classless Inter-Domain Routing) uses a slash followed by a number (e.g., /24) to indicate how many bits + are used for the network portion of an IP address. +

+
+ +
+

Subnet Mask

+

+ A 32-bit number that masks an IP address to divide it into network and host portions. Written in dotted + decimal notation (e.g., 255.255.255.0). +

+
+ +
+

Network Bits

+

+ The leftmost bits in an IP address that identify the network. More network bits mean smaller subnets with + fewer available host addresses. +

+
+ +
+

Host Bits

+

+ The remaining bits used for host addresses within the network. More host bits allow for more devices but fewer + possible subnets. +

+
+
+ +
+

Common Conversions

+
+
+ /24 + ↔ + 255.255.255.0 + 254 hosts +
+
+ /25 + ↔ + 255.255.255.128 + 126 hosts +
+
+ /26 + ↔ + 255.255.255.192 + 62 hosts +
+
+ /30 + ↔ + 255.255.255.252 + 2 hosts +
+
+
+ +
+

πŸ”§ Usage Tips

+
    +
  • Smaller CIDR = Bigger Network: /16 has more hosts than /24
  • +
  • Binary Thinking: Each bit doubles or halves the number of addresses
  • +
  • /30 for Links: Perfect for point-to-point connections (only 2 usable IPs)
  • +
  • Planning: Start with larger subnets and subdivide as needed
  • +
+
+
+
+ + diff --git a/src/routes/[lang]/cidr/mask-converter/+page.svelte b/src/routes/[lang]/cidr/mask-converter/+page.svelte new file mode 100644 index 00000000..e69de29b diff --git a/src/routes/[lang]/cidr/mask-converter/cidr-to-subnet-mask/+page.svelte b/src/routes/[lang]/cidr/mask-converter/cidr-to-subnet-mask/+page.svelte new file mode 100644 index 00000000..118e85fb --- /dev/null +++ b/src/routes/[lang]/cidr/mask-converter/cidr-to-subnet-mask/+page.svelte @@ -0,0 +1,140 @@ + + + +
+ +
+ + +
+ cidr.set(Number((e.target as HTMLInputElement).value))} + class="cidr-slider" + /> +
+ 08162432 +
+
+
+ + +
+
+ Subnet Mask + {$mask} +
+
+
+ + diff --git a/src/routes/[lang]/cidr/mask-converter/subnet-mask-to-cidr/+page.svelte b/src/routes/[lang]/cidr/mask-converter/subnet-mask-to-cidr/+page.svelte new file mode 100644 index 00000000..eb8fa30d --- /dev/null +++ b/src/routes/[lang]/cidr/mask-converter/subnet-mask-to-cidr/+page.svelte @@ -0,0 +1,87 @@ + + + +
+ +
+ + handleMaskChange((e.target as HTMLInputElement).value)} + /> +
+ + +
+
+ CIDR Notation + /{$cidr} +
+
+
+ + diff --git a/src/routes/[lang]/cidr/next-available/+page.svelte b/src/routes/[lang]/cidr/next-available/+page.svelte new file mode 100644 index 00000000..d694a4a2 --- /dev/null +++ b/src/routes/[lang]/cidr/next-available/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/cidr/range-to-cidr/+page.svelte b/src/routes/[lang]/cidr/range-to-cidr/+page.svelte new file mode 100644 index 00000000..1483daaf --- /dev/null +++ b/src/routes/[lang]/cidr/range-to-cidr/+page.svelte @@ -0,0 +1,733 @@ + + + + +
+
+ + +

Quick Examples

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+
+
+
+ + +
+ +
+ +
+ +
+ + +
+ + +
+
+
+
+ + {#if result} +
+ {#if result.error} +
+ +
+

Error

+

{result.error}

+
+
+ {:else if result.isValid} +
+

Conversion Result

+
+
+ IP Version + IPv{result.ipVersion} +
+
+ Start IP + {result.startIP} +
+
+ End IP + {result.endIP} +
+
+ Total Addresses + {result.totalAddresses.toLocaleString()} +
+
+ CIDR Blocks + {result.totalBlocks} +
+
+
+ +
+
+

CIDR Blocks ({result.totalBlocks})

+
+ + + + +
+
+ +
+ {#each result.cidrs as cidr, index (cidr.cidr)} +
+
+ {index + 1} + {cidr.cidr} + +
+
+
+ Network: + {cidr.network} +
+
+ Prefix: + /{cidr.prefix} +
+
+ Range: + {cidr.firstIP} - {cidr.lastIP} +
+
+ Addresses: + {BigInt(cidr.totalIPs).toLocaleString()} +
+
+
+ {/each} +
+
+ {/if} +
+ {/if} + + +
+

About Range to CIDR

+
+

This tool converts arbitrary IP ranges into CIDR notation blocks:

+
    +
  • Minimal set: Finds the smallest number of CIDR blocks
  • +
  • Aligned blocks: CIDRs respect network boundaries
  • +
  • IPv4 & IPv6: Supports both address families
  • +
  • Exact coverage: All addresses in range are included
  • +
+

Useful for converting firewall rules, ACLs, or vendor-provided IP ranges to CIDR format.

+
+
+
+ + diff --git a/src/routes/[lang]/cidr/set-operations/+layout.svelte b/src/routes/[lang]/cidr/set-operations/+layout.svelte new file mode 100644 index 00000000..35aaae72 --- /dev/null +++ b/src/routes/[lang]/cidr/set-operations/+layout.svelte @@ -0,0 +1,270 @@ + + + + +
+

{setOperationsContent.title}

+

{setOperationsContent.description}

+ +
+ {#each setOperationsContent.operations as operation, index (index)} +
+
+
{operation.symbol}
+
{operation.name}
+
+
{operation.description}
+
+ Example: {operation.example} +
+
+ {/each} +
+ +
+

Common Network Patterns

+
+ {#each setOperationsContent.patterns as pattern, index (index)} +
+
{pattern.title}
+
    + {#each pattern.items as item, index (index)} +
  • {item.term}: {item.description}
  • + {/each} +
+
+ {/each} +
+
+ +
+

Implementation Notes

+
+ {#each setOperationsContent.notes as note, index (index)} +
+
{note.title}
+

{note.content}

+
+ {/each} +
+
+ +
+

Best Practices

+
    + {#each setOperationsContent.bestPractices as practice, index (index)} +
  • {practice.term}: {practice.description}
  • + {/each} +
+
+
+ + diff --git a/src/routes/[lang]/cidr/set-operations/+page.svelte b/src/routes/[lang]/cidr/set-operations/+page.svelte new file mode 100644 index 00000000..61b70619 --- /dev/null +++ b/src/routes/[lang]/cidr/set-operations/+page.svelte @@ -0,0 +1,54 @@ + + + + {#if selectedTool === 'diff'} + + {:else if selectedTool === 'overlap'} + + {:else if selectedTool === 'contains'} + + {/if} + diff --git a/src/routes/[lang]/cidr/set-operations/contains/+page.svelte b/src/routes/[lang]/cidr/set-operations/contains/+page.svelte new file mode 100644 index 00000000..eccedf50 --- /dev/null +++ b/src/routes/[lang]/cidr/set-operations/contains/+page.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/routes/[lang]/cidr/set-operations/diff/+page.svelte b/src/routes/[lang]/cidr/set-operations/diff/+page.svelte new file mode 100644 index 00000000..a9b2aa67 --- /dev/null +++ b/src/routes/[lang]/cidr/set-operations/diff/+page.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/routes/[lang]/cidr/set-operations/overlap/+page.svelte b/src/routes/[lang]/cidr/set-operations/overlap/+page.svelte new file mode 100644 index 00000000..e689fb33 --- /dev/null +++ b/src/routes/[lang]/cidr/set-operations/overlap/+page.svelte @@ -0,0 +1,52 @@ + + + + + diff --git a/src/routes/[lang]/cidr/split/+page.svelte b/src/routes/[lang]/cidr/split/+page.svelte new file mode 100644 index 00000000..ebb82fe0 --- /dev/null +++ b/src/routes/[lang]/cidr/split/+page.svelte @@ -0,0 +1,299 @@ + + +
+ + +
+

About CIDR Subnet Splitting

+

+ CIDR subnet splitting divides a larger network (parent) into smaller networks (children) of equal size. This is + essential for efficient IP address allocation and network design. +

+ +
+
+

Split by Count

+

+ Specify how many equal subnets you need. The tool calculates the required prefix length and creates exactly + that many subnets (rounded up to the nearest power of 2). +

+
+
+ 192.168.1.0/24 β†’ 4 subnets +
+
β†’
+
+ 192.168.1.0/26
+ 192.168.1.64/26
+ 192.168.1.128/26
+ 192.168.1.192/26 +
+
+
+ +
+

Split by Prefix

+

+ Specify the target prefix length for child subnets. The tool creates all possible subnets at that prefix + length within the parent network. +

+
+
+ 10.0.0.0/16 β†’ /20 subnets +
+
β†’
+
+ 10.0.0.0/20 (16 subnets)
+ 10.0.16.0/20
+ 10.0.32.0/20
+ ... and 13 more +
+
+
+
+ +
+

Key Concepts

+
+
+
Power of 2 Rule
+

Network splitting always results in a power-of-2 number of subnets due to binary addressing.

+
+
+
Prefix Length
+

Child subnets always have a longer prefix (smaller network) than the parent.

+
+
+
Address Space
+

All child subnets combined exactly equal the parent's address space.

+
+
+
Binary Boundaries
+

Subnet boundaries align with binary bit boundaries for efficient routing.

+
+
+
+ +
+

Common Use Cases

+
    +
  • Office Networks: Split a /24 into department subnets
  • +
  • Cloud VPCs: Create isolated subnets for different tiers
  • +
  • VLSM Design: Plan hierarchical network addressing
  • +
  • Data Centers: Segment networks for different services
  • +
+
+ +
+

Technical Notes

+
+
+
IPv4 vs IPv6
+

+ IPv4 uses 32-bit addresses with /0-/32 prefixes. IPv6 uses 128-bit addresses with /0-/128 prefixes. The + splitting logic is identical for both. +

+
+
+
Network vs Host Addresses
+

+ In IPv4, the first address is the network address and the last is the broadcast address. IPv6 doesn't have + broadcast, so the first and last addresses are both usable. +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/cidr/summarize/+page.svelte b/src/routes/[lang]/cidr/summarize/+page.svelte new file mode 100644 index 00000000..42ed834c --- /dev/null +++ b/src/routes/[lang]/cidr/summarize/+page.svelte @@ -0,0 +1,305 @@ + + +
+ + +
+

{$t('pages/cidr-summarize.cidrSummarize.about.title')}

+

+ {$t('pages/cidr-summarize.cidrSummarize.about.description')} +

+ +
+
+

{$t('pages/cidr-summarize.cidrSummarize.benefits.routeTable.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.routeTable.description')}

+
+
+

{$t('pages/cidr-summarize.cidrSummarize.benefits.networkEfficiency.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.networkEfficiency.description')}

+
+
+

{$t('pages/cidr-summarize.cidrSummarize.benefits.dualProtocol.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.dualProtocol.description')}

+
+
+

{$t('pages/cidr-summarize.cidrSummarize.benefits.flexibleInput.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.flexibleInput.description')}

+
+
+ +
+

{$t('pages/cidr-summarize.cidrSummarize.modes.title')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.modes.exactMerge.title')}
+

+ {$t('pages/cidr-summarize.cidrSummarize.modes.exactMerge.description')} +

+
+ {$t('pages/cidr-summarize.cidrSummarize.modes.example.label')} + 192.168.1.0/24 + 192.168.2.0/24 + β†’ + 192.168.1.0/24, 192.168.2.0/24 +
+
+
+
{$t('pages/cidr-summarize.cidrSummarize.modes.minimalCover.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.modes.minimalCover.description')}

+
+ {$t('pages/cidr-summarize.cidrSummarize.modes.example.label')} + 192.168.1.0/24 + 192.168.2.0/24 + β†’ + 192.168.0.0/23 +
+
+
+
+ +
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.title')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.useCases.bgp.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.bgp.description')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.useCases.firewall.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.firewall.description')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.useCases.planning.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.planning.description')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.useCases.migration.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.migration.description')}

+
+
+
+ +
+

{$t('pages/cidr-summarize.cidrSummarize.inputFormats.title')}

+
+
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.singleIP.title')}
+
+ 192.168.1.100 + 2001:db8::1 +
+
+
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.cidrBlocks.title')}
+
+ 10.0.0.0/8 + 2001:db8::/32 +
+
+
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.ipRanges.title')}
+
+ 172.16.1.1-172.16.1.100 + 2001:db8::1-2001:db8::ffff +
+
+
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.title')}
+
+ {$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.example1')} + {$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.example2')} +
+
+
+
+ +
+

+ + {$t('pages/cidr-summarize.cidrSummarize.optimizationTips.title')} +

+

+ {$t('pages/cidr-summarize.cidrSummarize.optimizationTips.description')} +

+
+
+
+ + diff --git a/src/routes/[lang]/cidr/wildcard-mask/+page.svelte b/src/routes/[lang]/cidr/wildcard-mask/+page.svelte new file mode 100644 index 00000000..0d59dfae --- /dev/null +++ b/src/routes/[lang]/cidr/wildcard-mask/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/dhcp/+page.svelte b/src/routes/[lang]/dhcp/+page.svelte new file mode 100644 index 00000000..f1921126 --- /dev/null +++ b/src/routes/[lang]/dhcp/+page.svelte @@ -0,0 +1,41 @@ + + +
+ + + +
+ + diff --git a/src/routes/[lang]/dhcp/calculators/lease-time/+page.svelte b/src/routes/[lang]/dhcp/calculators/lease-time/+page.svelte new file mode 100644 index 00000000..ecf15255 --- /dev/null +++ b/src/routes/[lang]/dhcp/calculators/lease-time/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/kea-isc-snippets/+page.svelte b/src/routes/[lang]/dhcp/kea-isc-snippets/+page.svelte new file mode 100644 index 00000000..2a1d01ae --- /dev/null +++ b/src/routes/[lang]/dhcp/kea-isc-snippets/+page.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/routes/[lang]/dhcp/tools/fingerprinting/+page.svelte b/src/routes/[lang]/dhcp/tools/fingerprinting/+page.svelte new file mode 100644 index 00000000..f52fdc91 --- /dev/null +++ b/src/routes/[lang]/dhcp/tools/fingerprinting/+page.svelte @@ -0,0 +1,17 @@ + + + + DHCP Fingerprinting Database | IP Calc + + + + + diff --git a/src/routes/[lang]/dhcp/v4/options/freeform-tlv/+page.svelte b/src/routes/[lang]/dhcp/v4/options/freeform-tlv/+page.svelte new file mode 100644 index 00000000..15679c9e --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/freeform-tlv/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option119-domain-search/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option119-domain-search/+page.svelte new file mode 100644 index 00000000..09b3f7b3 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option119-domain-search/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option121-classless-routes/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option121-classless-routes/+page.svelte new file mode 100644 index 00000000..ec57edd4 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option121-classless-routes/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option150-tftp-server/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option150-tftp-server/+page.svelte new file mode 100644 index 00000000..aa23d713 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option150-tftp-server/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option3-default-gateway/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option3-default-gateway/+page.svelte new file mode 100644 index 00000000..ecd9e7c3 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option3-default-gateway/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option43-vendor-specific/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option43-vendor-specific/+page.svelte new file mode 100644 index 00000000..cffce1c9 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option43-vendor-specific/+page.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option51-lease-time/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option51-lease-time/+page.svelte new file mode 100644 index 00000000..e0c88728 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option51-lease-time/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option60-vendor-class/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option60-vendor-class/+page.svelte new file mode 100644 index 00000000..a27226c3 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option60-vendor-class/+page.svelte @@ -0,0 +1,11 @@ + + + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option61-clientid/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option61-clientid/+page.svelte new file mode 100644 index 00000000..766aa1f5 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option61-clientid/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/option82-relay-agent/+page.svelte b/src/routes/[lang]/dhcp/v4/options/option82-relay-agent/+page.svelte new file mode 100644 index 00000000..73e1fa39 --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/option82-relay-agent/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/options6-15-dns/+page.svelte b/src/routes/[lang]/dhcp/v4/options/options6-15-dns/+page.svelte new file mode 100644 index 00000000..0bc0070e --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/options6-15-dns/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v4/options/pxe-boot-profile/+page.svelte b/src/routes/[lang]/dhcp/v4/options/pxe-boot-profile/+page.svelte new file mode 100644 index 00000000..84f491ec --- /dev/null +++ b/src/routes/[lang]/dhcp/v4/options/pxe-boot-profile/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v6/identity/duid-generator/+page.svelte b/src/routes/[lang]/dhcp/v6/identity/duid-generator/+page.svelte new file mode 100644 index 00000000..551daace --- /dev/null +++ b/src/routes/[lang]/dhcp/v6/identity/duid-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v6/identity/iaid-calculator/+page.svelte b/src/routes/[lang]/dhcp/v6/identity/iaid-calculator/+page.svelte new file mode 100644 index 00000000..d39715a6 --- /dev/null +++ b/src/routes/[lang]/dhcp/v6/identity/iaid-calculator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v6/options/option23-24-dns/+page.svelte b/src/routes/[lang]/dhcp/v6/options/option23-24-dns/+page.svelte new file mode 100644 index 00000000..2b7640eb --- /dev/null +++ b/src/routes/[lang]/dhcp/v6/options/option23-24-dns/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dhcp/v6/options/option25-prefix-delegation/+page.svelte b/src/routes/[lang]/dhcp/v6/options/option25-prefix-delegation/+page.svelte new file mode 100644 index 00000000..883c7431 --- /dev/null +++ b/src/routes/[lang]/dhcp/v6/options/option25-prefix-delegation/+page.svelte @@ -0,0 +1,17 @@ + + + + DHCPv6 Prefix Delegation (IA_PD) - Options 25/26 | IP Calc + + + + + diff --git a/src/routes/[lang]/dhcp/v6/options/option39-fqdn/+page.svelte b/src/routes/[lang]/dhcp/v6/options/option39-fqdn/+page.svelte new file mode 100644 index 00000000..ea75964f --- /dev/null +++ b/src/routes/[lang]/dhcp/v6/options/option39-fqdn/+page.svelte @@ -0,0 +1,17 @@ + + + + DHCPv6 Client FQDN Option (Option 39) - RFC 4704 | IP Calc + + + + + diff --git a/src/routes/[lang]/diagnostics/+layout.svelte b/src/routes/[lang]/diagnostics/+layout.svelte new file mode 100644 index 00000000..9161c0c8 --- /dev/null +++ b/src/routes/[lang]/diagnostics/+layout.svelte @@ -0,0 +1,125 @@ + + +{#if showWarning} + +{/if} + + + + diff --git a/src/routes/[lang]/diagnostics/+page.svelte b/src/routes/[lang]/diagnostics/+page.svelte new file mode 100644 index 00000000..2ed8ea55 --- /dev/null +++ b/src/routes/[lang]/diagnostics/+page.svelte @@ -0,0 +1,36 @@ + + +
+ + + +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/+page.svelte b/src/routes/[lang]/diagnostics/dns/+page.svelte new file mode 100644 index 00000000..f3068f49 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/+page.svelte @@ -0,0 +1,39 @@ + + +
+ + + +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/axfr-tester/+page.svelte b/src/routes/[lang]/diagnostics/dns/axfr-tester/+page.svelte new file mode 100644 index 00000000..30057ab1 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/axfr-tester/+page.svelte @@ -0,0 +1,1149 @@ + + +
+
+

DNS Zone Transfer (AXFR) Security Tester

+

Test if zone transfers are improperly exposed - a critical DNS security vulnerability

+
+ + +
+
+ + +

Quick Examples

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
{ + e.preventDefault(); + testAXFR(); + }} + > +
+ + { + selectedExampleIndex = null; + }} + aria-invalid={!isInputValid} + /> +
+ + +
+ + + {#if error} + + {/if} + + + {#if results} + + {#if results.limitedMode} + + {/if} + + + {#if results.summary.vulnerable > 0} + + {:else if results.summary.secure > 0} +
+ +
+ All Nameservers Secure +

Zone transfers are properly restricted. No AXFR vulnerabilities detected.

+
+
+ {/if} + + +
+
+ +
+ Total Nameservers + {results.summary.total} +
+
+ +
+ +
+ Vulnerable + {results.summary.vulnerable} +
+
+ +
+ +
+ Secure + {results.summary.secure} +
+
+ +
+ +
+ Errors + {results.summary.errors} +
+
+
+ + +
+

Nameserver Test Results

+ +
+ {#each results.nameservers as ns (ns.nameserver)} +
+
+
+ +
+

{ns.nameserver}

+ {ns.ip} +
+
+
+ {ns.responseTime}ms + + {getStatusText(ns)} + +
+
+ + {#if ns.vulnerable && ns.recordCount} +
+
+ + Zone transfer succeeded! Exposed {ns.recordCount} DNS records +
+ + {#if ns.records && ns.records.length > 0} + + + {#if expandedRecords.has(ns.nameserver)} +
+
{ns.records.join('\n')}
+ {#if ns.recordCount > ns.records.length} +

+ Showing first {ns.records.length} of {ns.recordCount} total records +

+ {/if} +
+ {/if} + {/if} +
+ {:else if ns.error} +
+ + {ns.error} +
+ {:else} +
+ + Zone transfer properly refused +
+ {/if} +
+ {/each} +
+
+ + + + {/if} +
+ + +
+
+

{axfrContent.sections.whatIsAXFR.title}

+

{axfrContent.sections.whatIsAXFR.content}

+
+ +
+

{axfrContent.sections.security.title}

+
+ {#each axfrContent.sections.security.risks as risk (risk.risk)} +
+
+

{risk.risk}

+ {risk.severity} +
+

{risk.description}

+
+ + {risk.impact} +
+
+ {/each} +
+
+ +
+

{axfrContent.sections.interpretation.title}

+
+ {#each axfrContent.sections.interpretation.statuses as status (status.status)} +
+
+ +

{status.status}: {status.meaning}

+
+

{status.description}

+
+ + Action: + {status.action} +
+
+ {/each} +
+
+ +
+

{axfrContent.sections.properConfiguration.title}

+ {#each axfrContent.sections.properConfiguration.configurations as config (config.server)} +
+

{config.server}

+

{config.description}

+
{config.syntax}
+
+ {/each} +
+ +
+

{axfrContent.sections.remediation.title}

+
+ {#each axfrContent.sections.remediation.steps as step, i (step.step)} +
+
{i + 1}
+
+

{step.step}

+

{step.details}

+ {#if step.command} +
{step.command}
+ {/if} +
+
+ {/each} +
+
+ +
+

{axfrContent.sections.bestPractices.title}

+
+ {#each axfrContent.sections.bestPractices.practices as practice (practice.practice)} +
+
+ +

{practice.practice}

+ {practice.priority} +
+

{practice.description}

+
+ {/each} +
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/blacklist-checker/+page.svelte b/src/routes/[lang]/diagnostics/dns/blacklist-checker/+page.svelte new file mode 100644 index 00000000..26a1a57a --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/blacklist-checker/+page.svelte @@ -0,0 +1,1171 @@ + + + + DNS Blacklist Checker | IP Calc + + + + +
+
+

DNS Blacklist Checker

+

Check if your IP or domain is listed on major spam blacklists (RBLs)

+
+ + +
+
+ + +

Quick Examples

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
{ + e.preventDefault(); + checkBlacklist(); + }} + > +
+ + +
+ +
+
+ + + {#if error} +
+ +
+

Error

+

{error}

+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

Checking Blacklists

+

Querying DNS blacklist records for {target}...

+
+
+
+
+ {/if} + + + {#if results} +
+
+

Blacklist Check Results

+
+ + +
0} + > +
+ +
+

{results.summary.listedCount === 0 ? 'Clean' : 'Listed on Blacklists'}

+

Target: {results.target} ({results.targetType})

+ {#if results.resolvedIPs && results.resolvedIPs.length > 0} +

Resolved IPs: {results.resolvedIPs.join(', ')}

+ {/if} +
+
+ +
+
+ Total Checked + {results.summary.totalChecked} +
+
+ Clean + {results.summary.cleanCount} +
+
+ Listed + {results.summary.listedCount} +
+
+ Errors + {results.summary.errorCount} +
+
+
+ + + {#if results.results.filter((r: any) => r.listed).length > 0} +
+
+

+ + Listed on Blacklists ({results.results.filter((r: any) => r.listed).length}) +

+
+
+ {#each results.results.filter((r: any) => r.listed) as result (result.rbl)} + + {/each} +
+
+ {/if} + + + {#if results.results.filter((r: any) => !r.listed && !r.error).length > 0} +
+
+ + + + +

+ + Clean Results ({results.results.filter((r: any) => !r.listed && !r.error).length}) +

+
+
+ {#each results.results.filter((r: any) => !r.listed && !r.error) as result (result.rbl)} +
+
+ {result.rbl} + {result.responseTime}ms + + + Clean + +
+
+ {/each} +
+
+
+ {/if} + + + {#if results.results.filter((r: any) => r.error).length > 0} +
+
+ + + + +

+ + Query Warnings ({results.results.filter((r: any) => r.error).length}) +

+
+
+ {#each results.results.filter((r: any) => r.error) as result (result.rbl)} +
+
+ {result.rbl} + + + Warning + +
+
+ Note: + {result.error} +
+
+ {/each} +
+
+

+ + These RBLs could not be queried, possibly due to rate limiting, public resolver restrictions, or API access + requirements. This does not indicate a listing. +

+
+
+
+ {/if} + + + {#if results} +
+
+ + + + +

{dnsblContent.sections.queryWarnings.title}

+
+
+
+

{dnsblContent.sections.queryWarnings.content}

+
+ {#each dnsblContent.sections.queryWarnings.warnings as warning (warning.type)} +
+ {warning.type} +

{warning.meaning}

+

{warning.action}

+
+ {/each} +
+
+
+
+
+ {/if} +
+ {/if} +
+ + +
+
+
+

{dnsblContent.sections.whatAreBlacklists.title}

+
+
+

{dnsblContent.sections.whatAreBlacklists.content}

+
+
+ +
+
+

{dnsblContent.sections.consequences.title}

+
+
+

{dnsblContent.sections.consequences.content}

+
+ {#each dnsblContent.sections.consequences.impacts.slice(0, 3) as impact (impact.impact)} +
+ {impact.severity} + {impact.impact} + {impact.description} +
+ {/each} +
+
+
+ +
+
+

{dnsblContent.sections.howToFix.title}

+
+
+

{dnsblContent.sections.howToFix.content}

+
+ {#each dnsblContent.sections.howToFix.steps as stepGroup (stepGroup.step)} +
+

{stepGroup.step}

+
    + {#each stepGroup.actions.slice(0, 3) as action, idx (`${stepGroup.step}-${idx}`)} +
  • {action}
  • + {/each} +
+
+ {/each} +
+
+
+ +
+
+

{dnsblContent.sections.majorBlacklists.title}

+
+
+
+ {#each dnsblContent.sections.majorBlacklists.lists as list (list.name)} +
+
+ {list.name} + {list.type} +
+

{list.description}

+
+
+ Usage: + {list.usage} +
+
+ Auto-removal: + {list.autoRemoval} +
+ {#if list.url} + + {/if} +
+
+ {/each} +
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/caa-effective/+page.svelte b/src/routes/[lang]/diagnostics/dns/caa-effective/+page.svelte new file mode 100644 index 00000000..a247ac91 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/caa-effective/+page.svelte @@ -0,0 +1,850 @@ + + +
+
+

{t('diagnostics/dns-caa-effective.title')}

+

+ {t('diagnostics/dns-caa-effective.subtitle')} +

+
+ + +
+
+ + +

{t('diagnostics/dns-caa-effective.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{t('diagnostics/dns-caa-effective.ui.checkTitle')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if results} +
+
+

{t('diagnostics/dns-caa-effective.results.title')}

+ +
+
+ +
+

{t('diagnostics/dns-caa-effective.results.effectivePolicy')}

+ {#if results.effective} +
+
+ +
+
+ {t('diagnostics/dns-caa-effective.results.policyFoundAt')}: + {results.effective.domain} +
+

{t('diagnostics/dns-caa-effective.results.policyDescription')}

+
+
+ +
+ {#each results.effective.records as record, index (index)} + {@const parsed = parseCAA(record)} + {#if parsed} +
+
+ +
+ {parsed.tag} + {getTagDescription(parsed.tag)} +
+ + {t('diagnostics/dns-caa-effective.results.flag')}: {parsed.flag} + +
+
+ {parsed.value} +
+
+ {:else} +
+
+ {record} +
+
+ {/if} + {/each} +
+
+ {:else} +
+ +
+
{t('diagnostics/dns-caa-effective.results.noPolicyFound')}
+

{t('diagnostics/dns-caa-effective.results.noRecordsFound', { domain: domainName })}

+

{t('diagnostics/dns-caa-effective.results.implication')}

+
+
+ {/if} +
+ + + {#if results.chain?.length === 1 && results.effective} +
+
+ +
+
{t('diagnostics/dns-caa-effective.results.topLevelPolicy')}
+

+ {t('diagnostics/dns-caa-effective.results.topLevelDescription', { domain: results.effective.domain })} +

+
+
+
+ {/if} + + + {#if results.chain?.length > 1} +
+

{t('diagnostics/dns-caa-effective.results.chainTitle')}

+

+ {t('diagnostics/dns-caa-effective.results.chainDescription')} +

+ +
+ {#each results.chain as item, index (index)} + {@const isEffective = item.domain === results.effective?.domain} +
+
+ {#if index > 0} +
+ + {/if} +
+ +
+
+
+ {item.domain} + + {t('diagnostics/dns-caa-effective.results.levelUp', { + count: getDomainDepth(item.domain, results.chain[results.chain.length - 1].domain), + })} + +
+ {#if isEffective} + + + {t('diagnostics/dns-caa-effective.results.effective')} + + {:else} + + + {t('diagnostics/dns-caa-effective.results.noCAA')} + + {/if} +
+ + {#if item.records?.length > 0} +
+ {#each item.records as record, index (index)} +
+ {record} +
+ {/each} +
+ {/if} +
+
+ {/each} +
+
+ {/if} +
+
+ {/if} + + {#if error} +
+
+
+ +
+ {t('diagnostics/dns-caa-effective.error.title')} +

{error}

+
+
+
+
+ {/if} + + +
+
+

{t('diagnostics/dns-caa-effective.education.title')}

+
+
+
+
+

{t('diagnostics/dns-caa-effective.education.format.title')}

+
+ flag tag "value" +
+
    +
  • + {t('diagnostics/dns-caa-effective.education.format.flag.label')}: + {t('diagnostics/dns-caa-effective.education.format.flag.description')} +
  • +
  • + {t('diagnostics/dns-caa-effective.education.format.tag.label')}: + {t('diagnostics/dns-caa-effective.education.format.tag.description')} +
  • +
  • + {t('diagnostics/dns-caa-effective.education.format.value.label')}: + {t('diagnostics/dns-caa-effective.education.format.value.description')} +
  • +
+
+ +
+

{t('diagnostics/dns-caa-effective.education.tags.title')}

+
+
+ issue: + {t('diagnostics/dns-caa-effective.education.tags.issue')} +
+
+ issuewild: + {t('diagnostics/dns-caa-effective.education.tags.issuewild')} +
+
+ iodef: + {t('diagnostics/dns-caa-effective.education.tags.iodef')} +
+
+
+ +
+

{t('diagnostics/dns-caa-effective.education.process.title')}

+
    +
  1. {t('diagnostics/dns-caa-effective.education.process.step1')}
  2. +
  3. {t('diagnostics/dns-caa-effective.education.process.step2')}
  4. +
  5. {t('diagnostics/dns-caa-effective.education.process.step3')}
  6. +
  7. {t('diagnostics/dns-caa-effective.education.process.step4')}
  8. +
+
+ +
+

{t('diagnostics/dns-caa-effective.education.examples.title')}

+
+
+ 0 issue "letsencrypt.org" + {t('diagnostics/dns-caa-effective.education.examples.letsencrypt')} +
+
+ 0 issuewild ";" + {t('diagnostics/dns-caa-effective.education.examples.prohibit')} +
+
+ 0 iodef "mailto:security@example.com" + {t('diagnostics/dns-caa-effective.education.examples.report')} +
+
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/dmarc-check/+page.svelte b/src/routes/[lang]/diagnostics/dns/dmarc-check/+page.svelte new file mode 100644 index 00000000..4c988c9e --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/dmarc-check/+page.svelte @@ -0,0 +1,851 @@ + + +
+
+

{$t('diagnostics/dns-dmarc-check.title')}

+

{$t('diagnostics/dns-dmarc-check.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/dns-dmarc-check.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/dns-dmarc-check.form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if results && results.hasRecord} +
+
+

{$t('diagnostics/dns-dmarc-check.results.title')}

+ +
+
+ + {#if results.parsed} + {@const issues = getIssues()} + {@const parsed = results.parsed} +
+
+ i.severity === 'high') + ? 'shield-x' + : 'shield-alert'} + size="md" + /> +
+

+ {#if issues.length === 0} + {$t('diagnostics/dns-dmarc-check.results.status.secure')} + {:else if issues.some((i) => i.severity === 'high')} + {$t('diagnostics/dns-dmarc-check.results.status.issuesFound')} + {:else} + {$t('diagnostics/dns-dmarc-check.results.status.needsImprovement')} + {/if} +

+

+ {#if issues.length === 0} + {$t('diagnostics/dns-dmarc-check.results.status.noCriticalIssues')} + {:else} + {$t('diagnostics/dns-dmarc-check.results.status.issuesIdentified', { + count: issues.length, + plural: issues.length > 1 ? 's' : '', + })} + {/if} +

+
+
+
+ + + {#if results.record} +
+

{$t('diagnostics/dns-dmarc-check.results.recordSection.title')}

+
+
+ {$t('diagnostics/dns-dmarc-check.results.recordSection.location', { domain })} +
+ {results.record} +
+
+ {/if} + + +
+

{$t('diagnostics/dns-dmarc-check.results.policyConfiguration.title')}

+
+ +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.mainPolicy')} +
+
+ {parsed.policy} + + {#if parsed.policy === 'reject'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.policies.reject')} + {:else if parsed.policy === 'quarantine'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.policies.quarantine')} + {:else if parsed.policy === 'none'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.policies.none')} + {:else} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.policies.unknown')} + {/if} + +
+
+ + + {#if parsed.subdomainPolicy} +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.subdomainPolicy')} +
+
+ {parsed.subdomainPolicy} +
+
+ {/if} + + +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.coverage')} +
+
+ {parsed.percentage}% + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.coverageDescription')} +
+
+ + +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.dkimAlignment')} +
+
+ + {parsed.alignment.dkim === 's' + ? $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.strict') + : $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.relaxed')} + + + {parsed.alignment.dkim === 's' + ? $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.strictDescription') + : $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.relaxedDescription')} + +
+
+ + +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.spfAlignment')} +
+
+ + {parsed.alignment.spf === 's' + ? $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.strict') + : $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.relaxed')} + + + {parsed.alignment.spf === 's' + ? $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.strictDescription') + : $t('diagnostics/dns-dmarc-check.results.policyConfiguration.alignment.relaxedDescription')} + +
+
+ + +
+
+ + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptions')} +
+
+ {parsed.reporting.failureOptions} + + {#if parsed.reporting.failureOptions === '0'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptionsDescriptions.both')} + {:else if parsed.reporting.failureOptions === '1'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptionsDescriptions.any')} + {:else if parsed.reporting.failureOptions === 'd'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptionsDescriptions.dkim')} + {:else if parsed.reporting.failureOptions === 's'} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptionsDescriptions.spf')} + {:else} + {$t('diagnostics/dns-dmarc-check.results.policyConfiguration.failureOptionsDescriptions.custom')} + {/if} + +
+
+
+
+ + +
+

{$t('diagnostics/dns-dmarc-check.results.reporting.title')}

+
+
+
+ + {$t('diagnostics/dns-dmarc-check.results.reporting.aggregateReports')} +
+
+ {#if parsed.reporting.aggregate} + + {:else} + {$t('diagnostics/dns-dmarc-check.results.reporting.notConfigured')} + {/if} +
+
+ +
+
+ + {$t('diagnostics/dns-dmarc-check.results.reporting.forensicReports')} +
+
+ {#if parsed.reporting.forensic} + + {:else} + {$t('diagnostics/dns-dmarc-check.results.reporting.notConfigured')} + {/if} +
+
+
+
+ + + {#if issues.length > 0} +
+

{$t('diagnostics/dns-dmarc-check.results.issues.title')}

+
+ {#each issues as issue, index (index)} +
+ +
+ {issue.severity.toUpperCase()} + {issue.message} +
+
+ {/each} +
+
+ {/if} + {/if} +
+
+ {/if} + + + {#if results && results.hasRecord === false} +
+
+
+ +
+ {$t('diagnostics/dns-dmarc-check.noRecord.title')} +

+ {$t('diagnostics/dns-dmarc-check.noRecord.message', { domain, dmarcDomain: results.domain })} +

+

+ {$t('diagnostics/dns-dmarc-check.noRecord.helpText')} +

+
+
+
+
+ {/if} + + {#if error || results?.error} +
+
+
+ +
+ {$t('diagnostics/dns-dmarc-check.error.title')} +

{error || results.error}

+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/dns-dmarc-check.educational.title')}

+
+
+
+
+

{$t('diagnostics/dns-dmarc-check.educational.policies.title')}

+
+
+ none: + {$t('diagnostics/dns-dmarc-check.educational.policies.none')} +
+
+ quarantine: + {$t('diagnostics/dns-dmarc-check.educational.policies.quarantine')} +
+
+ reject: + {$t('diagnostics/dns-dmarc-check.educational.policies.reject')} +
+
+
+ +
+

{$t('diagnostics/dns-dmarc-check.educational.alignmentModes.title')}

+
+
+ Relaxed (r): + {$t('diagnostics/dns-dmarc-check.educational.alignmentModes.relaxed')} +
+
+ Strict (s): + {$t('diagnostics/dns-dmarc-check.educational.alignmentModes.strict')} +
+
+
+ +
+

{$t('diagnostics/dns-dmarc-check.educational.reportingTypes.title')}

+
    +
  • + Aggregate (RUA): + {$t('diagnostics/dns-dmarc-check.educational.reportingTypes.aggregate')} +
  • +
  • + Forensic (RUF): + {$t('diagnostics/dns-dmarc-check.educational.reportingTypes.forensic')} +
  • +
+
+ +
+

{$t('diagnostics/dns-dmarc-check.educational.bestPractices.title')}

+
    +
  • {$t('diagnostics/dns-dmarc-check.educational.bestPractices.items.startMonitoring')}
  • +
  • {$t('diagnostics/dns-dmarc-check.educational.bestPractices.items.gradualEnforcement')}
  • +
  • {$t('diagnostics/dns-dmarc-check.educational.bestPractices.items.setupReporting')}
  • +
  • {$t('diagnostics/dns-dmarc-check.educational.bestPractices.items.strictAlignment')}
  • +
  • {$t('diagnostics/dns-dmarc-check.educational.bestPractices.items.subdomainPolicy')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/dnssec-adflag/+page.svelte b/src/routes/[lang]/diagnostics/dns/dnssec-adflag/+page.svelte new file mode 100644 index 00000000..ad32482e --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/dnssec-adflag/+page.svelte @@ -0,0 +1,482 @@ + + +
+
+

{$t('diagnostics.dnssec-adflag.title')}

+

+ {$t('diagnostics.dnssec-adflag.description')} +

+
+ + +
+
+ + +

{$t('diagnostics.dnssec-adflag.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics.dnssec-adflag.form.title')}

+
+
+
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics.dnssec-adflag.results.title', { domain: results.name })}

+ +
+
+
+
+ {$t('diagnostics.dnssec-adflag.results.query.label')}: + {results.name} ({results.type}) +
+
+ {$t('diagnostics.dnssec-adflag.results.resolver.label')}: + {results.resolver} +
+
+ + +
+

{$t('diagnostics.dnssec-adflag.results.validation.title')}

+
+
+ +
+ {$t('diagnostics.dnssec-adflag.results.validation.adFlag.title')} +

+ {results.authenticated + ? $t('diagnostics.dnssec-adflag.results.validation.adFlag.set') + : $t('diagnostics.dnssec-adflag.results.validation.adFlag.notSet')} +

+
+
+ + {#if results.checkingDisabled} +
+ +
+ {$t('diagnostics.dnssec-adflag.results.validation.cdFlag.title')} +

{$t('diagnostics.dnssec-adflag.results.validation.cdFlag.message')}

+
+
+ {/if} + +
+ +
+ {$t('diagnostics.dnssec-adflag.results.validation.responseCode.title')} +

{results.rcodeText}

+
+
+
+ +
+
{$t('diagnostics.dnssec-adflag.results.explanation.title')}
+

{results.explanation}

+
+
+ + + {#if results.records?.length} +
+

DNS Records ({results.records.length})

+
+ {#each results.records as record, index (index)} +
+
{record.data}
+ {#if record.TTL} +
TTL: {record.TTL}s
+ {/if} +
+ {/each} +
+
+ {/if} + + + {#if results.authority?.length} +
+

Authority Section ({results.authority.length})

+
+ {#each results.authority as record, index (index)} +
+
{record.name} {record.type} {record.data}
+ {#if record.TTL} +
TTL: {record.TTL}s
+ {/if} +
+ {/each} +
+
+ {/if} +
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics.dnssec-adflag.error.title')} +

{error}

+
+

{$t('diagnostics.dnssec-adflag.error.troubleshooting.title')}:

+
    +
  • {$t('diagnostics.dnssec-adflag.error.troubleshooting.domain')}
  • +
  • {$t('diagnostics.dnssec-adflag.error.troubleshooting.recordType')}
  • +
  • {$t('diagnostics.dnssec-adflag.error.troubleshooting.resolver')}
  • +
  • {$t('diagnostics.dnssec-adflag.error.troubleshooting.records')}
  • +
+
+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics.dnssec-adflag.education.title')}

+
+
+
+
+

{$t('diagnostics.dnssec-adflag.education.dnssec.title')}

+

+ {$t('diagnostics.dnssec-adflag.education.dnssec.description')} +

+
+ +
+

{$t('diagnostics.dnssec-adflag.education.adFlag.title')}

+

+ {$t('diagnostics.dnssec-adflag.education.adFlag.description')} +

+
+ +
+

{$t('diagnostics.dnssec-adflag.education.doh.title')}

+

+ {$t('diagnostics.dnssec-adflag.education.doh.description')} +

+
+ +
+

{$t('diagnostics.dnssec-adflag.education.interpreting.title')}

+
    +
  • + {$t('diagnostics.dnssec-adflag.education.interpreting.adSet.title')}: + {$t('diagnostics.dnssec-adflag.education.interpreting.adSet.description')} +
  • +
  • + {$t('diagnostics.dnssec-adflag.education.interpreting.adNotSet.title')}: + {$t('diagnostics.dnssec-adflag.education.interpreting.adNotSet.description')} +
  • +
  • + {$t('diagnostics.dnssec-adflag.education.interpreting.cdSet.title')}: + {$t('diagnostics.dnssec-adflag.education.interpreting.cdSet.description')} +
  • +
  • + {$t('diagnostics.dnssec-adflag.education.interpreting.servfail.title')}: + {$t('diagnostics.dnssec-adflag.education.interpreting.servfail.description')} +
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/dnssec-validation-chain/+page.svelte b/src/routes/[lang]/diagnostics/dns/dnssec-validation-chain/+page.svelte new file mode 100644 index 00000000..7e3808e2 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/dnssec-validation-chain/+page.svelte @@ -0,0 +1,871 @@ + + +
+
+

{$t('diagnostics/dns-dnssec-validation-chain.title')}

+

{$t('diagnostics/dns-dnssec-validation-chain.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/dns-dnssec-validation-chain.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/dns-dnssec-validation-chain.form.title')}

+
+
+
{ + e.preventDefault(); + validateChain(); + }} + > +
+ + clearExampleSelection()} + /> +
+ + +
+
+
+ + {#if error} +
+
+
+ +
+ {$t('diagnostics/dns-dnssec-validation-chain.error.title')} +

{error}

+
+
+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

{$t('diagnostics/dns-dnssec-validation-chain.loading.title')}

+

{$t('diagnostics/dns-dnssec-validation-chain.loading.message', { domain })}

+
+
+
+
+ {/if} + + {#if results} +
+
+

{$t('diagnostics/dns-dnssec-validation-chain.results.title')}

+
+
+ +
+
+

+ {#if results.valid} + + {$t('diagnostics/dns-dnssec-validation-chain.results.summary.valid')} + {:else} + + {$t('diagnostics/dns-dnssec-validation-chain.results.summary.invalid')} + {/if} +

+
+
+
+
+ {$t('diagnostics/dns-dnssec-validation-chain.results.summary.domain')} + {results.domain} +
+
+ {$t('diagnostics/dns-dnssec-validation-chain.results.summary.chainLinks')} + {results.summary.totalLinks} +
+
+ {$t('diagnostics/dns-dnssec-validation-chain.results.summary.validated')} + {results.summary.validatedLinks}/{results.summary.totalLinks} +
+
+ {$t('diagnostics/dns-dnssec-validation-chain.results.summary.status')} + + {results.valid + ? $t('diagnostics/dns-dnssec-validation-chain.results.summary.secure') + : $t('diagnostics/dns-dnssec-validation-chain.results.summary.brokenChain')} + +
+
+ + {#if results.summary.errors.length > 0} +
+

{$t('diagnostics/dns-dnssec-validation-chain.results.summary.errors')}

+
    + {#each results.summary.errors as err, i (i)} +
  • {err}
  • + {/each} +
+
+ {/if} +
+
+ + +
+
+

{$t('diagnostics/dns-dnssec-validation-chain.results.chain.title')}

+
+
+
+ {#each results.chain as link, i (i)} + + + {#if i < results.chain.length - 1} +
+ +
+ {/if} + {/each} +
+
+
+
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/glue-check/+page.svelte b/src/routes/[lang]/diagnostics/dns/glue-check/+page.svelte new file mode 100644 index 00000000..5f0fecfc --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/glue-check/+page.svelte @@ -0,0 +1,645 @@ + + +
+
+

{$t('title')}

+

{$t('description')}

+
+ + +
+
+ + +

{$t('examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('results.summary.title')}

+
+
+
+ +
+ clearExampleSelection()} + onkeydown={(e) => e.key === 'Enter' && checkGlue()} + /> + +
+
+
+
+ + {#if error} +
+
+
+ +
+ {$t('results.summary.title')} +

{error}

+
+
+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

{$t('loading.title')}

+

{$t('loading.description')}

+
+
+
+
+ {/if} + + {#if results} +
+
+

{$t('results.title')}

+
+
+
+
+

Zone: {results.zone}

+ {#if results.parent} +

Parent Zone: {results.parent}

+ {/if} +
+ +
+
+ +

Nameservers Analysis

+
+ +
+ {#each results.nameservers as ns (ns.name)} +
+
+
+ + {ns.name} +
+
+ {#if ns.requiresGlue} + + + Glue Required + + {:else} + + + External + + {/if} +
+
+ +
+ {#if ns.requiresGlue} +
+
+
+ + A Records +
+ {#if ns.glue.a && ns.glue.a.length > 0} +
+ {#each ns.glue.a as ip (ip)} + + + {ip} + + {/each} +
+ {:else} +
+ + No A records found +
+ {/if} +
+ +
+
+ + AAAA Records +
+ {#if ns.glue.aaaa && ns.glue.aaaa.length > 0} +
+ {#each ns.glue.aaaa as ip (ip)} + + + {ip} + + {/each} +
+ {:else} +
+ + No AAAA records found +
+ {/if} +
+
+ {:else} +
+ +
+

External nameserver

+ No glue records required as this nameserver is outside the zone +
+
+ {/if} +
+ + {#if ns.status} + + {/if} +
+ {/each} +
+
+
+
+
+ {#if results.summary} +
+
+

Glue Check Summary

+
+
+
+
+
Total Nameservers
+
{results.summary.total}
+
+
+
+ Requiring Glue +
+
{results.summary.requiringGlue}
+
+
+
+ With Valid Glue +
+
0 && + results.summary.withValidGlue < results.summary.requiringGlue} + > + 0 + ? 'alert-triangle' + : 'x-circle'} + size="sm" + /> + {results.summary.withValidGlue} +
+
+
+
+ Missing Glue +
+
0}> + 0 ? 'alert-circle' : 'check-circle'} size="sm" /> + {results.summary.missingGlue} +
+
+
+
+
+ + {#if results.summary.issues && results.summary.issues.length > 0} +
+
+

Issues Found

+
+
+
+
    + {#each results.summary.issues as issue (issue)} +
  • + + {issue} +
  • + {/each} +
+
+
+
+ {/if} + {/if} + {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/lookup/+page.svelte b/src/routes/[lang]/diagnostics/dns/lookup/+page.svelte new file mode 100644 index 00000000..c44aae0c --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/lookup/+page.svelte @@ -0,0 +1,402 @@ + + +
+
+

{$t('diagnostics/dns-lookup.title')}

+

{$t('diagnostics/dns-lookup.subtitle')}

+
+ + + `${ex.domain} (${ex.type})`} + getDescription={(ex: { description: string }) => ex.description} + getTooltip={(ex: { tooltip: string }) => ex.tooltip} + /> + + +
+
+

{$t('diagnostics/dns-lookup.form.title')}

+
+
+ +
+
+ + { + examples.clear(); + if (domainName) performLookup(); + }} + /> +
+
+ + +
+
+ + +
+ +
+ + {#if !useCustomResolver} + + {/if} + {#if useCustomResolver} + { + examples.clear(); + if (domainName) performLookup(); + }} + /> + {/if} + +
+
+ +
+ + {$t('diagnostics/dns-lookup.form.lookupButton')} + +
+
+
+ + + + + + {#if diagnosticState.results?.noRecords} +
+
+
+ +
+ {$t('diagnostics/dns-lookup.noRecords.title')} +

{diagnosticState.results.message}

+

+ {$t('diagnostics/dns-lookup.noRecords.usingResolver', { resolver: diagnosticState.results.resolver })} +

+
+
+
+
+ {/if} + + + {#if diagnosticState.results && !diagnosticState.results.noRecords} + 0} + > + {#if diagnosticState.results.Answer?.length > 0} +
+ {#each diagnosticState.results.Answer as record, i (i)} +
+
{record.data}
+ {#if record.TTL} +
+ TTL: {record.TTL}s +
+ {/if} +
+ {/each} +
+ {:else} +
+
+ +

{$t('diagnostics/dns-lookup.results.noRecordsMessage', { domain: domainName, type: recordType })}

+
+
+ {/if} +
+ {/if} + + +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/ns-soa-check/+page.svelte b/src/routes/[lang]/diagnostics/dns/ns-soa-check/+page.svelte new file mode 100644 index 00000000..217efd2f --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/ns-soa-check/+page.svelte @@ -0,0 +1,770 @@ + + +
+
+

{$t('diagnostics/dns-ns-soa-check.title')}

+

{$t('diagnostics/dns-ns-soa-check.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/dns-ns-soa-check.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/dns-ns-soa-check.form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if results && !results.error} +
+
+

{$t('diagnostics/dns-ns-soa-check.results.title')}

+ +
+
+ +
+ {#if results} + {@const status = getConsistencyStatus()} +
+ +
+

+ {#if status.status === 'good'} + {$t('diagnostics/dns-ns-soa-check.results.status.healthy')} + {:else if status.status === 'partial'} + {$t('diagnostics/dns-ns-soa-check.results.status.issues')} + {:else} + {$t('diagnostics/dns-ns-soa-check.results.status.problems')} + {/if} +

+

{status.message}

+
+
+ {/if} +
+ + + {#if results.nameservers?.length > 0} +
+

+ {$t('diagnostics/dns-ns-soa-check.results.nameservers.title', { count: results.nameservers.length })} +

+
+ {#each results.nameserverChecks as check, _index (_index)} +
+
+ + {check.nameserver} +
+ + {#if check.resolved && (check as { addresses?: string[] }).addresses && (check as { addresses?: string[] }).addresses!.length > 0} +
+ {#each (check as { addresses: string[] }).addresses! as address, addressIndex (addressIndex)} + {address} + {/each} +
+ {:else if !check.resolved} +
+ + {$t('diagnostics/dns-ns-soa-check.results.nameservers.failedToResolve')} +
+ {/if} +
+ {/each} +
+
+ {/if} + + + {#if results.soa} + {@const parsed = parseSOA(results.soa)} +
+

{$t('diagnostics/dns-ns-soa-check.results.soa.title')}

+ + +
+
{$t('diagnostics/dns-ns-soa-check.results.soa.rawTitle')}
+
+ {results.soa} +
+
+ + + {#if parsed} +
+
{$t('diagnostics/dns-ns-soa-check.results.soa.parsedTitle')}
+
+
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.primaryNS')} +
+
{parsed.primaryNS}
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.administrator')} +
+
{parsed.admin}
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.serial')} +
+
{parsed.serial}
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.refresh')} +
+
+ {parsed.refresh}s + ({formatTime(parsed.refresh)}) +
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.retry')} +
+
+ {parsed.retry}s + ({formatTime(parsed.retry)}) +
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.expire')} +
+
+ {parsed.expire}s + ({formatTime(parsed.expire)}) +
+
+ +
+
+ {$t('diagnostics/dns-ns-soa-check.results.soa.minimumTTL')} +
+
+ {parsed.minimum}s + ({formatTime(parsed.minimum)}) +
+
+
+ + +
+
{$t('diagnostics/dns-ns-soa-check.results.analysis.title')}
+
+ + {#if parsed.serial.toString().length === 10 && parsed.serial.toString().startsWith('202')} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.serialYYYYMMDDNN')} +
+ {:else} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.serialSuggestion')} +
+ {/if} + + + {#if parsed.refresh >= 3600 && parsed.refresh <= 86400} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.refreshGood', { + time: formatTime(parsed.refresh), + })} +
+ {:else if parsed.refresh < 3600} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.refreshTooFrequent', { + time: formatTime(parsed.refresh), + })} +
+ {:else} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.refreshTooLong', { + time: formatTime(parsed.refresh), + })} +
+ {/if} + + + {#if parsed.retry >= 600 && parsed.retry < parsed.refresh} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.retryGood')} +
+ {:else if parsed.retry >= parsed.refresh} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.retryTooLong')} +
+ {:else} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.retryTooShort', { + time: formatTime(parsed.retry), + })} +
+ {/if} + + + {#if parsed.expire >= 604800} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.expireGood', { + time: formatTime(parsed.expire), + })} +
+ {:else} +
+ + {$t('diagnostics/dns-ns-soa-check.results.analysis.expireShort', { + time: formatTime(parsed.expire), + })} +
+ {/if} +
+
+
+ {/if} +
+ {/if} +
+
+ {/if} + + {#if error || results?.error} +
+
+
+ +
+ {$t('diagnostics/dns-ns-soa-check.error.title')} +

{error || results.error}

+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/dns-ns-soa-check.education.title')}

+
+
+
+
+

{$t('diagnostics/dns-ns-soa-check.education.nsRecords.title')}

+

{$t('diagnostics/dns-ns-soa-check.education.nsRecords.description')}

+
    +
  • {$t('diagnostics/dns-ns-soa-check.education.nsRecords.resolveToValidIP')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.nsRecords.beReachable')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.nsRecords.serveConsistent')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.nsRecords.beDistributed')}
  • +
+
+ +
+

{$t('diagnostics/dns-ns-soa-check.education.soaRecord.title')}

+

{$t('diagnostics/dns-ns-soa-check.education.soaRecord.description')}

+
    +
  • {$t('diagnostics/dns-ns-soa-check.education.soaRecord.serial')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.soaRecord.refresh')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.soaRecord.retry')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.soaRecord.expire')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.soaRecord.minimum')}
  • +
+
+ +
+

{$t('diagnostics/dns-ns-soa-check.education.recommended.title')}

+
+
{$t('diagnostics/dns-ns-soa-check.education.recommended.refresh')}
+
{$t('diagnostics/dns-ns-soa-check.education.recommended.retry')}
+
{$t('diagnostics/dns-ns-soa-check.education.recommended.expire')}
+
{$t('diagnostics/dns-ns-soa-check.education.recommended.minimum')}
+
+
+ +
+

{$t('diagnostics/dns-ns-soa-check.education.commonIssues.title')}

+
    +
  • {$t('diagnostics/dns-ns-soa-check.education.commonIssues.unreachable')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.commonIssues.inconsistent')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.commonIssues.wrongValues')}
  • +
  • {$t('diagnostics/dns-ns-soa-check.education.commonIssues.serialIssues')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/propagation/+page.svelte b/src/routes/[lang]/diagnostics/dns/propagation/+page.svelte new file mode 100644 index 00000000..61391629 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/propagation/+page.svelte @@ -0,0 +1,567 @@ + + +
+
+

{$t('diagnostics/dns-propagation.title')}

+

+ {$t('diagnostics/dns-propagation.subtitle')} +

+
+ + + `${ex.domain} (${ex.type})`} + getDescription={(ex) => $t(`diagnostics/dns-propagation.examples.items.${ex.description}.description`)} + getTooltip={(ex) => $t(`diagnostics/dns-propagation.examples.items.${ex.description}.tooltip`)} + /> + + +
+
+

{$t('diagnostics/dns-propagation.form.title')}

+
+
+
+
+ +
+ +
+ +
+
+ +
+ + {$t('diagnostics/dns-propagation.form.checkButton')} + +
+
+
+ + + {#if diagnosticState.results} +
+
+
+

{$t('diagnostics/dns-propagation.results.title')}

+
+ {#if areResultsConsistent()} +
+ + {$t('diagnostics/dns-propagation.results.fullyPropagated')} +
+ {:else} +
+ + {$t('diagnostics/dns-propagation.results.inconsistentResults')} +
+ {/if} +
+
+ +
+
+
+ {#each diagnosticState.results as result, resultIndex (resultIndex)} + {@const res = result as { resolver: string }} + {@const info = resolverInfo[res.resolver as keyof typeof resolverInfo]} + {@const status = getStatusColor(result)} + {@const icon = getStatusIcon(result)} + {@const resultData = result as { + error?: string; + result?: { Answer?: Array<{ data: string; TTL?: number }> }; + }} + +
+
+
+ +
+

{info?.name || res.resolver}

+

{info?.ip || 'Custom'} β€’ {info?.location || 'Unknown'}

+
+
+
+ +
+ {#if resultData.error} +
+ + Error: {resultData.error} +
+ {:else if resultData.result?.Answer?.length} +
+ {#each resultData.result.Answer as record, recordIndex (recordIndex)} +
+ {record.data} + {#if record.TTL} + TTL: {record.TTL}s + {/if} +
+ {/each} +
+ {:else} +
+ + {$t('diagnostics/dns-propagation.results.noRecordsFound')} +
+ {/if} +
+
+ {/each} +
+ + {#if lastQuery} + {@const queryInfo = lastQuery as { domain: string; type: string }} +
+ Last checked: {queryInfo.domain} ({queryInfo.type}) at {new Date().toLocaleString()} +
+ {/if} +
+
+ {/if} + + + + +
+
+

{$t('diagnostics/dns-propagation.education.title')}

+
+
+
+
+

{$t('diagnostics/dns-propagation.education.whatIsPropagation.title')}

+

+ {$t('diagnostics/dns-propagation.education.whatIsPropagation.description')} +

+
+ +
+

{$t('diagnostics/dns-propagation.education.factors.title')}

+
    +
  • {$t('diagnostics/dns-propagation.education.factors.ttl')}
  • +
  • {$t('diagnostics/dns-propagation.education.factors.caching')}
  • +
  • {$t('diagnostics/dns-propagation.education.factors.geography')}
  • +
  • {$t('diagnostics/dns-propagation.education.factors.infrastructure')}
  • +
+
+ +
+

{$t('diagnostics/dns-propagation.education.interpreting.title')}

+
+
+
+ +
+ {$t('diagnostics/dns-propagation.education.interpreting.fullyPropagated')} +
+
+
+ +
+ {$t('diagnostics/dns-propagation.education.interpreting.inconsistent')} +
+
+
+ +
+ {$t('diagnostics/dns-propagation.education.interpreting.error')} +
+
+
+ +
+

{$t('diagnostics/dns-propagation.education.resolversTested.title')}

+
+ {#each Object.entries(resolverInfo) as [_key, info] (_key)} +
+ {info.name} ({info.ip}) + {info.location} +
+ {/each} +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/query-performance/+page.svelte b/src/routes/[lang]/diagnostics/dns/query-performance/+page.svelte new file mode 100644 index 00000000..2e26e48e --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/query-performance/+page.svelte @@ -0,0 +1,1225 @@ + + + + DNS Query Performance Comparison | IP Calc + + + + +
+
+

DNS Query Performance Comparison

+

Compare response times across multiple DNS resolvers to find the fastest for your location

+
+ + +
+
+ + +

Quick Examples

+
+
+ {#each EXAMPLES as example, i (i)} + + {/each} +
+
+
+ + +
+
{ + e.preventDefault(); + testPerformance(); + }} + aria-busy={loading} + > +
+ + { + selectedExampleIndex = null; + }} + placeholder="e.g., google.com" + disabled={loading} + autocomplete="off" + inputmode="url" + aria-invalid={!!(domain && !isInputValid)} + /> +
+
+ + +
+ +
+ + +
+ + + Advanced Options + +
+
+ + + {#if !customResolversValid} + + Invalid IP address format. Enter valid IPv4 or IPv6 addresses. + + {/if} + +
+ +
+
+ +
+ + + Default: 5000ms +
+ +
+ +

Custom DNS servers must be valid IPv4 or IPv6 addresses. Duplicates will be removed automatically.

+
+
+
+
+ + + {#if error} +
+ +
+

Error

+

{error}

+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

Testing DNS Resolvers

+

Querying {recordType} records for {domain}...

+
+
+
+
+ {/if} + + + {#if results} +
+
+

Performance Results

+
+ + +
+
+ +
+ Fastest + {results.statistics.fastest.resolver} + {results.statistics.fastest.time}ms +
+
+
+ +
+ Average + {results.statistics.average}ms + Median: {results.statistics.median}ms +
+
+
+ +
+ Slowest + {results.statistics.slowest.resolver} + {results.statistics.slowest.time}ms +
+
+
+ +
+ Success Rate + {results.statistics.successRate}% + {successCount}/{totalCount} +
+
+
+ + +
+

Resolver Comparison

+
+ {#each sortedResults as result (result.resolver)} + {@const perf = result.success ? getPerformance(result.responseTime) : null} +
+
+
+ {result.resolverName} + {result.resolver} +
+ {#if perf} +
+ {result.responseTime}ms + {perf.label} +
+ {:else} +
+ + Failed +
+ {/if} +
+ + {#if result.records?.length} +
+ + + View {result.records.length} record{result.records.length === 1 ? '' : 's'} + +
+ {#each result.records as record (record)} + {record} + {/each} +
+
+ {/if} + + {#if result.error} +
+ + {result.error} +
+ {/if} +
+ {/each} +
+
+ + +
+
+ Domain + {results.domain} +
+
+ Record Type + {results.recordType} +
+
+ Total Records + {totalRecords} +
+
+ Resolvers Tested + {successCount} of {totalCount} successful +
+
+ Performance Spread + {performanceSpread}ms +
+
+ Fastest + {fastestResolver} +
+
+ Tested + {formattedTimestamp} +
+
+
+ {/if} +
+ + +
+
+
+

{dnsPerformanceContent.sections.whatIsDnsPerformance.title}

+
+
+

{dnsPerformanceContent.sections.whatIsDnsPerformance.content}

+
+
+ +
+
+

{dnsPerformanceContent.sections.interpretingResults.title}

+
+
+

{dnsPerformanceContent.sections.interpretingResults.content}

+
+ {#each dnsPerformanceContent.sections.interpretingResults.ranges as range (range.range)} +
+
+ {range.range} + {range.performance} +
+

{range.description}

+
+ {/each} +
+
+
+ +
+
+

{dnsPerformanceContent.sections.publicResolvers.title}

+
+
+
+ {#each dnsPerformanceContent.sections.publicResolvers.resolvers as resolver (resolver.name)} +
+
+

+ {resolver.name} ({resolver.ip}) +

+
+

{resolver.description}

+
+
+
+ + Pros +
+
    + {#each resolver.pros as pro (pro)} +
  • {pro}
  • + {/each} +
+
+
+
+ + Cons +
+
    + {#each resolver.cons as con (con)} +
  • {con}
  • + {/each} +
+
+
+
+ + Best for +
+

{resolver.bestFor}

+
+
+
+ {/each} +
+
+
+ +
+
+

{dnsPerformanceContent.sections.optimization.title}

+
+
+
+ {#each dnsPerformanceContent.sections.optimization.tips as tip (tip.tip)} +
+

{tip.tip}

+

{tip.description}

+
+ {/each} +
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/reverse-lookup/+page.svelte b/src/routes/[lang]/diagnostics/dns/reverse-lookup/+page.svelte new file mode 100644 index 00000000..ea74374f --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/reverse-lookup/+page.svelte @@ -0,0 +1,516 @@ + + +
+
+

Reverse DNS Lookup

+

+ Perform reverse DNS lookups (PTR records) to find hostnames associated with IP addresses. Automatically handles + both IPv4 and IPv6 addresses with proper .in-addr.arpa and .ip6.arpa zone formatting. +

+
+ + +
+
+ + +

Common IP Examples

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

Reverse Lookup Configuration

+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + {#if results?.warnings?.length > 0} + {@const res = results as { warnings?: string[] }} +
+
+
+ +
+ {#each res.warnings || [] as warning, warningIndex (warningIndex)} +

{warning}

+ {/each} +
+
+
+
+ {/if} + + + {#if results} +
+
+

Reverse DNS Results

+ {#if results.Answer?.length > 0} + + {/if} +
+
+
+
+ IP Address: + {ipAddress} +
+
+ Reverse Zone: + {results.reverseName} +
+
+ + {#if results.Answer?.length > 0} + {@const resData = results as { Answer: Array<{ data: string; TTL?: number }> }} +
+

PTR Records Found:

+ {#each resData.Answer as record, _i (_i)} +
+
{record.data}
+ {#if record.TTL} +
+ TTL: {record.TTL}s +
+ {/if} +
+ {/each} +
+ {:else} +
+ +

No PTR records found for {ipAddress}

+

This IP address may not have a reverse DNS entry configured.

+
+ {/if} +
+
+ {/if} + + {#if error} +
+
+
+ +
+ Reverse Lookup Failed +

{error}

+
+
+
+
+ {/if} + + +
+
+

About Reverse DNS Lookups

+
+
+
+
+

How it Works

+

+ Reverse DNS converts IP addresses to hostnames using PTR records. IPv4 addresses use .in-addr.arpa zones, + while IPv6 addresses use .ip6.arpa zones with each nibble reversed. +

+
+ +
+

Common Use Cases

+
    +
  • Email server verification
  • +
  • Security analysis and logging
  • +
  • Network troubleshooting
  • +
  • Identifying server ownership
  • +
+
+ +
+

Zone Format Examples

+
+
+ IPv4: 8.8.8.8 β†’ 8.8.8.8.in-addr.arpa +
+
+ IPv6: 2001:db8::1 β†’ 1.0.0.0...b.d.0.1.0.0.2.ip6.arpa +
+
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/soa-serial/+page.svelte b/src/routes/[lang]/diagnostics/dns/soa-serial/+page.svelte new file mode 100644 index 00000000..8b038422 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/soa-serial/+page.svelte @@ -0,0 +1,708 @@ + + +
+
+

{$t('diagnostics/dns-soa-serial.title')}

+

{$t('diagnostics/dns-soa-serial.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/dns-soa-serial.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/dns-soa-serial.form.title')}

+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + {#if results} + {@const res = results as { soa?: { serial?: number }; serialAnalysis?: { format?: string } }} + {@const serialInfo = (results as { serialAnalysis?: { formatDescription?: string; explanation?: string } }) + .serialAnalysis} + {@const serialAnalysis = ( + results as { + serialAnalysis?: { + parsed?: { year?: number; month?: number; day?: number; revision?: number; timestamp?: number }; + format?: string; + valid?: boolean; + }; + } + ).serialAnalysis} + {@const soaData = (results as { soa?: { mname?: string; rname?: string; ttl?: number } }).soa} + {@const timingData = (results as { soa?: { refresh?: number; retry?: number; expire?: number; minimum?: number } }) + .soa} +
+
+

{$t('diagnostics/dns-soa-serial.results.title', { name: results.name })}

+ +
+
+
+
+ {$t('diagnostics/dns-soa-serial.results.domainLabel')} + {results.name} +
+
+ {$t('diagnostics/dns-soa-serial.results.resolverLabel')} + {results.resolver} +
+
+ +
+ +
+

{$t('diagnostics/dns-soa-serial.results.serialAnalysis.title')}

+
+
+ {res.soa?.serial || $t('diagnostics/dns-soa-serial.results.serialAnalysis.notAvailable')} + {res.serialAnalysis?.format || $t('diagnostics/dns-soa-serial.results.serialAnalysis.unknown')} +
+ +
+
{$t('diagnostics/dns-soa-serial.results.serialAnalysis.formatLabel')}
+
+ {serialInfo?.formatDescription || + $t('diagnostics/dns-soa-serial.results.serialAnalysis.formatUnknown')} +

+ {serialInfo?.explanation || + $t('diagnostics/dns-soa-serial.results.serialAnalysis.formatExplanation')} +

+
+ {#if serialAnalysis?.parsed} +
{$t('diagnostics/dns-soa-serial.results.serialAnalysis.parsedDateLabel')}
+
+ {#if serialAnalysis.format === 'YYYYMMDDNN'} +
+ {$t('diagnostics/dns-soa-serial.results.serialAnalysis.parsedYear', { + year: serialAnalysis.parsed.year!, + })} + {$t('diagnostics/dns-soa-serial.results.serialAnalysis.parsedMonth', { + month: serialAnalysis.parsed.month!, + })} + {$t('diagnostics/dns-soa-serial.results.serialAnalysis.parsedDay', { + day: serialAnalysis.parsed.day!, + })} + {$t('diagnostics/dns-soa-serial.results.serialAnalysis.parsedRevision', { + revision: serialAnalysis.parsed.revision!, + })} +
+ {:else if serialAnalysis.format === 'Unix Timestamp'} + {formatDate(serialAnalysis.parsed.timestamp!)} + {/if} +
+ {/if} + +
{$t('diagnostics/dns-soa-serial.results.serialAnalysis.validityLabel')}
+
+ + {serialAnalysis?.valid + ? $t('diagnostics/dns-soa-serial.results.serialAnalysis.valid') + : $t('diagnostics/dns-soa-serial.results.serialAnalysis.invalid')} +
+
+
+
+ + +
+

{$t('diagnostics/dns-soa-serial.results.soaDetails.title')}

+
+
{$t('diagnostics/dns-soa-serial.results.soaDetails.primaryServer')}
+
{soaData?.mname || $t('diagnostics/dns-soa-serial.results.soaDetails.notAvailable')}
+ +
{$t('diagnostics/dns-soa-serial.results.soaDetails.contactEmail')}
+
{soaData?.rname || $t('diagnostics/dns-soa-serial.results.soaDetails.notAvailable')}
+ +
{$t('diagnostics/dns-soa-serial.results.soaDetails.ttl')}
+
+ {#if soaData?.ttl} + {soaData.ttl}s + ({formatDuration(soaData.ttl)}) + {:else} + {$t('diagnostics/dns-soa-serial.results.soaDetails.notAvailable')} + {/if} +
+
+
+ + +
+

{$t('diagnostics/dns-soa-serial.results.timing.title')}

+
+
+
{$t('diagnostics/dns-soa-serial.results.timing.refresh.title')}
+
{timingData?.refresh || 0}s
+
+ {formatDuration(timingData?.refresh || 0)} +

{$t('diagnostics/dns-soa-serial.results.timing.refresh.description')}

+
+
+ +
+
{$t('diagnostics/dns-soa-serial.results.timing.retry.title')}
+
{timingData?.retry || 0}s
+
+ {formatDuration(timingData?.retry || 0)} +

{$t('diagnostics/dns-soa-serial.results.timing.retry.description')}

+
+
+ +
+
{$t('diagnostics/dns-soa-serial.results.timing.expire.title')}
+
{timingData?.expire || 0}s
+
+ {formatDuration(timingData?.expire || 0)} +

{$t('diagnostics/dns-soa-serial.results.timing.expire.description')}

+
+
+ +
+
{$t('diagnostics/dns-soa-serial.results.timing.minimum.title')}
+
{timingData?.minimum || 0}s
+
+ {formatDuration(timingData?.minimum || 0)} +

{$t('diagnostics/dns-soa-serial.results.timing.minimum.description')}

+
+
+
+
+ + + {#if results.assessment?.length} + {@const assessmentData = ( + results as { + assessment?: Array<{ severity: string; aspect: string; message: string; recommendation?: string }>; + } + ).assessment} +
+

{$t('diagnostics/dns-soa-serial.results.assessment.title')}

+
+ {#each assessmentData || [] as item, itemIndex (itemIndex)} +
+ +
+ {item.aspect} +

{item.message}

+ {#if item.recommendation} + {item.recommendation} + {/if} +
+
+ {/each} +
+
+ {/if} +
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/dns-soa-serial.error.title')} +

{error}

+
+

{$t('diagnostics/dns-soa-serial.error.troubleshooting')}

+
    +
  • {$t('diagnostics/dns-soa-serial.error.tips.validDomain')}
  • +
  • {$t('diagnostics/dns-soa-serial.error.tips.tryDifferent')}
  • +
  • {$t('diagnostics/dns-soa-serial.error.tips.someResolvers')}
  • +
  • {$t('diagnostics/dns-soa-serial.error.tips.checkDomain')}
  • +
+
+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/dns-soa-serial.education.title')}

+
+
+
+
+

{$t('diagnostics/dns-soa-serial.education.whatIsSOA.title')}

+

{$t('diagnostics/dns-soa-serial.education.whatIsSOA.description')}

+
+ +
+

{$t('diagnostics/dns-soa-serial.education.serialFormats.title')}

+
    +
  • + YYYYMMDDNN: + {$t('diagnostics/dns-soa-serial.education.serialFormats.yyyymmddnn').replace('YYYYMMDDNN: ', '')} +
  • +
  • + Unix Timestamp: + {$t('diagnostics/dns-soa-serial.education.serialFormats.unixTimestamp').replace('Unix Timestamp: ', '')} +
  • +
  • + Sequential: + {$t('diagnostics/dns-soa-serial.education.serialFormats.sequential').replace('Sequential: ', '')} +
  • +
+
+ +
+

{$t('diagnostics/dns-soa-serial.education.timingParams.title')}

+
    +
  • + Refresh: + {$t('diagnostics/dns-soa-serial.education.timingParams.refresh').replace('Refresh: ', '')} +
  • +
  • + Retry: + {$t('diagnostics/dns-soa-serial.education.timingParams.retry').replace('Retry: ', '')} +
  • +
  • + Expire: + {$t('diagnostics/dns-soa-serial.education.timingParams.expire').replace('Expire: ', '')} +
  • +
  • + Minimum: + {$t('diagnostics/dns-soa-serial.education.timingParams.minimum').replace('Minimum: ', '')} +
  • +
+
+ +
+

{$t('diagnostics/dns-soa-serial.education.bestPractices.title')}

+
    +
  • {$t('diagnostics/dns-soa-serial.education.bestPractices.useYYYYMMDDNN')}
  • +
  • {$t('diagnostics/dns-soa-serial.education.bestPractices.setRefresh')}
  • +
  • {$t('diagnostics/dns-soa-serial.education.bestPractices.retryShorter')}
  • +
  • {$t('diagnostics/dns-soa-serial.education.bestPractices.expireLonger')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/spf-evaluator/+page.svelte b/src/routes/[lang]/diagnostics/dns/spf-evaluator/+page.svelte new file mode 100644 index 00000000..6ebe0cc3 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/spf-evaluator/+page.svelte @@ -0,0 +1,672 @@ + + +
+
+

{$t('title')}

+

+ {$t('description')} +

+
+ + +
+
+ + +

{$t('examplesSection.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if results && !results.error} + {@const status = getLookupStatus()} +
+
+

{$t('results.title')}

+ +
+
+ +
+
+ + {status.message} +
+
+ + + {#if results.record} +
+

{$t('results.record.title')}

+
+ {results.record} +
+
+ {/if} + + + {#if (results as { expanded?: { mechanisms?: string[] } }).expanded?.mechanisms?.length} + {@const resultsExpanded = (results as { expanded?: { mechanisms?: string[] } }).expanded} +
+

{$t('results.mechanisms.title')}

+
+ {#each resultsExpanded!.mechanisms! as mechanism, mechanismIndex (mechanismIndex)} + {@const mechInfo = getMechanismType(mechanism as string)} +
+ +
+ {mechanism} + {mechInfo.type} +
+
+ {/each} +
+
+ {/if} + + + {#if results.expanded?.includes?.length > 0} + {@const includesData = (results as { expanded: { includes: unknown[] } }).expanded.includes} +
+

{$t('results.includes.title')}

+
+ {#each renderIncludeTree(includesData) as item, itemIndex (itemIndex)} +
+
+ + {$t(`results.includes.types.${item.type}`)}: + {item.domain} + {#if item.result?.error} + + {:else} + + {/if} +
+ + {#if item.result?.error} +
+ + {item.result.error} +
+ {:else if item.result?.record} +
+ {item.result.record} +
+ {/if} +
+ {/each} +
+
+ {/if} + + + {#if results.expanded?.redirects?.length > 0} + {@const redirectsData = ( + results as { + expanded: { redirects: Array<{ domain: string; result?: { error?: string; record?: string } }> }; + } + ).expanded.redirects} +
+

{$t('results.redirects.title')}

+
+ {#each redirectsData as redirect, redirectIndex (redirectIndex)} +
+
+ + {$t('results.includes.redirectTo', { domain: redirect.domain })} + {#if redirect.result?.error} + + {:else} + + {/if} +
+ + {#if redirect.result?.error} +
+ {redirect.result.error} +
+ {:else if redirect.result?.record} +
+ {redirect.result.record} +
+ {/if} +
+ {/each} +
+
+ {/if} +
+
+ {/if} + + {#if error || results?.error} +
+
+
+ +
+ {$t('error.title')} +

{error || results.error}

+
+
+
+
+ {/if} + + +
+
+

{$t('education.title')}

+
+
+
+
+

{$t('education.mechanisms.title')}

+
+
+ all: + {$t('education.mechanisms.all')} +
+
+ ip4/ip6: + {$t('education.mechanisms.ipAddresses')} +
+
+ a/mx: + {$t('education.mechanisms.records')} +
+
+ include: + {$t('education.mechanisms.include')} +
+
+ redirect: + {$t('education.mechanisms.redirect')} +
+
+
+ +
+

{$t('education.qualifiers.title')}

+
+
+ + + {$t('education.qualifiers.pass')} +
+
+ - + {$t('education.qualifiers.fail')} +
+
+ ~ + {$t('education.qualifiers.softFail')} +
+
+ ? + {$t('education.qualifiers.neutral')} +
+
+
+ +
+

{$t('education.dnsLimits.title')}

+

{$t('education.dnsLimits.description')}

+
    +
  • {$t('education.dnsLimits.includeMechanisms', { mechanism: 'include' })}
  • +
  • {$t('education.dnsLimits.recordMechanisms', { mechanisms: 'a, mx, exists, ptr' })}
  • +
  • {$t('education.dnsLimits.redirectLookups', { modifier: 'redirect' })}
  • +
+
+ +
+

{$t('education.bestPractices.title')}

+
    +
  • {$t('education.bestPractices.keepUnderLimit')}
  • +
  • {$t('education.bestPractices.endWithAll', { failAll: '-all', softFailAll: '~all' })}
  • +
  • {$t('education.bestPractices.useIpAddresses')}
  • +
  • {$t('education.bestPractices.avoidNesting')}
  • +
  • {$t('education.bestPractices.regularAudit')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/dns/spf-flatten/+page.svelte b/src/routes/[lang]/diagnostics/dns/spf-flatten/+page.svelte new file mode 100644 index 00000000..1a43d674 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/spf-flatten/+page.svelte @@ -0,0 +1,556 @@ + + +
+
+

SPF Flatten

+

Resolve include:/redirect= and output a flattened SPF with lookup counts

+
+ + +
+
+ + +

Quick Examples

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

SPF Flatten Configuration

+
+
+
+ +
+ clearExampleSelection()} + onkeydown={(e) => e.key === 'Enter' && flattenSPF()} + /> + +
+
+
+
+ + {#if error} +
+
+
+ +
+ SPF Flatten Failed +

{error}

+
+
+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

Flattening SPF Record

+

Resolving SPF includes and redirects to create a flattened record...

+
+
+
+
+ {/if} + + {#if results} +
+
+

SPF Flatten Results

+
+
+
+ +
+
+

Original SPF Record

+
+
+
+ {results.original} +
+
+
+ + +
+
+
+

Flattened SPF Record

+ +
+
+
+
+ {results.flattened} +
+
+
+ + + {#if results.expansions && results.expansions.length > 0} +
+
+

Expansion Tree

+
+
+
+ {#each results.expansions as expansion, _i (expansion.value)} +
+
+ {expansion.type} + {expansion.value} + + {expansion.lookups} + +
+ + {#if expansion.resolved} +
+ {#each expansion.resolved as item (item)} + {item} + {/each} +
+ {/if} +
+ {/each} +
+
+
+ {/if} + + +
+
+

Statistics

+
+
+
+
+
+ DNS Lookups +
+
7} + class:error={results.stats.dnsLookups > 10} + > + 10 + ? 'alert-circle' + : results.stats.dnsLookups > 7 + ? 'alert-triangle' + : 'check-circle'} + size="sm" + /> + {results.stats.dnsLookups}/10 +
+ {#if results.stats.dnsLookups > 10} +
Exceeds RFC limit!
+ {:else if results.stats.dnsLookups > 7} +
Close to limit
+ {/if} +
+ +
+
+ IPv4 Addresses +
+
{results.stats.ipv4Count}
+
+ +
+
+ IPv6 Addresses +
+
{results.stats.ipv6Count}
+
+ +
+
+ Max Include Depth +
+
{results.stats.includeDepth}
+
+ +
+
+ Record Length +
+
400}> + 450 ? 'alert-triangle' : 'check-circle'} size="sm" /> + {results.stats.recordLength} +
+ {#if results.stats.recordLength > 450} +
May need splitting
+ {/if} +
+ +
+
+ Total Mechanisms +
+
{results.stats.mechanisms}
+
+
+
+
+ + + {#if results.warnings && results.warnings.length > 0} +
+
+

Warnings

+
+
+
+ {#each results.warnings as warning (warning)} +
+ + {warning} +
+ {/each} +
+
+
+ {/if} +
+
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/dns/trace/+page.svelte b/src/routes/[lang]/diagnostics/dns/trace/+page.svelte new file mode 100644 index 00000000..e6649227 --- /dev/null +++ b/src/routes/[lang]/diagnostics/dns/trace/+page.svelte @@ -0,0 +1,595 @@ + + +
+
+

{$t('diagnostics/dns-trace.title')}

+

{$t('diagnostics/dns-trace.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/dns-trace.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/dns-trace.form.title')}

+
+
+
+ +
+ clearExampleSelection()} + onkeydown={(e) => e.key === 'Enter' && performTrace()} + /> + +
+
+
+
+ + {#if error} +
+
+
+ +
+ {$t('diagnostics/dns-trace.error.title')} +

{error}

+
+
+
+
+ {/if} + + {#if loading} +
+
+
+ +
+

{$t('diagnostics/dns-trace.loading.title')}

+

{$t('diagnostics/dns-trace.loading.message')}

+
+
+
+
+ {/if} + + {#if results} +
+
+

{$t('diagnostics/dns-trace.results.pathTitle')}

+
+
+
+ {#each results.path as step, i (i)} +
+
+ {i + 1} +
+ +
+
+ {step.type} + {formatTiming(step.timing)} +
+ +
+ {$t('diagnostics/dns-trace.results.step.query')} + {step.query} + {#if step.qtype} + {step.qtype} + {/if} +
+ +
+ {$t('diagnostics/dns-trace.results.step.server')} + {step.server} + {#if step.serverName} + ({step.serverName}) + {/if} +
+ + {#if step.response} +
+ {$t('diagnostics/dns-trace.results.step.response')} + {#if step.response.type === 'referral'} + + {$t('diagnostics/dns-trace.results.step.referral', { + nameservers: step.response.nameservers.join(', '), + })} + + {:else if step.response.type === 'answer'} + + {#if Array.isArray(step.response.data)} + {step.response.data.join(', ')} + {:else} + {step.response.data} + {/if} + + {:else if step.response.type === 'nodata'} + {$t('diagnostics/dns-trace.results.step.nodata')} + {:else if step.response.type === 'nxdomain'} + {$t('diagnostics/dns-trace.results.step.nxdomain')} + {/if} +
+ {/if} + + {#if step.flags} +
+ {#if step.flags.aa} + AA + {/if} + {#if step.flags.ad} + AD + {/if} + {#if step.flags.rd} + RD + {/if} + {#if step.flags.ra} + RA + {/if} +
+ {/if} +
+
+ {/each} +
+
+
+ + {#if results.summary} +
+
+

{$t('diagnostics/dns-trace.results.summary.title')}

+
+
+
+
+
+ {$t('diagnostics/dns-trace.results.summary.totalTime.label')} +
+
+ + {formatTiming(results.summary.totalTime)} +
+
+
+
+ {$t('diagnostics/dns-trace.results.summary.dnsQueries.label')} +
+
{results.summary.queryCount}
+
+ {#if results.summary.finalServer} +
+
+ {$t('diagnostics/dns-trace.results.summary.finalServer.label')} +
+
{results.summary.finalServer}
+
+ {/if} + {#if results.summary.recordType} +
+
+ {$t('diagnostics/dns-trace.results.summary.recordType.label')} +
+
{results.summary.recordType}
+
+ {/if} + {#if results.summary.totalHops} +
+
+ {$t('diagnostics/dns-trace.results.summary.totalHops.label')} +
+
{results.summary.totalHops}
+
+ {/if} + {#if results.summary.averageLatency} +
+
+ {$t('diagnostics/dns-trace.results.summary.avgLatency.label')} +
+
{results.summary.averageLatency}ms
+
+ {/if} + {#if results.summary.dnssecValid !== undefined} +
+
+ {$t('diagnostics/dns-trace.results.summary.dnssecStatus.label')} +
+
+ + {results.summary.dnssecValid + ? $t('diagnostics/dns-trace.results.summary.dnssecStatus.valid') + : $t('diagnostics/dns-trace.results.summary.dnssecStatus.notValidated')} +
+
+ {/if} + {#if results.summary.authoritativeAnswer !== undefined} +
+
+ {$t('diagnostics/dns-trace.results.summary.authoritative.label')} +
+
+ + {results.summary.authoritativeAnswer + ? $t('diagnostics/dns-trace.results.summary.authoritative.yes') + : $t('diagnostics/dns-trace.results.summary.authoritative.no')} +
+
+ {/if} + {#if results.summary.resolverPath} +
+
+ {$t('diagnostics/dns-trace.results.summary.resolutionPath.label')} +
+
{results.summary.resolverPath}
+
+ {/if} + {#if results.summary.finalAnswer} +
+
+ {$t('diagnostics/dns-trace.results.summary.finalAnswer.label')} +
+
+ {Array.isArray(results.summary.finalAnswer) + ? results.summary.finalAnswer.join(', ') + : results.summary.finalAnswer} +
+
+ {/if} +
+
+
+ {/if} + {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/email/dmarc-check/+page.svelte b/src/routes/[lang]/diagnostics/email/dmarc-check/+page.svelte new file mode 100644 index 00000000..785d9cd0 --- /dev/null +++ b/src/routes/[lang]/diagnostics/email/dmarc-check/+page.svelte @@ -0,0 +1,809 @@ + + +
+
+

{$t('diagnostics/email-dmarc-check.title')}

+

{$t('diagnostics/email-dmarc-check.subtitle')}

+
+ + + ex.domain} + getDescription={(ex) => ex.description} + getTooltip={(ex) => + $t('diagnostics/email-dmarc-check.examples.items.gmail.tooltip').replace('gmail.com', ex.domain)} + /> + + +
+
+

{$t('diagnostics/email-dmarc-check.form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if diagnosticState.results && diagnosticState.results.hasRecord} +
+
+

{$t('diagnostics/email-dmarc-check.results.title')}

+ +
+
+ {#if diagnosticState.results.parsed && diagnosticState.results.deliverabilityHints} + +
+
+ +
+

{$t('diagnostics/email-dmarc-check.results.deliverability.title')}

+

{diagnosticState.results.deliverabilityHints.policyImpact}

+ {#if diagnosticState.results.deliverabilityHints.alignmentComplexity?.strict} +

+ {diagnosticState.results.deliverabilityHints.alignmentComplexity.strict} +

+ {/if} +
+
+ + + {#if diagnosticState.results.deliverabilityHints.recommendations.length > 0} + {@const hintsData = (diagnosticState.results as { deliverabilityHints: { recommendations: string[] } }) + .deliverabilityHints} +
+
{$t('diagnostics/email-dmarc-check.results.deliverability.recommendations')}
+
+ {#each hintsData.recommendations as recommendation, recIndex (recIndex)} +
+ + {recommendation} +
+ {/each} +
+
+ {/if} +
+ + +
+

{$t('diagnostics/email-dmarc-check.results.record.title')}

+
+
_dmarc.{domain}
+ {diagnosticState.results.record} +
+
+ + +
+

{$t('diagnostics/email-dmarc-check.results.policy.title')}

+
+ +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.mainPolicy')} +
+
+ {diagnosticState.results.parsed.policy} + + {#if diagnosticState.results.parsed.policy === 'reject'} + {$t('diagnostics/email-dmarc-check.results.policy.values.reject')} + {:else if diagnosticState.results.parsed.policy === 'quarantine'} + {$t('diagnostics/email-dmarc-check.results.policy.values.quarantine')} + {:else if diagnosticState.results.parsed.policy === 'none'} + {$t('diagnostics/email-dmarc-check.results.policy.values.none')} + {:else} + {$t('diagnostics/email-dmarc-check.results.policy.values.unknown')} + {/if} + +
+
+ + + {#if diagnosticState.results.parsed.subdomainPolicy} +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.subdomainPolicy')} +
+
+ {diagnosticState.results.parsed.subdomainPolicy} +
+
+ {/if} + + +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.coverage')} +
+
+ {diagnosticState.results.parsed.percentage}% + {$t('diagnostics/email-dmarc-check.results.policy.coverageDescription')} +
+
+ + +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.dkimAlignment')} +
+
+ {diagnosticState.results.parsed.alignment.dkim === 's' + ? $t('diagnostics/email-dmarc-check.results.policy.values.strict') + : $t('diagnostics/email-dmarc-check.results.policy.values.relaxed')} + + {diagnosticState.results.parsed.alignment.dkim === 's' + ? $t('diagnostics/email-dmarc-check.results.policy.values.strictDescription') + : $t('diagnostics/email-dmarc-check.results.policy.values.relaxedDescription')} + +
+
+ + +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.spfAlignment')} +
+
+ {diagnosticState.results.parsed.alignment.spf === 's' + ? $t('diagnostics/email-dmarc-check.results.policy.values.strict') + : $t('diagnostics/email-dmarc-check.results.policy.values.relaxed')} + + {diagnosticState.results.parsed.alignment.spf === 's' + ? $t('diagnostics/email-dmarc-check.results.policy.values.strictDescription') + : $t('diagnostics/email-dmarc-check.results.policy.values.relaxedDescription')} + +
+
+ + +
+
+ + {$t('diagnostics/email-dmarc-check.results.policy.failureOptions')} +
+
+ {diagnosticState.results.parsed.reporting.failureOptions} + + {#if diagnosticState.results.parsed.reporting.failureOptions === '0'} + {$t('diagnostics/email-dmarc-check.results.policy.failureOptionsValues.0')} + {:else if diagnosticState.results.parsed.reporting.failureOptions === '1'} + {$t('diagnostics/email-dmarc-check.results.policy.failureOptionsValues.1')} + {:else if diagnosticState.results.parsed.reporting.failureOptions === 'd'} + {$t('diagnostics/email-dmarc-check.results.policy.failureOptionsValues.d')} + {:else if diagnosticState.results.parsed.reporting.failureOptions === 's'} + {$t('diagnostics/email-dmarc-check.results.policy.failureOptionsValues.s')} + {:else} + {$t('diagnostics/email-dmarc-check.results.policy.failureOptionsValues.custom')} + {/if} + +
+
+
+
+ + +
+

{$t('diagnostics/email-dmarc-check.results.reporting.title')}

+
+
+
+ + {$t('diagnostics/email-dmarc-check.results.reporting.aggregateTitle')} +
+
+ {#if diagnosticState.results.parsed.reporting.aggregate} + + {$t('diagnostics/email-dmarc-check.results.reporting.aggregateDescription')} + {:else} + {$t('diagnostics/email-dmarc-check.results.reporting.aggregateNotConfigured')} + {$t('diagnostics/email-dmarc-check.results.reporting.aggregateNotConfiguredHint')} + {/if} +
+
+ +
+
+ + {$t('diagnostics/email-dmarc-check.results.reporting.forensicTitle')} +
+
+ {#if diagnosticState.results.parsed.reporting.forensic} + + {$t('diagnostics/email-dmarc-check.results.reporting.forensicDescription')} + {:else} + {$t('diagnostics/email-dmarc-check.results.reporting.forensicNotConfigured')} + {$t('diagnostics/email-dmarc-check.results.reporting.forensicNotConfiguredHint')} + {/if} +
+
+
+
+ {/if} +
+
+ {/if} + + + {#if diagnosticState.results && diagnosticState.results.hasRecord === false} +
+
+
+ +
+ {$t('diagnostics/email-dmarc-check.noRecord.title')} +

{$t('diagnostics/email-dmarc-check.noRecord.message', { domain })}

+
+
{$t('diagnostics/email-dmarc-check.noRecord.impactTitle')}
+
    +
  • {$t('diagnostics/email-dmarc-check.noRecord.impacts.noProtection')}
  • +
  • {$t('diagnostics/email-dmarc-check.noRecord.impacts.reputation')}
  • +
  • {$t('diagnostics/email-dmarc-check.noRecord.impacts.visibility')}
  • +
  • {$t('diagnostics/email-dmarc-check.noRecord.impacts.recommendation')}
  • +
+
+
+
+
+
+ {/if} + + + + +
+
+

{$t('diagnostics/email-dmarc-check.education.title')}

+
+
+
+
+

{$t('diagnostics/email-dmarc-check.education.policiesTitle')}

+
+
+ none: + {$t('diagnostics/email-dmarc-check.education.policies.none').replace('none: ', '')} +
+
+ quarantine: + {$t('diagnostics/email-dmarc-check.education.policies.quarantine').replace('quarantine: ', '')} +
+
+ reject: + {$t('diagnostics/email-dmarc-check.education.policies.reject').replace('reject: ', '')} +
+
+
+ +
+

{$t('diagnostics/email-dmarc-check.education.bestPracticesTitle')}

+
    +
  • {$t('diagnostics/email-dmarc-check.education.bestPractices.startNone')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.bestPractices.gradual')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.bestPractices.reporting')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.bestPractices.testAlignment')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.bestPractices.subdomain')}
  • +
+
+ +
+

{$t('diagnostics/email-dmarc-check.education.alignmentTitle')}

+
+
+ Relaxed (r): + {$t('diagnostics/email-dmarc-check.education.alignment.relaxed').replace('Relaxed (r): ', '')} +
+
+ Strict (s): + {$t('diagnostics/email-dmarc-check.education.alignment.strict').replace('Strict (s): ', '')} +
+
+
+ +
+

{$t('diagnostics/email-dmarc-check.education.issuesTitle')}

+
    +
  • {$t('diagnostics/email-dmarc-check.education.issues.thirdParty')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.issues.forwarding')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.issues.mailingLists')}
  • +
  • {$t('diagnostics/email-dmarc-check.education.issues.percentage')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/email/greylist-tester/+page.svelte b/src/routes/[lang]/diagnostics/email/greylist-tester/+page.svelte new file mode 100644 index 00000000..ec9f96c8 --- /dev/null +++ b/src/routes/[lang]/diagnostics/email/greylist-tester/+page.svelte @@ -0,0 +1,789 @@ + + +
+
+

{content.title}

+

{content.description}

+
+ + +
+
+

{$t('form.title')}

+
+
+
+
+ + e.key === 'Enter' && testGreylist()} + disabled={diagnosticState.loading} + /> +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + + {#if diagnosticState.loading} +
+
+
+ +
+

{$t('loading.title')}

+

+ {$t('loading.description', { + attempts, + plural: attempts > 1 ? 's' : '', + domain, + port, + delay: delayBetweenAttempts, + })} +

+

+ {$t('loading.note', { duration: attempts * delayBetweenAttempts + 10 })} +

+
+
+
+
+ {/if} + + + + +
+
+ + +

{$t('examples.title')}

+
+
+ {#each examplesList as example (example.domain)} + + {/each} +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('results.title', { domain: diagnosticState.results.domain, port: diagnosticState.results.port })}

+
+
+ +
+ {#if diagnosticState.results.implementsGreylisting} +
+ +
+

{$t('results.status.detected.title')}

+

+ {$t('results.status.detected.description', { + confidence: diagnosticState.results.analysis.confidence, + })} +

+
+
+ {:else} +
+ +
+

{$t('results.status.not_detected.title')}

+

{$t('results.status.not_detected.description')}

+
+
+ {/if} + + {#if diagnosticState.results.analysis.typicalDelay} +
+ +
+

{$t('results.status.typical_delay.title')}

+

+ {$t('results.status.typical_delay.description', { + delay: diagnosticState.results.analysis.typicalDelay, + })} +

+
+
+ {/if} +
+ + +
+

+ + {$t('results.attempts.title')} +

+
+ {#each diagnosticState.results.attempts as attempt (attempt.attemptNumber)} +
+
+ #{attempt.attemptNumber} + {attempt.responseCode || $t('results.attempts.failed')} + {new Date(attempt.timestamp).toLocaleTimeString()} +
+
+ {#if attempt.connected} +
+ {#if attempt.responseCode?.startsWith('2')} + + {/if} + {$t('results.attempts.response')} + + {attempt.response} + +
+
+ {$t('results.attempts.duration')} + {attempt.duration}ms +
+ {:else} +
+ {$t('results.attempts.error')} + + + {attempt.error} + +
+ {/if} +
+
+ {/each} +
+
+ + +
+

+ + {$t('results.analysis.title')} +

+
+
+ {$t('results.analysis.server')} + + + {diagnosticState.results.domain}:{diagnosticState.results.port} + +
+
+ {$t('results.analysis.total_attempts')} + + + {diagnosticState.results.attempts.length} + +
+
+ {$t('results.analysis.successful_connections')} + + 0 ? 'check-circle' : 'x-circle'} size="sm" /> + {connectedCount} / {diagnosticState.results.attempts.length} + +
+
+ {$t('results.analysis.test_duration')} + + + {testDuration}s + +
+
+ {$t('results.analysis.initial_connection')} + + + {diagnosticState.results.analysis.initialRejected + ? $t('results.analysis.temporarily_rejected') + : $t('results.analysis.accepted_immediately')} + +
+
+ {$t('results.analysis.subsequent_attempts')} + + + {diagnosticState.results.analysis.subsequentAccepted + ? $t('results.analysis.accepted_after_delay') + : $t('results.analysis.still_rejected')} + +
+ {#if diagnosticState.results.analysis.typicalDelay} +
+ {$t('results.analysis.delay_duration')} + + + {$t('results.analysis.delay_seconds', { delay: diagnosticState.results.analysis.typicalDelay })} + +
+ {/if} +
+ {$t('results.analysis.confidence_level')} + + + {diagnosticState.results.analysis.confidence.charAt(0).toUpperCase() + + diagnosticState.results.analysis.confidence.slice(1)} + +
+
+
+
+
+ {/if} + + +
+
+

{$t('info.title')}

+
+
+ {#each [{ title: content.sections.whatIsGreylisting.title, content: content.sections.whatIsGreylisting.content, open: true }, { title: content.sections.howItWorks.title, list: content.sections.howItWorks.steps, listKey: 'step' }, { title: content.sections.smtpCodes.title, codes: content.sections.smtpCodes.codes }, { title: content.sections.confidenceLevels.title, list: content.sections.confidenceLevels.levels, listKey: 'level' }, { title: content.sections.benefits.title, list: content.sections.benefits.points, listKey: 'point' }, { title: content.sections.drawbacks.title, list: content.sections.drawbacks.points, listKey: 'point' }, { title: content.sections.bestPractices.title, simpleList: content.sections.bestPractices.practices }, { title: $t('info.quick_tips.title'), simpleList: content.quickTips }] as section (section.title)} +
+ + +

{section.title}

+
+
+ {#if section.content} +

{section.content}

+ {:else if section.codes} +
+ {#each section.codes as code (code.code)} +
+
{code.code}
+
+ {code.name} +

{code.desc}

+
+
+ {/each} +
+ {:else if section.list} +
    + {#each section.list as item ('step' in item ? item.step : 'level' in item ? item.level : item.point)} +
  • + {'step' in item ? item.step : 'level' in item ? item.level : item.point}: + {item.desc} +
  • + {/each} +
+ {:else if section.simpleList} +
    + {#each section.simpleList as item, i (i)} +
  • {item}
  • + {/each} +
+ {/if} +
+
+ {/each} +
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/email/mail-tls-check/+page.svelte b/src/routes/[lang]/diagnostics/email/mail-tls-check/+page.svelte new file mode 100644 index 00000000..faa3f75d --- /dev/null +++ b/src/routes/[lang]/diagnostics/email/mail-tls-check/+page.svelte @@ -0,0 +1,616 @@ + + +
+
+

{content.title}

+

{content.description}

+
+ + +
+
+

Check Mail Server TLS

+
+
+
+ e.key === 'Enter' && checkTLS()} + disabled={diagnosticState.loading} + /> + + +
+
+
+ + + `${ex.domain}:${ex.port}`} + getDescription={(ex) => ex.desc} + getTooltip={(ex) => `Check TLS for ${ex.domain} on port ${ex.port}`} + /> + + + {#if diagnosticState.loading} +
+
+
+ +
+

Checking TLS Support

+

Testing connection to {domain}:{port}...

+
+
+
+
+ {/if} + + + + + {#if diagnosticState.results} +
+
+

TLS Check Results for {diagnosticState.results.domain}:{diagnosticState.results.port}

+
+
+ +
+ {#if diagnosticState.results.supportsSTARTTLS} +
+ +
+

STARTTLS Supported

+

Server supports upgrading to TLS

+
+
+ {/if} + {#if diagnosticState.results.supportsDirectTLS} +
+ +
+

Direct TLS Supported

+

Server supports implicit TLS

+
+
+ {/if} + {#if !diagnosticState.results.supportsSTARTTLS && !diagnosticState.results.supportsDirectTLS} +
+ +
+

TLS Not Supported

+

Server does not support TLS encryption

+
+
+ {/if} +
+ + + {#if diagnosticState.results.tlsVersion || diagnosticState.results.cipherSuite} +
+

+ + Connection Details +

+
+ {#if diagnosticState.results.tlsVersion} +
+ TLS Version + {diagnosticState.results.tlsVersion} +
+ {/if} + {#if diagnosticState.results.cipherSuite} +
+ Cipher Suite + {diagnosticState.results.cipherSuite} +
+ {/if} +
+
+ {/if} + + + {#if diagnosticState.results.certificate} +
+

+ + Certificate Information +

+
+
+ Common Name + {diagnosticState.results.certificate.commonName} +
+
+ Issuer + {diagnosticState.results.certificate.issuer} +
+
+ Valid From + {new Date(diagnosticState.results.certificate.validFrom).toLocaleDateString()} +
+
+ Valid To + + {new Date(diagnosticState.results.certificate.validTo).toLocaleDateString()} + {#if diagnosticState.results.certificate.daysUntilExpiry < 30} + Expires in {diagnosticState.results.certificate.daysUntilExpiry} days + {/if} + +
+
+ Serial Number + {diagnosticState.results.certificate.serialNumber} +
+
+ Fingerprint + {diagnosticState.results.certificate.fingerprint} +
+ {#if diagnosticState.results.certificate.altNames.length > 0} +
+ Alternative Names ({diagnosticState.results.certificate.altNames.length}) +
+ {#each diagnosticState.results.certificate.altNames as altName (altName)} + {altName} + {/each} +
+
+ {/if} +
+
+ {/if} +
+
+ {/if} + + +
+
+

About SMTP TLS

+
+
+
+ + +

{content.sections.whatIsTLS.title}

+
+
+

{content.sections.whatIsTLS.content}

+
+
+ +
+ + +

{content.sections.portInfo.title}

+
+
+
+ {#each content.sections.portInfo.ports as portInfo (portInfo.port)} +
+
{portInfo.port}
+
+ {portInfo.name} +

{portInfo.desc}

+ {portInfo.security} +
+
+ {/each} +
+
+
+ + {#each [{ title: content.sections.tlsTypes.title, items: content.sections.tlsTypes.types, keys: ['name', 'desc', 'ports'] }, { title: content.sections.certificateFields.title, items: content.sections.certificateFields.fields, keys: ['field', 'desc'] }, { title: content.sections.security.title, items: content.sections.security.points, keys: ['point', 'desc'] }, { title: content.sections.troubleshooting.title, items: content.sections.troubleshooting.issues, keys: ['issue', 'solution'] }] as section (section.title)} +
+ + +

{section.title}

+
+
+
    + {#each section.items as item ((item as any)[section.keys[0]])} +
  • + {(item as any)[section.keys[0]]}: + {(item as any)[section.keys[1]]} + {#if section.keys[2] && (item as any)[section.keys[2]]} + ({(item as any)[section.keys[2]]}) + {/if} +
  • + {/each} +
+
+
+ {/each} + +
+ + +

Quick Tips

+
+
+
    + {#each content.quickTips as tip (tip)} +
  • {tip}
  • + {/each} +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/email/mx-health/+page.svelte b/src/routes/[lang]/diagnostics/email/mx-health/+page.svelte new file mode 100644 index 00000000..977d26ce --- /dev/null +++ b/src/routes/[lang]/diagnostics/email/mx-health/+page.svelte @@ -0,0 +1,750 @@ + + +
+
+

{$t('diagnostics.mx-health.title')}

+

+ {$t('diagnostics.mx-health.description')} +

+
+ + + ex.domain} + getDescription={(ex) => ex.description} + getTooltip={(ex) => $t('diagnostics.mx-health.examples.tooltip', { domain: ex.domain })} + /> + + +
+
+

{$t('diagnostics.mx-health.form.title')}

+
+
+
+ +
+ +
+ +
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('diagnostics.mx-health.results.title')}

+ +
+
+ +
+
+ +
+

+ {#if diagnosticState.results.summary.healthy} + {$t('diagnostics.mx-health.results.summary.healthy')} + {:else} + {$t('diagnostics.mx-health.results.summary.issues')} + {/if} +

+

+ {diagnosticState.results.summary.healthyMX} of {diagnosticState.results.summary.totalMX} MX records resolved + successfully + {#if checkPorts && diagnosticState.results.summary.reachableMX !== null} + β€’ {diagnosticState.results.summary.reachableMX} reachable via SMTP + {/if} +

+
+
+ +
+
+ +
+ {$t('diagnostics.mx-health.results.stats.mxRecords')} + {diagnosticState.results.summary.totalMX} +
+
+ +
+ +
+ {$t('diagnostics.mx-health.results.stats.healthy')} + {diagnosticState.results.summary.healthyMX} +
+
+ + {#if checkPorts && diagnosticState.results.summary.reachableMX !== null} +
+ +
+ {$t('diagnostics.mx-health.results.stats.reachable')} + {diagnosticState.results.summary.reachableMX} +
+
+ {/if} + +
+ +
+ {$t('diagnostics.mx-health.results.stats.redundancy')} + {diagnosticState.results.summary.hasRedundancy ? $t('common.yes') : $t('common.no')} +
+
+
+
+ + +
+

{$t('diagnostics.mx-health.results.mxRecords.title')}

+
+ {#each (diagnosticState.results as { mxRecords: Array<{ error?: string; exchange: string; priority: number; addresses?: { ipv4: string[]; ipv6: string[] }; portChecks?: Array<{ port: number; open: boolean; latency?: number }> }> }).mxRecords as mx, _index (_index)} +
+
+
+
+ + {mx.exchange} + Priority {mx.priority} +
+ {#if mx.error} +
+ + {mx.error} +
+ {/if} +
+ +
+ +
+
+ + {#if mx.addresses && !mx.error} +
+ +
+
+
+ + {$t('diagnostics.mx-health.results.mxRecords.ipv4')} +
+
+ {#if mx.addresses.ipv4.length > 0} + {#each mx.addresses.ipv4 as ip, ipIndex (ipIndex)} + {ip} + {/each} + {:else} + {$t('common.none')} + {/if} +
+
+ +
+
+ + {$t('diagnostics.mx-health.results.mxRecords.ipv6')} +
+
+ {#if mx.addresses.ipv6.length > 0} + {#each mx.addresses.ipv6 as ip, ipIndex (ipIndex)} + {ip} + {/each} + {:else} + {$t('common.none')} + {/if} +
+
+
+ + + {#if mx.portChecks && checkPorts} +
+
+ + {$t('diagnostics.mx-health.results.mxRecords.portConnectivity')} +
+
+ {#each mx.portChecks || [] as portCheck, portIndex (portIndex)} +
+
+ {portCheck.port} + {getPortDescription(portCheck.port)} +
+
+ + {portCheck.open + ? $t('diagnostics.mx-health.results.mxRecords.open') + : $t('diagnostics.mx-health.results.mxRecords.closed')} + {#if portCheck.latency} + ({portCheck.latency}ms) + {/if} +
+
+ {/each} +
+
+ {/if} +
+ {/if} +
+ {/each} +
+
+
+
+ {/if} + + + + +
+
+

{$t('diagnostics.mx-health.education.title')}

+
+
+
+
+

{$t('diagnostics.mx-health.education.basics.title')}

+
    +
  • + {$t('diagnostics.mx-health.education.basics.priority.title')}: + {$t('diagnostics.mx-health.education.basics.priority.description')} +
  • +
  • + {$t('diagnostics.mx-health.education.basics.exchange.title')}: + {$t('diagnostics.mx-health.education.basics.exchange.description')} +
  • +
  • + {$t('diagnostics.mx-health.education.basics.redundancy.title')}: + {$t('diagnostics.mx-health.education.basics.redundancy.description')} +
  • +
  • + {$t('diagnostics.mx-health.education.basics.loadBalancing.title')}: + {$t('diagnostics.mx-health.education.basics.loadBalancing.description')} +
  • +
+
+ +
+

{$t('diagnostics.mx-health.education.smtpPorts.title')}

+
+
+ {$t('diagnostics.mx-health.education.smtpPorts.port25.title')}: + {$t('diagnostics.mx-health.education.smtpPorts.port25.description')} +
+
+ {$t('diagnostics.mx-health.education.smtpPorts.port587.title')}: + {$t('diagnostics.mx-health.education.smtpPorts.port587.description')} +
+
+ {$t('diagnostics.mx-health.education.smtpPorts.port465.title')}: + {$t('diagnostics.mx-health.education.smtpPorts.port465.description')} +
+
+
+ +
+

{$t('diagnostics.mx-health.education.healthIndicators.title')}

+
    +
  • {$t('diagnostics.mx-health.education.healthIndicators.allResolve')}
  • +
  • {$t('diagnostics.mx-health.education.healthIndicators.oneReachable')}
  • +
  • {$t('diagnostics.mx-health.education.healthIndicators.multipleRedundancy')}
  • +
  • {$t('diagnostics.mx-health.education.healthIndicators.lowerPriority')}
  • +
+
+ +
+

{$t('diagnostics.mx-health.education.commonIssues.title')}

+
    +
  • {$t('diagnostics.mx-health.education.commonIssues.nonExistentHosts')}
  • +
  • {$t('diagnostics.mx-health.education.commonIssues.portsBlocked')}
  • +
  • {$t('diagnostics.mx-health.education.commonIssues.singlePoint')}
  • +
  • {$t('diagnostics.mx-health.education.commonIssues.incorrectPriority')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/email/spf-check/+page.svelte b/src/routes/[lang]/diagnostics/email/spf-check/+page.svelte new file mode 100644 index 00000000..6e7a2368 --- /dev/null +++ b/src/routes/[lang]/diagnostics/email/spf-check/+page.svelte @@ -0,0 +1,870 @@ + + +
+
+

{$t('diagnostics/email-spf-check.title')}

+

+ {$t('diagnostics/email-spf-check.description')} +

+
+ + +
+
+ + +

{$t('diagnostics/email-spf-check.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/email-spf-check.form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics/email-spf-check.results.title')}

+ +
+
+ {#if results.record} + + {#if results.emailAnalysis} +
+
+ +
+

+ {$t('diagnostics/email-spf-check.results.deliverability.title')}: {results.emailAnalysis.deliverabilityRisk.toUpperCase()} +

+

+ {#if results.emailAnalysis.deliverabilityRisk === 'low'} + {$t('diagnostics/email-spf-check.results.deliverability.low')} + {:else if results.emailAnalysis.deliverabilityRisk === 'medium'} + {$t('diagnostics/email-spf-check.results.deliverability.medium')} + {:else} + {$t('diagnostics/email-spf-check.results.deliverability.high')} + {/if} +

+
+
+ +
+
+ +
+ {$t('diagnostics/email-spf-check.results.details.hardFail.label')} + {results.emailAnalysis.hasHardFail + ? $t('diagnostics/email-spf-check.results.details.hardFail.enabled') + : $t('diagnostics/email-spf-check.results.details.hardFail.disabled')} + + {results.emailAnalysis.hasHardFail + ? $t('diagnostics/email-spf-check.results.details.hardFail.enabledDesc') + : $t('diagnostics/email-spf-check.results.details.hardFail.disabledDesc')} + +
+
+ +
+ +
+ {$t('diagnostics/email-spf-check.results.details.softFail.label')} + {results.emailAnalysis.hasSoftFail + ? $t('diagnostics/email-spf-check.results.details.softFail.enabled') + : $t('diagnostics/email-spf-check.results.details.softFail.disabled')} + + {results.emailAnalysis.hasSoftFail + ? $t('diagnostics/email-spf-check.results.details.softFail.enabledDesc') + : results.emailAnalysis.hasHardFail + ? $t('diagnostics/email-spf-check.results.details.softFail.hardFailInstead') + : $t('diagnostics/email-spf-check.results.details.softFail.noEnforcement')} + +
+
+ + {#if results.emailAnalysis.allowsAll} +
+ +
+ {$t('diagnostics/email-spf-check.results.details.allowsAll.label')} + {$t('diagnostics/email-spf-check.results.details.allowsAll.enabled')} + {$t('diagnostics/email-spf-check.results.details.allowsAll.warning')} +
+
+ {/if} +
+
+ {/if} + + +
+

{$t('diagnostics/email-spf-check.results.record.title')}

+
+
{$t('diagnostics/email-spf-check.results.record.location', { domain })}
+ {results.record} +
+
+ + + {#if results.expanded} +
+

{$t('diagnostics/email-spf-check.results.breakdown.title')}

+ + + {#if results.lookupCount > 8} +
+ +
+ {$t('diagnostics/email-spf-check.results.breakdown.lookupLimitExceeded.title')} +

+ {$t('diagnostics/email-spf-check.results.breakdown.lookupLimitExceeded.message', { + count: results.lookupCount, + })} +

+
+
+ {:else if results.lookupCount > 6} +
+ +
+ {$t('diagnostics/email-spf-check.results.breakdown.highLookupCount.title')} +

+ {$t('diagnostics/email-spf-check.results.breakdown.highLookupCount.message', { + count: results.lookupCount, + })} +

+
+
+ {/if} + + + {#if results.expanded.mechanisms.length > 0} + {@const spfExpanded = (results as { expanded: { mechanisms: string[] } }).expanded} +
+
{$t('diagnostics/email-spf-check.results.breakdown.directMechanisms')}
+
+ {#each spfExpanded.mechanisms as mechanism, mechanismIndex (mechanismIndex)} +
+ {mechanism} + + {#if mechanism.startsWith('v=spf1')} + {$t('diagnostics/email-spf-check.results.mechanisms.spfVersion')} + {:else if mechanism.startsWith('ip4:')} + {$t('diagnostics/email-spf-check.results.mechanisms.ipv4Address', { + address: mechanism.substring(4), + })} + {:else if mechanism.startsWith('ip6:')} + {$t('diagnostics/email-spf-check.results.mechanisms.ipv6Address', { + address: mechanism.substring(4), + })} + {:else if mechanism.startsWith('a:')} + {$t('diagnostics/email-spf-check.results.mechanisms.aRecordSpecific', { + domain: mechanism.substring(2), + })} + {:else if mechanism === 'a'} + {$t('diagnostics/email-spf-check.results.mechanisms.aRecordDomain')} + {:else if mechanism.startsWith('mx:')} + {$t('diagnostics/email-spf-check.results.mechanisms.mxRecordSpecific', { + domain: mechanism.substring(3), + })} + {:else if mechanism === 'mx'} + {$t('diagnostics/email-spf-check.results.mechanisms.mxRecordDomain')} + {:else if mechanism.startsWith('exists:')} + {$t('diagnostics/email-spf-check.results.mechanisms.existsLookup', { + domain: mechanism.substring(7), + })} + {:else if mechanism === '-all'} + {$t('diagnostics/email-spf-check.results.mechanisms.hardFail')} + {:else if mechanism === '~all'} + {$t('diagnostics/email-spf-check.results.mechanisms.softFail')} + {:else if mechanism === '+all'} + {$t('diagnostics/email-spf-check.results.mechanisms.passAll')} + {:else if mechanism === '?all'} + {$t('diagnostics/email-spf-check.results.mechanisms.neutral')} + {:else} + {mechanism} + {/if} + +
+ {/each} +
+
+ {/if} + + + {#if results.expanded.includes.length > 0} + {@const spfIncludes = ( + results as { + expanded: { includes: Array<{ domain: string; result: { record?: string; error?: string } }> }; + } + ).expanded} +
+
{$t('diagnostics/email-spf-check.results.breakdown.includedPolicies')}
+
+ {#each spfIncludes.includes as include, includeIndex (includeIndex)} +
+
+ + {include.domain} +
+ {#if include.result.record} +
+ {include.result.record} +
+ {/if} + {#if include.result.error} +
+ + {include.result.error} +
+ {/if} +
+ {/each} +
+
+ {/if} +
+ {/if} + {:else} +
+
+ +
+

{$t('diagnostics/email-spf-check.results.noRecord.title')}

+

{$t('diagnostics/email-spf-check.results.noRecord.message', { domain })}

+

+ {$t('diagnostics/email-spf-check.results.noRecord.warning')} +

+
+
+
+ {/if} +
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/email-spf-check.results.error.title')} +

{error}

+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/email-spf-check.education.title')}

+
+
+
+
+

{$t('diagnostics/email-spf-check.education.mechanisms.title')}

+
+
+ ip4/ip6: + {$t('diagnostics/email-spf-check.education.mechanisms.ip')} +
+
+ a/mx: + {$t('diagnostics/email-spf-check.education.mechanisms.amx')} +
+
+ include: + {$t('diagnostics/email-spf-check.education.mechanisms.include')} +
+
+ all: + {$t('diagnostics/email-spf-check.education.mechanisms.all')} +
+
+
+ +
+

{$t('diagnostics/email-spf-check.education.deliverability.title')}

+
    +
  • + Hard Fail (-all): + {$t('diagnostics/email-spf-check.education.deliverability.hardFail')} +
  • +
  • + Soft Fail (~all): + {$t('diagnostics/email-spf-check.education.deliverability.softFail')} +
  • +
  • No SPF: {$t('diagnostics/email-spf-check.education.deliverability.noSpf')}
  • +
  • + Too many lookups: + {$t('diagnostics/email-spf-check.education.deliverability.tooManyLookups')} +
  • +
+
+ +
+

{$t('diagnostics/email-spf-check.education.bestPractices.title')}

+
    +
  • {$t('diagnostics/email-spf-check.education.bestPractices.useHardFail')}
  • +
  • {$t('diagnostics/email-spf-check.education.bestPractices.limitLookups')}
  • +
  • {$t('diagnostics/email-spf-check.education.bestPractices.testChanges')}
  • +
  • {$t('diagnostics/email-spf-check.education.bestPractices.monitorDelivery')}
  • +
+
+ +
+

{$t('diagnostics/email-spf-check.education.examples.title')}

+
+
+ v=spf1 include:_spf.google.com ~all + {$t('diagnostics/email-spf-check.education.examples.googleWorkspace')} +
+
+ v=spf1 ip4:192.168.1.1 -all + {$t('diagnostics/email-spf-check.education.examples.specificIp')} +
+
+ v=spf1 a mx -all + {$t('diagnostics/email-spf-check.education.examples.aMxRecords')} +
+
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/http/compression/+page.svelte b/src/routes/[lang]/diagnostics/http/compression/+page.svelte new file mode 100644 index 00000000..e8e45b73 --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/compression/+page.svelte @@ -0,0 +1,466 @@ + + +
+
+

HTTP Compression Check

+

Test gzip, brotli, and deflate compression support and measure size differences

+
+ + + ex.url} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Test compression for ${ex.url}`} + /> + + +
+
+

URL to Test

+
+
+
+ +
+ examples.clear()} + onkeydown={(e) => e.key === 'Enter' && checkCompression()} + /> + +
+
+
+
+ + + + {#if diagnosticState.loading} +
+
+
+ +
+

Testing Compression

+

Checking support for gzip, brotli, and deflate compression...

+
+
+
+
+ {/if} + + {#if diagnosticState.results} +
+
+

Compression Results

+
+
+ +
+
+

Overview

+
+
+
+
+
Server Compression
+
+ + {diagnosticState.results.serverCompression.enabled ? 'Enabled' : 'Disabled'} +
+ {#if diagnosticState.results.serverCompression.encoding} +
{diagnosticState.results.serverCompression.encoding}
+ {/if} +
+ +
+
Best Compression
+
+ + {diagnosticState.results.bestCompression.encoding} +
+
{diagnosticState.results.bestCompression.ratio.toFixed(1)}% reduction
+
+ +
+
Uncompressed Size
+
{formatBytes(diagnosticState.results.uncompressed.size)}
+
+ +
+
Time Taken
+
{diagnosticState.results.timings.total}ms
+
+
+
+
+ + +
+
+

Compression Methods

+
+
+
+ {#each diagnosticState.results.compressionResults as result (result.encoding)} +
+
+
+ + {result.encoding} +
+
+ + {result.supported ? 'Supported' : 'Not Supported'} +
+
+ + {#if result.supported} +
+
+
+
+
+
+
+ {formatBytes(diagnosticState.results.uncompressed.size)} + {formatBytes(result.compressedSize)} +
+
+ +
+
+ Reduction: + {result.ratio.toFixed(1)}% +
+
+ Time: + {result.responseTime}ms +
+
+
+ {/if} +
+ {/each} +
+
+
+ + +
+
+

Response Headers

+
+
+
+ {#each Object.entries(diagnosticState.results.headers) as [key, value] (key)} +
+ {key} + {value} +
+ {/each} +
+
+
+
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/http/cookie-security/+page.svelte b/src/routes/[lang]/diagnostics/http/cookie-security/+page.svelte new file mode 100644 index 00000000..f96ea82b --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/cookie-security/+page.svelte @@ -0,0 +1,712 @@ + + +
+
+

HTTP Cookie Security Inspector

+

Analyze Set-Cookie headers for Secure, HttpOnly, SameSite, and other security attributes

+
+ + + ex.url} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Inspect cookies for ${ex.url}`} + /> + + +
+
+

URL to Inspect

+
+
+
+ +
+ examples.clear()} + onkeydown={(e) => e.key === 'Enter' && checkCookieSecurity()} + /> + +
+
+
+
+ + + + {#if diagnosticState.loading} +
+
+
+ +
+

Analyzing Cookies

+

Inspecting Set-Cookie headers for security attributes...

+
+
+
+
+ {/if} + + {#if diagnosticState.results} +
+
+

Cookie Security Analysis

+
+
+ +
+
+

Security Score

+
+
+
+
+
{getSecurityGrade(diagnosticState.results.securityScore).grade}
+
+ {diagnosticState.results.securityScore !== null + ? `${diagnosticState.results.securityScore}/100` + : 'No cookies'} +
+
+
+

Overall Assessment

+

{diagnosticState.results.summary}

+
+
+ Total Cookies: + {diagnosticState.results.totalCookies} +
+
+ Secure Cookies: + {diagnosticState.results.secureCookies} +
+
+ HttpOnly Cookies: + {diagnosticState.results.httpOnlyCookies} +
+
+
+
+
+
+ + + {#if diagnosticState.results.cookies && diagnosticState.results.cookies.length > 0} +
+
+

Cookie Details

+
+
+
+ {#each diagnosticState.results.cookies as cookie (cookie.id || cookie.name)} + + {/each} +
+
+
+ {:else} +
+
+
+ +

No Cookies Found

+

The server did not send any Set-Cookie headers in the response.

+
+
+
+ {/if} + + + {#if diagnosticState.results.recommendations && diagnosticState.results.recommendations.length > 0} +
+
+

Security Recommendations

+
+
+
+ {#each diagnosticState.results.recommendations as recommendation, index (index)} +
+ +
+

{recommendation.title}

+

{recommendation.description}

+ {#if recommendation.example} + {recommendation.example} + {/if} +
+
+ {/each} +
+
+
+ {/if} +
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/http/cors-check/+page.svelte b/src/routes/[lang]/diagnostics/http/cors-check/+page.svelte new file mode 100644 index 00000000..e6c2d99a --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/cors-check/+page.svelte @@ -0,0 +1,541 @@ + + +
+
+

{$t('diagnostics/http-cors-check.title')}

+

{$t('diagnostics/http-cors-check.description')}

+
+ + + ex.url} + getDescription={(ex: { description: string }) => ex.description} + getTooltip={(ex: { tooltip: string }) => ex.tooltip} + /> + + +
+
+

{$t('diagnostics/http-cors-check.form.title')}

+
+
+
+
+ +
+ +
+ +
+ +
+ +
+
+ +
+ + Check CORS Policy + +
+
+
+ + + {#if diagnosticState.results} + +
+ +
+
+ +
+ {getCORSStatusText()} +
CORS Status
+
+
+ +
+ +
+ {diagnosticState.results.preflight.status || 'Failed'} +
Preflight Status
+
+
+ + {#if diagnosticState.results.analysis.maxAge} +
+ +
+ {diagnosticState.results.analysis.maxAge}s +
Cache Max Age
+
+
+ {/if} +
+ + +
+

CORS Policy Details

+
+
+ +
+ CORS Enabled +

+ {diagnosticState.results.analysis.corsEnabled + ? 'Server has CORS headers configured' + : 'No CORS headers found - requests will be blocked by browsers'} +

+
+
+ +
+ +
+ Origin Access +

+ {#if diagnosticState.results.analysis.allowsOrigin} + Origin '{origin}' is allowed to access this resource + {:else} + Origin '{origin}' is not allowed to access this resource + {/if} +

+
+
+ +
+ +
+ Credentials Support +

+ {diagnosticState.results.analysis.allowsCredentials + ? 'Cookies and credentials can be sent' + : 'Cookies and credentials cannot be sent'} +

+
+
+
+
+ + + {#if diagnosticState.results.analysis.allowedMethods?.length > 0} +
+

Allowed Methods

+
+ {#each diagnosticState.results.analysis.allowedMethods as allowedMethod, index (index)} + {allowedMethod} + {/each} +
+
+ {/if} + + + {#if diagnosticState.results.analysis.allowedHeaders?.length > 0} +
+

Allowed Headers

+
+ {#each diagnosticState.results.analysis.allowedHeaders as header, index (index)} + {header} + {/each} +
+
+ {/if} + + + {#if Object.keys(diagnosticState.results.preflight.headers || {}).length > 0} +
+

CORS Headers

+
+ {#each Object.entries(diagnosticState.results.preflight.headers) as [name, value] (name)} +
+
+ {name}: + {value} +
+
+ {/each} +
+
+ {:else if diagnosticState.results.analysis.corsEnabled} +
+ +

CORS enabled but no detailed headers available

+
+ {:else} +
+ +

No CORS headers found

+

+ The server does not provide CORS headers - cross-origin requests will be blocked by browsers +

+
+ {/if} +
+
+ {/if} + + + + +
+
+

About CORS

+
+
+
+
+

What is CORS?

+

+ Cross-Origin Resource Sharing (CORS) is a security mechanism that allows or restricts web pages from making + requests to a different domain, protocol, or port than the one serving the web page. +

+
+ +
+

Preflight Requests

+

+ For certain requests, browsers send a preflight OPTIONS request to check if the actual request is allowed. + The server responds with CORS headers indicating permissions. +

+
+ +
+

Common CORS Headers

+
    +
  • Access-Control-Allow-Origin: Allowed origins
  • +
  • Access-Control-Allow-Methods: Allowed HTTP methods
  • +
  • Access-Control-Allow-Headers: Allowed request headers
  • +
  • Access-Control-Allow-Credentials: Cookie support
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/http/headers/+page.svelte b/src/routes/[lang]/diagnostics/http/headers/+page.svelte new file mode 100644 index 00000000..1628f8f1 --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/headers/+page.svelte @@ -0,0 +1,420 @@ + + +
+
+

{$t('diagnostics/http-headers.title')}

+

+ {$t('diagnostics/http-headers.description')} +

+
+ + + `${ex.method} ${ex.url}`} + getDescription={(ex) => $t(`diagnostics/http-headers.examples.${ex.description}`)} + getTooltip={(ex) => `Analyze headers for ${ex.url}`} + /> + + +
+
+

{$t('diagnostics/http-headers.form.title')}

+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+ + {$t('diagnostics/http-headers.form.analyze')} + +
+
+
+ + + {#if diagnosticState.results} + + +
+
+ +
+ {diagnosticState.results.status} {diagnosticState.results.statusText} +
{$t('diagnostics/http-headers.results.status.label')}
+
+
+ + {#if diagnosticState.results.size} +
+ +
+ {formatBytes(diagnosticState.results.size)} +
{$t('diagnostics/http-headers.results.status.response_size')}
+
+
+ {/if} + + {#if diagnosticState.results.timings} +
+ +
+ {diagnosticState.results.timings.total.toFixed(0)}ms +
{$t('diagnostics/http-headers.results.status.total_time')}
+
+
+ {/if} +
+ + +
+

{$t('diagnostics/http-headers.results.headers.title')}

+
+ {#each Object.entries(diagnosticState.results.headers) as [name, value] (name)} +
+
+ {name}: + {value} +
+
+ {/each} +
+
+ + {#if diagnosticState.results.timings} +
+

{$t('diagnostics/http-headers.results.timing.title')}

+
+
+
+ {$t('diagnostics/http-headers.results.timing.dns')} + ~{diagnosticState.results.timings.dns.toFixed(1)}ms +
+
+
+
+ {$t('diagnostics/http-headers.results.timing.tcp')} + ~{diagnosticState.results.timings.tcp.toFixed(1)}ms +
+
+ {#if diagnosticState.results.timings.tls > 0} +
+
+ {$t('diagnostics/http-headers.results.timing.tls')} + ~{diagnosticState.results.timings.tls.toFixed(1)}ms +
+
+ {/if} +
+
+ {$t('diagnostics/http-headers.results.timing.ttfb')} + ~{diagnosticState.results.timings.ttfb.toFixed(1)}ms +
+
+
+

{$t('diagnostics/http-headers.results.timing.help')}

+
+ {/if} +
+ {/if} + + + + +
+
+

{$t('diagnostics/http-headers.info.title')}

+
+
+
+
+

{$t('diagnostics/http-headers.info.response.heading')}

+

+ {$t('diagnostics/http-headers.info.response.description')} +

+
+ +
+

{$t('diagnostics/http-headers.info.status_codes.heading')}

+
    +
  • + {$t('diagnostics/http-headers.info.status_codes.2xx.name')} + {$t('diagnostics/http-headers.info.status_codes.2xx.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.status_codes.3xx.name')} + {$t('diagnostics/http-headers.info.status_codes.3xx.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.status_codes.4xx.name')} + {$t('diagnostics/http-headers.info.status_codes.4xx.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.status_codes.5xx.name')} + {$t('diagnostics/http-headers.info.status_codes.5xx.description')} +
  • +
+
+ +
+

{$t('diagnostics/http-headers.info.common.heading')}

+
    +
  • + {$t('diagnostics/http-headers.info.common.content_type.name')} + {$t('diagnostics/http-headers.info.common.content_type.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.common.cache_control.name')} + {$t('diagnostics/http-headers.info.common.cache_control.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.common.set_cookie.name')} + {$t('diagnostics/http-headers.info.common.set_cookie.description')} +
  • +
  • + {$t('diagnostics/http-headers.info.common.location.name')} + {$t('diagnostics/http-headers.info.common.location.description')} +
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/http/perf/+page.svelte b/src/routes/[lang]/diagnostics/http/perf/+page.svelte new file mode 100644 index 00000000..17a5af3e --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/perf/+page.svelte @@ -0,0 +1,590 @@ + + +
+
+

{$t('diagnostics.httpPerf.title')}

+

+ {$t('diagnostics.httpPerf.description')} +

+
+ + + ex.url} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Measure performance for ${ex.url}`} + /> + + +
+
+

{$t('diagnostics.httpPerf.form.title')}

+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} + {@const grade = getPerformanceGrade(diagnosticState.results.timings.total)} +
+
+

{$t('results.title')}

+ +
+
+ +
+
+ +
+ {$t('results.grade.label', { grade: grade.grade })} +
+ {$t('results.grade.total', { total: diagnosticState.results.timings.total.toFixed(0) })} +
+
+
+ +
+ +
+ {diagnosticState.results.status} +
{$t('results.overview.http_status')}
+
+
+ + {#if diagnosticState.results.size} +
+ +
+ {formatBytes(diagnosticState.results.size)} +
{$t('results.overview.response_size')}
+
+
+ {/if} + + {#if diagnosticState.results.size && diagnosticState.results.timings.total} +
+ +
+ {calculateThroughput(diagnosticState.results.size, diagnosticState.results.timings.total)} +
{$t('results.overview.throughput')}
+
+
+ {/if} +
+ + +
+

{$t('results.timing.title')}

+
+
+
+ + {$t('results.timing.dns')} + {#if diagnosticState.results.timings.dns_note} + ({diagnosticState.results.timings.dns_note}) + {/if} +
+
+
+
+
{diagnosticState.results.timings.dns.toFixed(1)}ms
+
+ +
+
+ + {$t('results.timing.tcp')} + {#if diagnosticState.results.timings.tcp_note} + ({diagnosticState.results.timings.tcp_note}) + {/if} +
+
+
+
+
{diagnosticState.results.timings.tcp.toFixed(1)}ms
+
+ + {#if diagnosticState.results.timings.tls > 0} +
+
+ + {$t('results.timing.tls')} + {#if diagnosticState.results.timings.tls_note} + ({diagnosticState.results.timings.tls_note}) + {/if} +
+
+
+
+
{diagnosticState.results.timings.tls.toFixed(1)}ms
+
+ {/if} + +
+
+ + {$t('results.timing.ttfb')} +
+
+
+
+
{diagnosticState.results.timings.ttfb.toFixed(1)}ms
+
+ +
+
+ + {$t('results.timing.total')} +
+
+
+
+
{diagnosticState.results.timings.total.toFixed(1)}ms
+
+
+
+ + +
+

{$t('results.features.title')}

+
+
+ +
+ {$t('results.features.https.name')} +

+ {diagnosticState.results.performance.isHTTPS + ? $t('results.features.https.secure') + : $t('results.features.https.unsecure')} +

+
+
+ +
+ +
+ {$t('results.features.compression.name')} +

+ {diagnosticState.results.performance.hasCompression + ? $t('results.features.compression.enabled') + : $t('results.features.compression.disabled')} +

+
+
+ +
+ +
+ {$t('results.features.http_version')} +

{diagnosticState.results.performance.httpVersion}

+
+
+ +
+ +
+ {$t('results.features.connection_reuse')} +

{diagnosticState.results.performance.connectionReused}

+
+
+
+
+
+
+ {/if} + + + + +
+
+

{$t('info.title')}

+
+
+
+
+

{$t('info.timing.heading')}

+
    +
  • {$t('info.timing.dns.name')} {$t('info.timing.dns.description')}
  • +
  • {$t('info.timing.tcp.name')} {$t('info.timing.tcp.description')}
  • +
  • {$t('info.timing.tls.name')} {$t('info.timing.tls.description')}
  • +
  • {$t('info.timing.ttfb.name')} {$t('info.timing.ttfb.description')}
  • +
+
+ +
+

{$t('info.grades.heading')}

+
    +
  • {$t('info.grades.a.label')} {$t('info.grades.a.description')}
  • +
  • {$t('info.grades.b.label')} {$t('info.grades.b.description')}
  • +
  • {$t('info.grades.c.label')} {$t('info.grades.c.description')}
  • +
  • {$t('info.grades.d.label')} {$t('info.grades.d.description')}
  • +
+
+ +
+

{$t('info.tips.heading')}

+

+ {$t('info.tips.content')} +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/http/redirect-trace/+page.svelte b/src/routes/[lang]/diagnostics/http/redirect-trace/+page.svelte new file mode 100644 index 00000000..afbeba17 --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/redirect-trace/+page.svelte @@ -0,0 +1,538 @@ + + +
+
+

{$t('title')}

+

+ Follow and analyze HTTP redirect chains to understand the complete journey from initial URL to final destination. + Track status codes, locations, and security implications. +

+
+ + + ex.url} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Trace redirects for ${ex.url}`} + /> + + +
+
+

{$t('form.title')}

+
+
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('results.title')}

+ +
+
+ +
+
+ +
+ {diagnosticState.results.totalRedirects} +
{$t('results.stats.totalRedirects')}
+
+
+ +
+ +
+ {diagnosticState.results.finalStatus} +
{$t('results.stats.finalStatus')}
+
+
+ + {#if diagnosticState.results.timings} +
+ +
+ {diagnosticState.results.timings.total.toFixed(0)}ms +
{$t('results.stats.totalTime')}
+
+
+ {/if} +
+ + + {#if diagnosticState.results.redirectChain?.length > 0} +
+

{$t('results.chain.title')}

+
+ {#each diagnosticState.results.redirectChain as step, i (i)} +
+
{i + 1}
+
+
+
+ + {step.status} +
+ {#if hasHSTS(step.headers)} +
+ + HSTS +
+ {/if} + {#if i < diagnosticState.results.redirectChain.length - 1 && isSecureRedirect(step.url, step.location)} +
+ + Secure Upgrade +
+ {/if} +
+
{step.url}
+ {#if step.location} +
+ + {step.location} +
+ {/if} +
+
+ + {#if i < diagnosticState.results.redirectChain.length - 1} +
+ +
+ {/if} + {/each} +
+
+ +
+

{$t('results.chain.finalDestination')}

+
+
+ + {diagnosticState.results.finalStatus} +
+
{diagnosticState.results.finalUrl}
+
+
+ {:else} +
+ +

{$t('results.chain.noRedirects')}

+

Final URL: {diagnosticState.results.finalUrl}

+
+ {/if} +
+
+ {/if} + + + + +
+
+

{$t('education.title')}

+
+
+
+
+

{$t('education.redirectTypes.title')}

+
    +
  • 301: Permanent redirect
  • +
  • 302: Temporary redirect
  • +
  • 303: See other (POST β†’ GET)
  • +
  • 307: Temporary (preserve method)
  • +
  • 308: Permanent (preserve method)
  • +
+
+ +
+

{$t('education.security.title')}

+
    +
  • {$t('education.security.hsts')} {$t('education.security.hstsDescription')}
  • +
  • + {$t('education.security.httpsUpgrade')} + {$t('education.security.httpsDescription')} +
  • +
  • + {$t('education.security.openRedirects')} + {$t('education.security.openDescription')} +
  • +
  • {$t('education.security.loops')} {$t('education.security.loopsDescription')}
  • +
+
+ +
+

{$t('education.performance.title')}

+

+ Each redirect adds latency. Minimize redirect chains for better performance. Use 301/308 for permanent moves + and 302/307 for temporary ones. +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/http/security/+page.svelte b/src/routes/[lang]/diagnostics/http/security/+page.svelte new file mode 100644 index 00000000..28ebd6a9 --- /dev/null +++ b/src/routes/[lang]/diagnostics/http/security/+page.svelte @@ -0,0 +1,478 @@ + + +
+
+

{$t('title')}

+

+ {$t('description')} +

+
+ + + ex.url} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Analyze security headers for ${ex.url}`} + /> + + +
+
+

{$t('form.title')}

+
+
+
+ +
+ +
+ +
+
+
+ + + {#if diagnosticState.results} + {@const overallScore = getOverallScore()} +
+
+

{$t('results.title')}

+ +
+
+ +
+
+ +
+ {$t('results.score.grade', { grade: overallScore.grade })} +
{$t('results.score.label', { score: overallScore.score })}
+
+
+ +
+ +
+ {(diagnosticState.results as { analysis?: Array<{ status: string }> })?.analysis?.filter( + (a) => a.status === 'present', + ).length || 0} +
{$t('results.score.headers_present')}
+
+
+ +
+ +
+ {(diagnosticState.results as { analysis?: Array<{ status: string }> })?.analysis?.filter( + (a) => a.status === 'missing', + ).length || 0} +
{$t('results.score.headers_missing')}
+
+
+
+ + +
+

{$t('results.analysis.title')}

+
+ {#each diagnosticState.results.analysis as analysis, index (index)} +
+
+
+ + {analysis.header} +
+
+ {analysis.status} +
+
+
{analysis.message}
+ {#if analysis.recommendation} +
+ + {analysis.recommendation} +
+ {/if} +
+ {/each} +
+
+ + + {#if Object.keys(diagnosticState.results.headers || {}).length > 0} +
+

{$t('results.headers.title')}

+
+ {#each Object.entries(diagnosticState.results.headers) as [name, value], index (index)} +
+
+ {name}: + {value} +
+
+ {/each} +
+
+ {:else} +
+ +

{$t('results.none.title')}

+

{$t('results.none.message')}

+
+ {/if} +
+
+ {/if} + + + + +
+
+

{$t('info.title')}

+
+
+
+
+

{$t('info.critical.heading')}

+
    +
  • {$t('info.critical.hsts.name')} {$t('info.critical.hsts.description')}
  • +
  • {$t('info.critical.csp.name')} {$t('info.critical.csp.description')}
  • +
  • {$t('info.critical.xfo.name')} {$t('info.critical.xfo.description')}
  • +
  • {$t('info.critical.xcto.name')} {$t('info.critical.xcto.description')}
  • +
+
+ +
+

{$t('info.additional.heading')}

+
    +
  • {$t('info.additional.referrer.name')} {$t('info.additional.referrer.description')}
  • +
  • + {$t('info.additional.permissions.name')} + {$t('info.additional.permissions.description')} +
  • +
  • {$t('info.additional.cors.name')} {$t('info.additional.cors.description')}
  • +
  • {$t('info.additional.xss.name')} {$t('info.additional.xss.description')}
  • +
+
+ +
+

{$t('info.tips.heading')}

+

+ {$t('info.tips.content')} +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/network/asn-geo-lookup/+page.svelte b/src/routes/[lang]/diagnostics/network/asn-geo-lookup/+page.svelte new file mode 100644 index 00000000..1360ad32 --- /dev/null +++ b/src/routes/[lang]/diagnostics/network/asn-geo-lookup/+page.svelte @@ -0,0 +1,661 @@ + + +
+
+

{$t('diagnostics/network-asn-geo-lookup.title')}

+

{$t('diagnostics/network-asn-geo-lookup.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/network-asn-geo-lookup.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/network-asn-geo-lookup.form.title')}

+
+
+
+
+ + { + clearExampleSelection(); + if (ip.trim()) lookupIP(); + }} + /> +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics/network-asn-geo-lookup.results.title', { ip: results.ip })}

+ +
+
+
+ +
+

{$t('diagnostics/network-asn-geo-lookup.results.networkInfo.title')}

+
+ {#if results.asn} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.networkInfo.asn')} + AS{results.asn} +
+
+ {/if} + {#if results.asnOrg} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.networkInfo.organization')} + {results.asnOrg} +
+
+ {/if} + {#if results.isp} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.networkInfo.isp')} + {results.isp} +
+
+ {/if} +
+
+ + +
+

{$t('diagnostics/network-asn-geo-lookup.results.geoLocation.title')}

+
+ {#if results.country} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.geoLocation.country')} + {results.country} ({results.countryCode}) +
+
+ {/if} + {#if results.regionName} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.geoLocation.region')} + {results.regionName} +
+
+ {/if} + {#if results.city} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.geoLocation.city')} + {results.city} +
+
+ {/if} + {#if results.timezone} +
+ +
+ {$t('diagnostics/network-asn-geo-lookup.results.geoLocation.timezone')} + {results.timezone} +
+
+ {/if} +
+
+ + + {#if results.latitude !== undefined && results.longitude !== undefined} +
+

{$t('diagnostics/network-asn-geo-lookup.results.coordinates.title')}

+
+
+
+ +
+
+ {$t('diagnostics/network-asn-geo-lookup.results.coordinates.latitude')} + {results.latitude.toFixed(4)}Β° +
+
+ {$t('diagnostics/network-asn-geo-lookup.results.coordinates.longitude')} + {results.longitude.toFixed(4)}Β° +
+
+
+ + + {$t('diagnostics/network-asn-geo-lookup.results.coordinates.viewMap')} + +
+
+ +
+
+
+ {/if} + + +
+

{$t('diagnostics/network-asn-geo-lookup.results.connectionType.title')}

+
+
+ + {$t('diagnostics/network-asn-geo-lookup.results.connectionType.mobile')} +
+
+ + {$t('diagnostics/network-asn-geo-lookup.results.connectionType.proxy')} +
+
+ + {$t('diagnostics/network-asn-geo-lookup.results.connectionType.hosting')} +
+
+
+
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/network-asn-geo-lookup.error.title')} +

{error}

+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/network-asn-geo-lookup.info.title')}

+
+
+
+
+

{asnGeoContent.sections.whatIsGeoIP.title}

+

{asnGeoContent.sections.whatIsGeoIP.content}

+
+ +
+

{asnGeoContent.sections.accuracy.title}

+
    + {#each asnGeoContent.sections.accuracy.levels as level (level.level)} +
  • + {level.level} ({level.accuracy}): + {level.description} +
  • + {/each} +
+
+ +
+

{asnGeoContent.sections.asnExplained.title}

+

{asnGeoContent.sections.asnExplained.content}

+
+ +
+

{asnGeoContent.sections.dataSource.title}

+

{asnGeoContent.sections.dataSource.content}

+
+
+ +
+

{$t('diagnostics/network-asn-geo-lookup.info.quickTips.title')}

+
    + {#each asnGeoContent.quickTips as tip, idx (idx)} +
  • {tip}
  • + {/each} +
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/network/bgp-route-lookup/+page.svelte b/src/routes/[lang]/diagnostics/network/bgp-route-lookup/+page.svelte new file mode 100644 index 00000000..9bb93430 --- /dev/null +++ b/src/routes/[lang]/diagnostics/network/bgp-route-lookup/+page.svelte @@ -0,0 +1,691 @@ + + +
+
+

{bgpContent.title}

+

{bgpContent.description}

+
+ + +
+
+ + +

Example Lookups

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

BGP Lookup

+
+
+
+ +
+ { + clearExampleSelection(); + if (resource.trim()) lookupBGP(); + }} + /> + +
+
+
+
+ + + {#if results} +
+
+

BGP Routing Information for {results.resource}

+ +
+
+ +
+
+ +
+

{results.announced ? 'Announced in BGP' : 'Not Announced'}

+

+ {results.announced + ? 'This resource is actively advertised in the global BGP routing table' + : 'This resource is not currently visible in BGP'} +

+
+
+
+ + +
+ + {#if results.originAS} +
+

Origin Autonomous System

+
+ +
+
AS{results.originAS}
+ {#if results.originName} +
{results.originName}
+ {/if} +
+
+
+ {/if} + + + {#if results.asPath && results.asPath.path.length > 0} +
+

AS Path

+
+ {#each results.asPath.path as asn, index (index)} + + AS{asn} + {#if index < results.asPath.path.length - 1} + + {/if} + + {/each} +
+
+ Path length: {results.asPath.path.length} hops +
+
+ {/if} + + + {#if results.prefixes.length > 0} +
+

BGP Prefixes ({results.prefixes.length})

+ {#each results.prefixes as prefix, index (index)} +
+
+ + {prefix.prefix} +
+
+
+ ASN: + AS{prefix.asn} +
+
+ Holder: + {prefix.holder} +
+ {#if prefix.country} +
+ Country: + {prefix.country} +
+ {/if} +
+
+ {/each} +
+ {/if} + + + {#if results.peers && results.peers.length > 0} +
+

BGP Peers ({results.peers.length})

+
+ {#each results.peers.slice(0, 8) as peer, index (index)} +
+ AS{peer.asn} + {#if peer.country} + {peer.country} + {/if} +
+ {/each} +
+
+ {/if} + + + {#if (results.moreSpecifics && results.moreSpecifics.length > 0) || (results.lessSpecifics && results.lessSpecifics.length > 0)} +
+

Related Prefixes

+ +
+ {/if} +
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ BGP Lookup Failed +

{error}

+
+
+
+
+ {/if} + + +
+
+

About BGP Routing

+
+
+
+
+

{bgpContent.sections.whatIsBGP.title}

+

{bgpContent.sections.whatIsBGP.content}

+
+ +
+

{bgpContent.sections.asPath.title}

+

{bgpContent.sections.asPath.content}

+
    + {#each bgpContent.sections.asPath.attributes as attr (attr.name)} +
  • {attr.name}: {attr.description}
  • + {/each} +
+
+ +
+

{bgpContent.sections.routeTypes.title}

+
    + {#each bgpContent.sections.routeTypes.types as type (type.type)} +
  • + {type.type}: + {type.description} + ({type.indicator}) +
  • + {/each} +
+
+ +
+

{bgpContent.sections.dataSource.title}

+

{bgpContent.sections.dataSource.content}

+
+
+ +
+

Quick Tips

+
    + {#each bgpContent.quickTips as tip, idx (idx)} +
  • {tip}
  • + {/each} +
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/network/http-ping/+page.svelte b/src/routes/[lang]/diagnostics/network/http-ping/+page.svelte new file mode 100644 index 00000000..7c2644b7 --- /dev/null +++ b/src/routes/[lang]/diagnostics/network/http-ping/+page.svelte @@ -0,0 +1,712 @@ + + +
+
+

{$t('diagnostics/network-http-ping.title')}

+

+ {$t('diagnostics/network-http-ping.description')} +

+
+ + + ex.description} + getDescription={(ex) => `${ex.url} (${ex.method})`} + getTooltip={(ex) => $t('diagnostics/network-http-ping.examples.tooltip', { url: ex.url, method: ex.method })} + /> + + +
+
+

{$t('diagnostics/network-http-ping.form.title')}

+
+
+
+
+ +
+
+ + +
+
+

{$t('diagnostics/network-http-ping.form.method.title')}

+
+ {#each httpMethods as methodOption, index (index)} + + {/each} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('diagnostics/network-http-ping.results.title')}

+ +
+
+ +
+
+ +
+ {diagnosticState.results.successful}/{diagnosticState.results.count} +

{$t('diagnostics/network-http-ping.results.successfulRequests')}

+
+
+ {#if diagnosticState.results.statistics?.avg} +
+ +
+ {diagnosticState.results.statistics.avg}ms +

+ {$t('diagnostics/network-http-ping.results.averageLatency', { + description: getLatencyDescription(diagnosticState.results.statistics.avg), + })} +

+
+
+ {/if} + {#if diagnosticState.results.failed > 0} +
+ +
+ {diagnosticState.results.failed} +

{$t('diagnostics/network-http-ping.results.failedRequests')}

+
+
+ {/if} +
+ + {#if diagnosticState.results.statistics && diagnosticState.results.successful > 0} + +
+

{$t('diagnostics/network-http-ping.results.latencyStatistics')}

+
+
+ {$t('diagnostics/network-http-ping.results.minimum')}: + {diagnosticState.results.statistics.min}ms +
+
+ {$t('diagnostics/network-http-ping.results.maximum')}: + {diagnosticState.results.statistics.max}ms +
+
+ {$t('diagnostics/network-http-ping.results.average')}: + {diagnosticState.results.statistics.avg}ms +
+
+ {$t('diagnostics/network-http-ping.results.median')}: + {diagnosticState.results.statistics.median}ms +
+
+ {$t('diagnostics/network-http-ping.results.p95')}: + {diagnosticState.results.statistics.p95}ms +
+
+ {$t('diagnostics/network-http-ping.results.range')}: + {diagnosticState.results.statistics.max - diagnosticState.results.statistics.min}ms +
+
+
+ + + {#if diagnosticState.results.latencies?.length > 0} +
+

{$t('diagnostics/network-http-ping.results.individualResults')}

+
+ {#each diagnosticState.results.latencies as latency, i (i)} +
+ #{i + 1} + {latency}ms + {getLatencyDescription(latency)} +
+ {/each} +
+
+ {/if} + {/if} + + + {#if diagnosticState.results.errors?.length > 0} +
+

+ {$t('diagnostics/network-http-ping.results.requestErrors', { + count: diagnosticState.results.errors.length, + })} +

+
+ {#each diagnosticState.results.errors as error, i (i)} +
+ #{i + diagnosticState.results.successful + 1} + {error} +
+ {/each} +
+
+ {/if} + + +
+

{$t('diagnostics/network-http-ping.results.requestInformation')}

+
+
+ {$t('diagnostics/network-http-ping.results.url')}: + {diagnosticState.results.url} +
+
+ {$t('diagnostics/network-http-ping.results.method')}: + {diagnosticState.results.method} +
+
+ {$t('diagnostics/network-http-ping.results.successRate')}: + {((diagnosticState.results.successful / diagnosticState.results.count) * 100).toFixed(1)}% +
+
+
+
+
+ {/if} + + + + +
+
+

{$t('diagnostics/network-http-ping.about.title')}

+
+
+
+
+

{$t('diagnostics/network-http-ping.about.comparison.title')}

+
    +
  • HTTP: {$t('diagnostics/network-http-ping.about.comparison.http')}
  • +
  • ICMP: {$t('diagnostics/network-http-ping.about.comparison.icmp')}
  • +
  • {$t('diagnostics/network-http-ping.about.comparison.userExperience')}
  • +
  • {$t('diagnostics/network-http-ping.about.comparison.firewall')}
  • +
+
+ +
+

{$t('diagnostics/network-http-ping.about.requestMethods.title')}

+
    +
  • HEAD: {$t('diagnostics/network-http-ping.about.requestMethods.head')}
  • +
  • GET: {$t('diagnostics/network-http-ping.about.requestMethods.get')}
  • +
  • OPTIONS: {$t('diagnostics/network-http-ping.about.requestMethods.options')}
  • +
+
+ +
+

{$t('diagnostics/network-http-ping.about.latencyGuidelines.title')}

+
    +
  • + < 100ms: + {$t('diagnostics/network-http-ping.about.latencyGuidelines.excellent')} +
  • +
  • 100-300ms: {$t('diagnostics/network-http-ping.about.latencyGuidelines.good')}
  • +
  • + 300-1000ms: + {$t('diagnostics/network-http-ping.about.latencyGuidelines.acceptable')} +
  • +
  • > 1000ms: {$t('diagnostics/network-http-ping.about.latencyGuidelines.poor')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/network/ipv6-connectivity-checker/+page.svelte b/src/routes/[lang]/diagnostics/network/ipv6-connectivity-checker/+page.svelte new file mode 100644 index 00000000..6bba74f0 --- /dev/null +++ b/src/routes/[lang]/diagnostics/network/ipv6-connectivity-checker/+page.svelte @@ -0,0 +1,412 @@ + + +
+
+

{content.title}

+

{content.description}

+
+ + +
+
+

Connectivity Test

+
+
+
+ +
+
+
+ + + {#if error} +
+
+
+ + {error} +
+
+
+ {/if} + + + {#if results} +
+
+

Connectivity Results

+ +
+
+ +
+
+ +
+

{results.dualStack ? 'Dual-Stack Available' : 'Single Protocol Only'}

+

+ {results.dualStack + ? 'Both IPv4 and IPv6 connectivity are available' + : results.ipv4.success + ? 'Only IPv4 connectivity is available' + : results.ipv6.success + ? 'Only IPv6 connectivity is available' + : 'No connectivity detected'} +

+
+
+
+ + +
+ +
+

+ + IPv4 Connectivity +

+
+ + {results.ipv4.success ? 'Connected' : 'Not Available'} +
+ {#if results.ipv4.success} +
+
+ +
+ IP Address + {results.ipv4.ip} +
+
+
+ +
+ Latency + {results.ipv4.latency}ms +
+
+
+ {:else if results.ipv4.error && results.ipv4.error !== 'fetch failed'} +
{results.ipv4.error}
+ {:else} +
No IPv4 connectivity available
+ {/if} +
+ + +
+

+ + IPv6 Connectivity +

+
+ + {results.ipv6.success ? 'Connected' : 'Not Available'} +
+ {#if results.ipv6.success} +
+
+ +
+ IP Address + {results.ipv6.ip} +
+
+
+ +
+ Latency + {results.ipv6.latency}ms +
+
+
+ {:else if results.ipv6.error && results.ipv6.error !== 'fetch failed'} +
{results.ipv6.error}
+ {:else} +
No IPv6 connectivity available
+ {/if} +
+ + + {#if results.dualStack} +
+

+ + Connection Summary +

+
+
+ +
+ Dual-Stack + Enabled +
+
+ {#if results.preferredProtocol} +
+ +
+ Preferred Protocol + {results.preferredProtocol} +
+
+
+ + Based on latency comparison +
+ {/if} +
+ +
+ Tested At + {new Date(results.timestamp).toLocaleString()} +
+
+
+
+ {/if} +
+
+
+ {/if} + + +
+
+

About IPv6 Connectivity

+
+
+
+

{content.sections.whatIsIPv6.title}

+

{content.sections.whatIsIPv6.content}

+
+ +
+ +
+

{content.sections.dualStack.title}

+

{content.sections.dualStack.content}

+
    + {#each content.sections.dualStack.benefits as { benefit, description } (benefit)} +
  • {benefit}: {description}
  • + {/each} +
+
+ +
+ +
+

{content.sections.ipv6Advantages.title}

+
    + {#each content.sections.ipv6Advantages.advantages as { advantage, description } (advantage)} +
  • {advantage}: {description}
  • + {/each} +
+
+ +
+ +
+

Quick Tips

+
    + {#each content.quickTips as tip (tip)} +
  • {tip}
  • + {/each} +
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/network/tcp-port-check/+page.svelte b/src/routes/[lang]/diagnostics/network/tcp-port-check/+page.svelte new file mode 100644 index 00000000..64fa23d2 --- /dev/null +++ b/src/routes/[lang]/diagnostics/network/tcp-port-check/+page.svelte @@ -0,0 +1,606 @@ + + +
+
+

{$t('diagnostics/network-tcp-port-check.title')}

+

{$t('diagnostics/network-tcp-port-check.subtitle')}

+
+ + + ex.description} + getDescription={(ex) => { + const targets = ex.targets.split('\n'); + const preview = targets.slice(0, 3).join(', '); + return targets.length > 3 ? `${preview} (+${targets.length - 3} more)` : preview; + }} + getTooltip={(ex) => `Test ports: ${ex.targets.split('\n').join(', ')}`} + /> + + +
+
+

{$t('diagnostics/network-tcp-port-check.form.title')}

+
+
+
+
+ +
+
+ + +
+
+

{$t('diagnostics/network-tcp-port-check.form.commonPortsTitle')}

+
+ {#each commonPorts as port, index (index)} + + {/each} +
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('diagnostics/network-tcp-port-check.results.title')}

+ +
+
+ +
+
+ +
+ {$t('diagnostics/network-tcp-port-check.results.summaryOpenPorts', { + count: diagnosticState.results.summary.open, + })} +

{$t('diagnostics/network-tcp-port-check.results.openPortsDescription')}

+
+
+
+ +
+ {$t('diagnostics/network-tcp-port-check.results.summaryClosedPorts', { + count: diagnosticState.results.summary.closed, + })} +

{$t('diagnostics/network-tcp-port-check.results.closedPortsDescription')}

+
+
+ {#if diagnosticState.results.summary.avgLatency} +
+ +
+ {$t('diagnostics/network-tcp-port-check.results.summaryAvgLatency', { + latency: diagnosticState.results.summary.avgLatency, + })} +

{$t('diagnostics/network-tcp-port-check.results.avgLatencyDescription')}

+
+
+ {/if} +
+ + +
+

+ {$t('diagnostics/network-tcp-port-check.results.portStatusTitle', { + count: diagnosticState.results.results.length, + })} +

+
+ {#each diagnosticState.results.results as result, index (index)} + {@const status = getPortStatus(result)} +
+
+
+ + {result.host}:{result.port} +
+ {status.text} +
+ {#if result.error && !result.open} +
+ {result.error} +
+ {/if} +
+ {/each} +
+
+
+
+ {/if} + + + + +
+
+

{$t('diagnostics/network-tcp-port-check.info.title')}

+
+
+
+
+

{$t('diagnostics/network-tcp-port-check.info.portStates.title')}

+
    +
  • + {$t('diagnostics/network-tcp-port-check.info.portStates.open')} + {$t('diagnostics/network-tcp-port-check.info.portStates.openDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.portStates.closed')} + {$t('diagnostics/network-tcp-port-check.info.portStates.closedDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.portStates.filtered')} + {$t('diagnostics/network-tcp-port-check.info.portStates.filteredDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.portStates.timeout')} + {$t('diagnostics/network-tcp-port-check.info.portStates.timeoutDesc')} +
  • +
+
+ +
+

{$t('diagnostics/network-tcp-port-check.info.commonPorts.title')}

+
    +
  • + {$t('diagnostics/network-tcp-port-check.info.commonPorts.ssh')} + {$t('diagnostics/network-tcp-port-check.info.commonPorts.sshDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.commonPorts.http')} + {$t('diagnostics/network-tcp-port-check.info.commonPorts.httpDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.commonPorts.https')} + {$t('diagnostics/network-tcp-port-check.info.commonPorts.httpsDesc')} +
  • +
  • + {$t('diagnostics/network-tcp-port-check.info.commonPorts.smtp')} + {$t('diagnostics/network-tcp-port-check.info.commonPorts.smtpDesc')} +
  • +
+
+ +
+

{$t('diagnostics/network-tcp-port-check.info.troubleshooting.title')}

+
    +
  • {$t('diagnostics/network-tcp-port-check.info.troubleshooting.tip1')}
  • +
  • {$t('diagnostics/network-tcp-port-check.info.troubleshooting.tip2')}
  • +
  • {$t('diagnostics/network-tcp-port-check.info.troubleshooting.tip3')}
  • +
  • {$t('diagnostics/network-tcp-port-check.info.troubleshooting.tip4')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/rdap/asn/+page.svelte b/src/routes/[lang]/diagnostics/rdap/asn/+page.svelte new file mode 100644 index 00000000..3245065c --- /dev/null +++ b/src/routes/[lang]/diagnostics/rdap/asn/+page.svelte @@ -0,0 +1,401 @@ + + +
+
+

{$t('diagnostics/rdap-asn.title')}

+

{$t('diagnostics/rdap-asn.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/rdap-asn.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/rdap-asn.form.title')}

+
+
+
+
+ +
+
+ +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics/rdap-asn.results.title', { asn: formatASN(results.asn) })}

+ +
+
+
+
+ {$t('diagnostics/rdap-asn.results.asn')}: + + {formatASN(results.asn)} + +
+
+ {$t('diagnostics/rdap-asn.results.rdapService')}: + {results.serviceUrl} +
+
+ +
+ +
+

{$t('diagnostics/rdap-asn.results.asnInfo.title')}

+
+
{$t('diagnostics/rdap-asn.results.asnInfo.organizationName')}
+
{results.data.name || $t('diagnostics/rdap-asn.results.notAvailable')}
+ +
{$t('diagnostics/rdap-asn.results.asnInfo.type')}
+
{results.data.type || $t('diagnostics/rdap-asn.results.notAvailable')}
+ +
{$t('diagnostics/rdap-asn.results.asnInfo.country')}
+
+ {#if results.data.country} + {results.data.country} + {:else} + {$t('diagnostics/rdap-asn.results.notAvailable')} + {/if} +
+ +
{$t('diagnostics/rdap-asn.results.asnInfo.registry')}
+
{results.data.registry || $t('diagnostics/rdap-asn.results.notAvailable')}
+
+
+ + +
+

{$t('diagnostics/rdap-asn.results.allocation.title')}

+
+
{$t('diagnostics/rdap-asn.results.allocation.status')}
+
+ {#if results.data.status?.length} +
+ {#each results.data.status as status, index (index)} + {status} + {/each} +
+ {:else} + {$t('diagnostics/rdap-asn.results.notAvailable')} + {/if} +
+ +
{$t('diagnostics/rdap-asn.results.allocation.allocationDate')}
+
{formatDate(results.data.allocation)}
+ +
{$t('diagnostics/rdap-asn.results.allocation.lastChanged')}
+
{formatDate(results.data.lastChanged)}
+
+
+ + + {#if results.data.contacts?.length} +
+

{$t('diagnostics/rdap-asn.results.contacts.title')}

+
+ {#each results.data.contacts as contact, index (index)} +
+
+ {#if contact.roles?.includes('registrant')} + {$t('diagnostics/rdap-asn.results.contacts.registrant')} + {:else if contact.roles?.includes('administrative')} + {$t('diagnostics/rdap-asn.results.contacts.administrative')} + {:else if contact.roles?.includes('technical')} + {$t('diagnostics/rdap-asn.results.contacts.technical')} + {:else if contact.roles?.includes('abuse')} + {$t('diagnostics/rdap-asn.results.contacts.abuse')} + {:else} + {$t('diagnostics/rdap-asn.results.contacts.contact')} + {/if} +
+

{formatContact(contact)}

+ {#if contact.handle} +

+ {$t('diagnostics/rdap-asn.results.contacts.handle', { handle: contact.handle })} +

+ {/if} + {#if contact.roles} +
+ {#each contact.roles as role, index (index)} + {role} + {/each} +
+ {/if} +
+ {/each} +
+
+ {/if} +
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/rdap-asn.error.title')} +

{error}

+
+

{$t('diagnostics/rdap-asn.error.troubleshooting.title')}

+
    +
  • {$t('diagnostics/rdap-asn.error.troubleshooting.validFormat')}
  • +
  • {$t('diagnostics/rdap-asn.error.troubleshooting.asnExists')}
  • +
  • {$t('diagnostics/rdap-asn.error.troubleshooting.privateASN')}
  • +
  • {$t('diagnostics/rdap-asn.error.troubleshooting.rateLimiting')}
  • +
  • {$t('diagnostics/rdap-asn.error.troubleshooting.tryAgain')}
  • +
+
+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/rdap-asn.educational.title')}

+
+
+
+
+

{$t('diagnostics/rdap-asn.educational.whatIsASN.title')}

+

{$t('diagnostics/rdap-asn.educational.whatIsASN.description')}

+
+ +
+

{$t('diagnostics/rdap-asn.educational.asnRanges.title')}

+
    +
  • {$t('diagnostics/rdap-asn.educational.asnRanges.arin')}
  • +
  • {$t('diagnostics/rdap-asn.educational.asnRanges.ripe')}
  • +
  • {$t('diagnostics/rdap-asn.educational.asnRanges.apnic')}
  • +
  • {$t('diagnostics/rdap-asn.educational.asnRanges.lacnic')}
  • +
  • {$t('diagnostics/rdap-asn.educational.asnRanges.afrinic')}
  • +
+
+ +
+

{$t('diagnostics/rdap-asn.educational.whatYouGet.title')}

+
    +
  • {$t('diagnostics/rdap-asn.educational.whatYouGet.organization')}
  • +
  • {$t('diagnostics/rdap-asn.educational.whatYouGet.country')}
  • +
  • {$t('diagnostics/rdap-asn.educational.whatYouGet.allocation')}
  • +
  • {$t('diagnostics/rdap-asn.educational.whatYouGet.contacts')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/rdap/domain/+page.svelte b/src/routes/[lang]/diagnostics/rdap/domain/+page.svelte new file mode 100644 index 00000000..63f098db --- /dev/null +++ b/src/routes/[lang]/diagnostics/rdap/domain/+page.svelte @@ -0,0 +1,383 @@ + + +
+
+

{$t('diagnostics/rdap-domain.title')}

+

{$t('diagnostics/rdap-domain.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/rdap-domain.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/rdap-domain.form.title')}

+
+
+
+
+ +
+
+ +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics/rdap-domain.results.title', { domain: results.domain })}

+ +
+
+
+
+ {$t('diagnostics/rdap-domain.results.domain')}: + {results.data.domain || results.domain} +
+
+ {$t('diagnostics/rdap-domain.results.rdapService')}: + {results.serviceUrl} +
+
+ +
+ +
+

{$t('diagnostics/rdap-domain.results.domainInfo.title')}

+
+
{$t('diagnostics/rdap-domain.results.domainInfo.domainName')}
+
{results.data.domain || results.domain}
+ +
{$t('diagnostics/rdap-domain.results.domainInfo.status')}
+
+ {#if results.data.status?.length} +
+ {#each results.data.status as status, index (index)} + {status} + {/each} +
+ {:else} + {$t('diagnostics/rdap-domain.results.notAvailable')} + {/if} +
+ +
{$t('diagnostics/rdap-domain.results.domainInfo.registrar')}
+
{results.data.registrar || $t('diagnostics/rdap-domain.results.notAvailable')}
+
+
+ + +
+

{$t('diagnostics/rdap-domain.results.dates.title')}

+
+
{$t('diagnostics/rdap-domain.results.dates.registration')}
+
{formatDate(results.data.created)}
+ +
{$t('diagnostics/rdap-domain.results.dates.lastUpdated')}
+
{formatDate(results.data.updated)}
+ +
{$t('diagnostics/rdap-domain.results.dates.expiration')}
+
+ {formatDate(results.data.expires)} + {#if results.data.expires && new Date(results.data.expires) < new Date(Date.now() + 30 * 24 * 60 * 60 * 1000)} + {$t('diagnostics/rdap-domain.results.dates.expiresSoon')} + {/if} +
+
+
+ + + {#if results.data.nameservers?.length} +
+

+ {$t('diagnostics/rdap-domain.results.nameservers.title', { count: results.data.nameservers.length })} +

+
    + {#each results.data.nameservers as ns, index (index)} +
  • {ns}
  • + {/each} +
+
+ {/if} + + + {#if results.data.contacts?.length} +
+

{$t('diagnostics/rdap-domain.results.contacts.title')}

+
+ {#each results.data.contacts as contact, index (index)} +
+
+ {#if contact.roles?.includes('registrant')} + {$t('diagnostics/rdap-domain.results.contacts.registrant')} + {:else if contact.roles?.includes('administrative')} + {$t('diagnostics/rdap-domain.results.contacts.administrative')} + {:else if contact.roles?.includes('technical')} + {$t('diagnostics/rdap-domain.results.contacts.technical')} + {:else} + {$t('diagnostics/rdap-domain.results.contacts.contact')} + {/if} +
+

{formatContact(contact)}

+ {#if contact.handle} +

+ {$t('diagnostics/rdap-domain.results.contacts.handle', { handle: contact.handle })} +

+ {/if} +
+ {/each} +
+
+ {/if} +
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/rdap-domain.error.title')} +

{error}

+
+

{$t('diagnostics/rdap-domain.error.troubleshooting.title')}

+
    +
  • {$t('diagnostics/rdap-domain.error.troubleshooting.validDomain')}
  • +
  • {$t('diagnostics/rdap-domain.error.troubleshooting.domainExists')}
  • +
  • {$t('diagnostics/rdap-domain.error.troubleshooting.rateLimiting')}
  • +
  • {$t('diagnostics/rdap-domain.error.troubleshooting.tryAgain')}
  • +
+
+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/rdap-domain.educational.title')}

+
+
+
+
+

{$t('diagnostics/rdap-domain.educational.whatIsRDAP.title')}

+

{$t('diagnostics/rdap-domain.educational.whatIsRDAP.description')}

+
+ +
+

{$t('diagnostics/rdap-domain.educational.whatYouGet.title')}

+
    +
  • {$t('diagnostics/rdap-domain.educational.whatYouGet.statusDates')}
  • +
  • {$t('diagnostics/rdap-domain.educational.whatYouGet.nameservers')}
  • +
  • {$t('diagnostics/rdap-domain.educational.whatYouGet.registrar')}
  • +
  • {$t('diagnostics/rdap-domain.educational.whatYouGet.contacts')}
  • +
+
+ +
+

{$t('diagnostics/rdap-domain.educational.rdapVsWhois.title')}

+
    +
  • {$t('diagnostics/rdap-domain.educational.rdapVsWhois.structuredJSON')}
  • +
  • {$t('diagnostics/rdap-domain.educational.rdapVsWhois.unicodeSupport')}
  • +
  • {$t('diagnostics/rdap-domain.educational.rdapVsWhois.rateLimiting')}
  • +
  • {$t('diagnostics/rdap-domain.educational.rdapVsWhois.restfulAPI')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/rdap/ip/+page.svelte b/src/routes/[lang]/diagnostics/rdap/ip/+page.svelte new file mode 100644 index 00000000..e258a4c5 --- /dev/null +++ b/src/routes/[lang]/diagnostics/rdap/ip/+page.svelte @@ -0,0 +1,400 @@ + + +
+
+

{$t('diagnostics/rdap-ip.title')}

+

{$t('diagnostics/rdap-ip.subtitle')}

+
+ + +
+
+ + +

{$t('diagnostics/rdap-ip.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + +
+
+

{$t('diagnostics/rdap-ip.form.title')}

+
+
+
+
+ +
+
+ +
+ +
+
+
+ + + {#if results} +
+
+

{$t('diagnostics/rdap-ip.results.title', { ip: results.ip })}

+ +
+
+
+
+ {$t('diagnostics/rdap-ip.results.ipAddress')}: + + {results.ip} + {getIPVersion(results.ip)} + +
+
+ {$t('diagnostics/rdap-ip.results.rdapService')}: + {results.serviceUrl} +
+
+ +
+ +
+

{$t('diagnostics/rdap-ip.results.networkInfo.title')}

+
+
{$t('diagnostics/rdap-ip.results.networkInfo.networkBlock')}
+
{results.data.network || $t('diagnostics/rdap-ip.results.notAvailable')}
+ +
{$t('diagnostics/rdap-ip.results.networkInfo.networkName')}
+
{results.data.name || $t('diagnostics/rdap-ip.results.notAvailable')}
+ +
{$t('diagnostics/rdap-ip.results.networkInfo.type')}
+
{results.data.type || $t('diagnostics/rdap-ip.results.notAvailable')}
+ +
{$t('diagnostics/rdap-ip.results.networkInfo.country')}
+
+ {#if results.data.country} + {results.data.country} + {:else} + {$t('diagnostics/rdap-ip.results.notAvailable')} + {/if} +
+ +
{$t('diagnostics/rdap-ip.results.networkInfo.registry')}
+
{results.data.registry || $t('diagnostics/rdap-ip.results.notAvailable')}
+
+
+ + +
+

{$t('diagnostics/rdap-ip.results.allocation.title')}

+
+
{$t('diagnostics/rdap-ip.results.allocation.status')}
+
+ {#if results.data.status?.length} +
+ {#each results.data.status as status, index (index)} + {status} + {/each} +
+ {:else} + {$t('diagnostics/rdap-ip.results.notAvailable')} + {/if} +
+ +
{$t('diagnostics/rdap-ip.results.allocation.allocationDate')}
+
{formatDate(results.data.allocation)}
+ +
{$t('diagnostics/rdap-ip.results.allocation.lastChanged')}
+
{formatDate(results.data.lastChanged)}
+
+
+ + + {#if results.data.contacts?.length} +
+

{$t('diagnostics/rdap-ip.results.contacts.title')}

+
+ {#each results.data.contacts as contact, index (index)} +
+
+ {#if contact.roles?.includes('registrant')} + {$t('diagnostics/rdap-ip.results.contacts.registrant')} + {:else if contact.roles?.includes('administrative')} + {$t('diagnostics/rdap-ip.results.contacts.administrative')} + {:else if contact.roles?.includes('technical')} + {$t('diagnostics/rdap-ip.results.contacts.technical')} + {:else if contact.roles?.includes('abuse')} + {$t('diagnostics/rdap-ip.results.contacts.abuse')} + {:else} + {$t('diagnostics/rdap-ip.results.contacts.contact')} + {/if} +
+

{formatContact(contact)}

+ {#if contact.handle} +

+ {$t('diagnostics/rdap-ip.results.contacts.handle', { handle: contact.handle })} +

+ {/if} + {#if contact.roles} +
+ {#each contact.roles as role, index (index)} + {role} + {/each} +
+ {/if} +
+ {/each} +
+
+ {/if} +
+
+
+ {/if} + + {#if error} +
+
+
+ +
+ {$t('diagnostics/rdap-ip.error.title')} +

{error}

+
+

{$t('diagnostics/rdap-ip.error.troubleshooting.title')}

+
    +
  • {$t('diagnostics/rdap-ip.error.troubleshooting.validIP')}
  • +
  • {$t('diagnostics/rdap-ip.error.troubleshooting.privateIP')}
  • +
  • {$t('diagnostics/rdap-ip.error.troubleshooting.rateLimiting')}
  • +
  • {$t('diagnostics/rdap-ip.error.troubleshooting.specialUse')}
  • +
  • {$t('diagnostics/rdap-ip.error.troubleshooting.tryAgain')}
  • +
+
+
+
+
+
+ {/if} + + +
+
+

{$t('diagnostics/rdap-ip.educational.title')}

+
+
+
+
+

{$t('diagnostics/rdap-ip.educational.howItWorks.title')}

+

{$t('diagnostics/rdap-ip.educational.howItWorks.description')}

+
+ +
+

{$t('diagnostics/rdap-ip.educational.rirs.title')}

+
    +
  • {$t('diagnostics/rdap-ip.educational.rirs.arin')}
  • +
  • {$t('diagnostics/rdap-ip.educational.rirs.ripe')}
  • +
  • {$t('diagnostics/rdap-ip.educational.rirs.apnic')}
  • +
  • {$t('diagnostics/rdap-ip.educational.rirs.lacnic')}
  • +
  • {$t('diagnostics/rdap-ip.educational.rirs.afrinic')}
  • +
+
+ +
+

{$t('diagnostics/rdap-ip.educational.whatYouGet.title')}

+
    +
  • {$t('diagnostics/rdap-ip.educational.whatYouGet.networkBlock')}
  • +
  • {$t('diagnostics/rdap-ip.educational.whatYouGet.allocationType')}
  • +
  • {$t('diagnostics/rdap-ip.educational.whatYouGet.organization')}
  • +
  • {$t('diagnostics/rdap-ip.educational.whatYouGet.contacts')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/tls/alpn/+page.svelte b/src/routes/[lang]/diagnostics/tls/alpn/+page.svelte new file mode 100644 index 00000000..d6d76f32 --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/alpn/+page.svelte @@ -0,0 +1,671 @@ + + +
+
+

{$t('diagnostics.alpn.title')}

+

+ {$t('diagnostics.alpn.description')} +

+
+ + + ex.host} + getDescription={(ex) => ex.description} + getTooltip={(ex) => $t('diagnostics.alpn.examples.tooltip', { host: ex.host, description: ex.description })} + /> + + +
+
+

{$t('diagnostics.alpn.form.title')}

+
+
+
+
+ +
+
+ + +
+
+ +
+ {$t('diagnostics.alpn.form.quickSelect')} + {#each commonProtocols as preset, index (index)} + + {/each} +
+
+
+ +
+
+ + {#if useCustomServername} + { + examples.clear(); + if (isInputValid()) probeALPN(); + }} + /> + {/if} +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('diagnostics.alpn.results.title')}

+ +
+
+ + {#if diagnosticState.results} + {@const status = getNegotiationStatus()} +
+
+ +
+ {$t('diagnostics.alpn.results.negotiationStatus', { status: status.status })} +

{status.description}

+
+
+ {#if diagnosticState.results.tlsVersion} +
+ +
+ {$t('diagnostics.alpn.results.tlsVersion', { version: diagnosticState.results.tlsVersion })} +

{$t('diagnostics.alpn.results.connectionEstablished')}

+
+
+ {/if} +
+ {/if} + + +
+

{$t('diagnostics.alpn.results.protocolDetails')}

+ +
+
+
{$t('diagnostics.alpn.results.requestedProtocols')}
+
+ {#each diagnosticState.results.requestedProtocols as protocol, i (i)} + {@const protocolInfo = getProtocolInfo(protocol)} +
+
+ {protocolInfo.name} + ({protocol}) + {$t('diagnostics.alpn.results.priority', { priority: i + 1 })} +
+

{protocolInfo.description}

+
+ {/each} +
+
+ + {#if diagnosticState.results.negotiatedProtocol} + {@const selectedProtocol = getProtocolInfo(diagnosticState.results.negotiatedProtocol)} +
+
{$t('diagnostics.alpn.results.selectedProtocol')}
+
+
+
+ + {selectedProtocol.name} + ({diagnosticState.results.negotiatedProtocol}) +
+

{selectedProtocol.description}

+
+
+
+ {:else} +
+
{$t('diagnostics.alpn.results.selectedProtocol')}
+
+ + {$t('diagnostics.alpn.results.noProtocolSelected')} +
+
+ {/if} +
+
+ + + {#if diagnosticState.results.servername || diagnosticState.results.tlsVersion} +
+

{$t('diagnostics.alpn.results.connectionInfo')}

+
+
+ {$t('diagnostics.alpn.results.serverName')} + {diagnosticState.results.servername} +
+ {#if diagnosticState.results.tlsVersion} +
+ {$t('diagnostics.alpn.results.tlsVersionLabel')} + {diagnosticState.results.tlsVersion} +
+ {/if} +
+
+ {/if} +
+
+ {/if} + + + + +
+
+

{$t('diagnostics.alpn.info.title')}

+
+
+
+
+

{$t('diagnostics.alpn.info.whatIsAlpn.title')}

+

+ {$t('diagnostics.alpn.info.whatIsAlpn.description')} +

+
+ +
+

{$t('diagnostics.alpn.info.commonProtocols.title')}

+
    +
  • h2: {$t('diagnostics.alpn.info.commonProtocols.h2')}
  • +
  • h3: {$t('diagnostics.alpn.info.commonProtocols.h3')}
  • +
  • http/1.1: {$t('diagnostics.alpn.info.commonProtocols.http11')}
  • +
  • spdy/3.1: {$t('diagnostics.alpn.info.commonProtocols.spdy')}
  • +
+
+ +
+

{$t('diagnostics.alpn.info.priority.title')}

+

+ {$t('diagnostics.alpn.info.priority.description')} +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/tls/banner/+page.svelte b/src/routes/[lang]/diagnostics/tls/banner/+page.svelte new file mode 100644 index 00000000..5a6a77cd --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/banner/+page.svelte @@ -0,0 +1,605 @@ + + +
+
+

{$t('diagnostics/tls-banner.title')}

+

{$t('diagnostics/tls-banner.subtitle')}

+
+ + + ex.description} + getDescription={(ex) => `${ex.host}:${ex.port}`} + getTooltip={(ex) => $t('diagnostics/tls-banner.examples.tooltip', { host: ex.host, port: ex.port })} + /> + + +
+
+

{$t('diagnostics/tls-banner.form.title')}

+
+
+
+
+ + +
+
+ +
+
+ + examples.clear()} + onkeydown={(e) => e.key === 'Enter' && grabBanner()} + /> +
+
+ + examples.clear()} + onkeydown={(e) => e.key === 'Enter' && grabBanner()} + /> +
+
+ + +
+
+ + + + {#if diagnosticState.loading} +
+
+
+ +
+

{$t('diagnostics/tls-banner.loading.title')}

+

{$t('diagnostics/tls-banner.loading.message', { host, port: port || 0 })}

+
+
+
+
+ {/if} + + {#if diagnosticState.results} +
+
+

{$t('diagnostics/tls-banner.results.title')}

+
+
+ +
+
+

{$t('diagnostics/tls-banner.results.connectionDetails.title')}

+
+
+
+
+ {$t('diagnostics/tls-banner.results.connectionDetails.host')} + {diagnosticState.results.host} +
+
+ {$t('diagnostics/tls-banner.results.connectionDetails.port')} + {diagnosticState.results.port} +
+
+ {$t('diagnostics/tls-banner.results.connectionDetails.protocol')} + + + {diagnosticState.results.protocol || $t('diagnostics/tls-banner.results.connectionDetails.unknown')} + +
+
+ {$t('diagnostics/tls-banner.results.connectionDetails.responseTime')} + {diagnosticState.results.responseTime}ms +
+
+
+
+ + + + + + {#if diagnosticState.results.analysis && (diagnosticState.results.analysis.software || diagnosticState.results.analysis.version || diagnosticState.results.analysis.os || (diagnosticState.results.analysis.security && diagnosticState.results.analysis.security.length > 0))} +
+
+

{$t('diagnostics/tls-banner.results.analysis.title')}

+
+
+
+ {#if diagnosticState.results.analysis.software} +
+ +
+

{$t('diagnostics/tls-banner.results.analysis.software')}

+

{diagnosticState.results.analysis.software}

+
+
+ {/if} + {#if diagnosticState.results.analysis.version} +
+ +
+

{$t('diagnostics/tls-banner.results.analysis.version')}

+

{diagnosticState.results.analysis.version}

+
+
+ {/if} + {#if diagnosticState.results.analysis.os} +
+ +
+

{$t('diagnostics/tls-banner.results.analysis.os')}

+

{diagnosticState.results.analysis.os}

+
+
+ {/if} + {#if diagnosticState.results.analysis.security && diagnosticState.results.analysis.security.length > 0} +
+ +
+

{$t('diagnostics/tls-banner.results.analysis.security')}

+
    + {#each diagnosticState.results.analysis.security as note, i (i)} +
  • {note}
  • + {/each} +
+
+
+ {/if} +
+
+
+ {/if} + + + {#if diagnosticState.results.tls} +
+
+

{$t('diagnostics/tls-banner.results.tls.title')}

+
+
+
+
+ {$t('diagnostics/tls-banner.results.tls.protocol')} + {diagnosticState.results.tls.protocol} +
+
+ {$t('diagnostics/tls-banner.results.tls.cipher')} + {diagnosticState.results.tls.cipher} +
+ {#if diagnosticState.results.tls.certificate} +
+ {$t('diagnostics/tls-banner.results.tls.certificateCN')} + {diagnosticState.results.tls.certificate.cn} +
+ {/if} +
+
+
+ {/if} +
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/tls/certificate/+page.svelte b/src/routes/[lang]/diagnostics/tls/certificate/+page.svelte new file mode 100644 index 00000000..f7b0300b --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/certificate/+page.svelte @@ -0,0 +1,512 @@ + + +
+
+

{$t('title')}

+

+ {$t('description')} +

+
+ + + example.host} + getDescription={(example) => example.description} + getTooltip={(example) => $t('examplesSection.tooltip', { host: example.host, description: example.description })} + /> + + +
+
+

{$t('form.title')}

+
+
+
+
+ +
+
+ +
+
+ + {#if useCustomServername} + { + examples.clear(); + if (isInputValid()) analyzeCertificate(); + }} + /> + {/if} +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('results.title')}

+ +
+
+ + {#if diagnosticState.results.peerCertificate} + {@const cert = diagnosticState.results.peerCertificate} + {@const expiryStatus = getExpiryStatus(cert)} + +
+
+
+ + {expiryStatus.status} +
+
+ + {cert.isNotYetValid ? $t('results.status.notYetValid') : $t('results.status.currentlyValid')} +
+
+ + +
+
+

{$t('results.certificate.title')}

+
+
+ {$t('results.certificate.commonName')} + {cert.subject.CN} +
+
+ {$t('results.certificate.organization')} + {cert.subject.O || $t('results.certificate.na')} +
+
+ {$t('results.certificate.issuer')} + {cert.issuer.CN} +
+
+ {$t('results.certificate.serialNumber')} + {cert.serialNumber} +
+
+ {$t('results.certificate.validFrom')} + {new Date(cert.validFrom).toLocaleString()} +
+
+ {$t('results.certificate.validTo')} + {new Date(cert.validTo).toLocaleString()} +
+
+
+ + + {#if cert.subjectAltNames?.length > 0} +
+

{$t('results.san.title')}

+
+ {#each cert.subjectAltNames as san, index (index)} + {san} + {/each} +
+
+ {/if} + + +
+

{$t('results.fingerprints.title')}

+
+
+ {$t('results.fingerprints.sha1')} + {cert.fingerprint} +
+
+ {$t('results.fingerprints.sha256')} + {cert.fingerprint256} +
+
+
+
+
+ {/if} + + + {#if diagnosticState.results.chain?.length > 0} +
+

{$t('results.chain.title', { count: diagnosticState.results.chain.length })}

+
+ {#each diagnosticState.results.chain as chainCert, i (i)} +
+
+ {$t('results.chain.level', { level: i })} + {chainCert.subject.CN} +
+
+ {$t('results.chain.issuer', { issuer: chainCert.issuer.CN })} + {$t('results.chain.expires', { date: new Date(chainCert.validTo).toLocaleDateString() })} +
+
+ {/each} +
+
+ {/if} + + + {#if diagnosticState.results.protocol || diagnosticState.results.cipher || diagnosticState.results.alpnProtocol} +
+

{$t('results.connection.title')}

+
+ {#if diagnosticState.results.protocol} +
+ {$t('results.connection.tlsVersion')} + {diagnosticState.results.protocol} +
+ {/if} + {#if diagnosticState.results.cipher} +
+ {$t('results.connection.cipherSuite')} + {diagnosticState.results.cipher.name} +
+ {/if} + {#if diagnosticState.results.alpnProtocol} +
+ {$t('results.connection.alpnProtocol')} + {diagnosticState.results.alpnProtocol} +
+ {/if} +
+
+ {/if} +
+
+ {/if} + + +
+ + diff --git a/src/routes/[lang]/diagnostics/tls/cipher-presets/+page.svelte b/src/routes/[lang]/diagnostics/tls/cipher-presets/+page.svelte new file mode 100644 index 00000000..e237c028 --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/cipher-presets/+page.svelte @@ -0,0 +1,581 @@ + + +
+
+

TLS Cipher Presets

+

Probe connectivity with preset cipher lists (modern/intermediate/legacy)

+
+ + + `${ex.host}:${ex.port}`} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Test cipher presets for ${ex.host}:${ex.port}`} + /> + + +
+
+

Cipher Presets Configuration

+
+
+
+ +
+ examples.clear()} + onkeydown={(e) => e.key === 'Enter' && testCiphers()} + class="flex-grow" + /> + examples.clear()} + onkeydown={(e) => e.key === 'Enter' && testCiphers()} + class="port-input" + /> + +
+
+
+
+ + + + {#if diagnosticState.loading} +
+
+
+ +
+

Testing Cipher Presets

+

Testing modern, intermediate, and legacy cipher suites...

+
+
+
+
+ {/if} + + {#if diagnosticState.results} +
+
+

Cipher Presets Results

+
+
+
+
+ {#each diagnosticState.results.presets as preset (preset.name)} +
+
+
+

{preset.name}

+ {preset.level} +
+
+ {getPresetGrade(preset)} +
+
+ +
+ {preset.description} +
+ +
+
+ Supported: + {preset.supportedCiphers.length}/{preset.ciphers.length} +
+
+ Coverage: + {getPresetScore(preset)}% +
+
+ +
+
+
+
+
+ + {#if preset.protocols} +
+ Protocols: +
+ {#each preset.protocols as protocol (protocol.name)} + + {protocol.name} + + {/each} +
+
+ {/if} + + {#if preset.supportedCiphers.length > 0} +
+ Supported Ciphers ({preset.supportedCiphers.length}) +
+ {#each preset.supportedCiphers as cipher (cipher)} +
+ + {cipher} +
+ {/each} +
+
+ {/if} + + {#if preset.unsupportedCiphers && preset.unsupportedCiphers.length > 0} +
+ Unsupported Ciphers ({preset.unsupportedCiphers.length}) +
+ {#each preset.unsupportedCiphers as cipher (cipher)} +
+ + {cipher} +
+ {/each} +
+
+ {/if} + + {#if preset.recommendation} +
+ + {preset.recommendation} +
+ {/if} +
+ {/each} +
+ + {#if diagnosticState.results.summary} +
+

Overall Assessment

+
+
+
+ {diagnosticState.results.summary.overallGrade} +
+
+

{diagnosticState.results.summary.rating}

+

{diagnosticState.results.summary.description}

+
+
+ + {#if diagnosticState.results.summary.recommendations && diagnosticState.results.summary.recommendations.length > 0} +
+

Recommendations

+
    + {#each diagnosticState.results.summary.recommendations as rec (rec)} +
  • {rec}
  • + {/each} +
+
+ {/if} +
+
+ {/if} +
+
+
+ {/if} +
+ + diff --git a/src/routes/[lang]/diagnostics/tls/ct-log-search/+page.svelte b/src/routes/[lang]/diagnostics/tls/ct-log-search/+page.svelte new file mode 100644 index 00000000..cc791a9a --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/ct-log-search/+page.svelte @@ -0,0 +1,812 @@ + + +
+
+

{content.title}

+

{content.description}

+
+ + +
+
+

Search CT Logs

+
+
+
+ e.key === 'Enter' && searchCTLogs()} + disabled={diagnosticState.loading} + /> + +
+
+
+ + + ex.domain} + getDescription={(ex) => ex.desc} + /> + + + {#if diagnosticState.loading} +
+
+
+ +
+

Searching Certificate Transparency Logs

+

Querying CT logs for {domain}...

+
+
+
+
+ {/if} + + + + + {#if diagnosticState.results} +
+
+

CT Log Results for {diagnosticState.results.domain}

+ +
+
+ +
+
+ +
+

{diagnosticState.results.totalCertificates} Total Certificates

+

Found in Certificate Transparency logs

+
+
+
+ + +
+ {#each statsConfig as stat (stat.key)} +
+

+ + {stat.label} +

+
+ {stat.isArray + ? (diagnosticState.results[stat.key as keyof CTLogResponse] as any[]).length + : diagnosticState.results[stat.key as keyof CTLogResponse]} +
+

{stat.desc}

+
+ {/each} +
+ + + {#if diagnosticState.results.discoveredHostnames.length > 0} +
+

+ + Discovered Hostnames ({diagnosticState.results.discoveredHostnames.length}) +

+
+ {#each diagnosticState.results.discoveredHostnames.slice(0, 50) as hostname (hostname)} + + {#if hostname.startsWith('*')} + + {/if} + {hostname} + + {/each} + {#if diagnosticState.results.discoveredHostnames.length > 50} + + +{diagnosticState.results.discoveredHostnames.length - 50} more + + {/if} +
+
+ {/if} + + + {#if diagnosticState.results.issuers.length > 0} +
+

+ + Certificate Issuers ({diagnosticState.results.issuers.length}) +

+
+ {#each diagnosticState.results.issuers.slice(0, 10) as issuer (issuer.name)} +
+ {issuer.name} + {issuer.count} +
+ {/each} +
+
+ {/if} + + +
+

+ + Certificates ({diagnosticState.results.certificates.length}) +

+
+ {#each diagnosticState.results.certificates.slice(0, 20) as cert (cert.id)} +
+
toggleCert(cert.id)} + onkeydown={(e) => e.key === 'Enter' && toggleCert(cert.id)} + > +
+ {#if cert.isWildcard} + + {/if} + {cert.commonName} + {#each getCertBadges(cert) as badge (badge.text)} + {badge.text} + {/each} +
+ +
+ + {#if expandedCert === cert.id} +
+
+ {#each certDetailFields as field (field.key)} +
+ +
+ {field.label}{field.isSans ? ` (${cert.sans.length})` : ''} + {#if field.isSans} +
+ {#each cert.sans.slice(0, 10) as san (san)} + {san} + {/each} + {#if cert.sans.length > 10} + +{cert.sans.length - 10} more + {/if} +
+ {:else if field.isLink} + + crt.sh #{cert.id} + + {:else if field.formatter} + {field.formatter(cert)} + {:else} + {cert[field.key as keyof ProcessedCertificate]} + {/if} +
+
+ {/each} +
+
+ {/if} +
+ {/each} + {#if diagnosticState.results.certificates.length > 20} +
+ Showing first 20 of {diagnosticState.results.certificates.length} certificates +
+ {/if} +
+
+
+
+ {/if} + + +
+
+

About Certificate Transparency

+
+
+ +
+ + +

{content.sections.whatIsCT.title}

+
+
+

{content.sections.whatIsCT.content}

+
+
+ + {#each [{ title: content.sections.benefits.title, items: content.sections.benefits.benefits, keys: ['benefit', 'description'] }, { title: content.sections.useCases.title, items: content.sections.useCases.cases, keys: ['useCase', 'description', 'example'] }, { title: content.sections.certificateFields.title, items: content.sections.certificateFields.fields, keys: ['field', 'description'] }, { title: content.sections.security.title, items: content.sections.security.points, keys: ['point', 'description'] }, { title: content.sections.bestPractices.title, items: content.sections.bestPractices.practices, keys: ['practice', 'description'] }] as section (section.title)} +
+ + +

{section.title}

+
+
+
    + {#each section.items as item ((item as any)[section.keys[0]])} +
  • + {(item as any)[section.keys[0]]}: + {(item as any)[section.keys[1]]} + {#if section.keys[2] && (item as any)[section.keys[2]]} + ({(item as any)[section.keys[2]]}) + {/if} +
  • + {/each} +
+
+
+ {/each} + + +
+ + +

Quick Tips

+
+
+
    + {#each content.quickTips as tip (tip)} +
  • {tip}
  • + {/each} +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/tls/handshake-analyzer/+page.svelte b/src/routes/[lang]/diagnostics/tls/handshake-analyzer/+page.svelte new file mode 100644 index 00000000..e5156ce0 --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/handshake-analyzer/+page.svelte @@ -0,0 +1,587 @@ + + +
+
+

{tlsHandshakeContent.title}

+

{tlsHandshakeContent.description}

+
+ + + ex.hostname} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Analyze ${ex.hostname}`} + /> + + +
+
+

Handshake Analysis

+
+
+
+
+ + { + examples.clear(); + if (hostname.trim()) analyzeHandshake(); + }} + /> +
+
+ + +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

+ Handshake Results for {diagnosticState.results.hostname}:{diagnosticState.results.port} +

+ +
+
+
+ +
+

Connection Summary

+
+
+ +
+ Total Time + {diagnosticState.results.totalTime}ms +
+
+
+ +
+ TLS Version + {diagnosticState.results.tlsVersion} +
+
+
+ +
+ Cipher Suite + {diagnosticState.results.cipherSuite} +
+
+ {#if diagnosticState.results.alpnProtocol} +
+ +
+ ALPN Protocol + {diagnosticState.results.alpnProtocol} +
+
+ {/if} +
+
+ + + {#if diagnosticState.results.certificateInfo} +
+

Certificate Information

+
+
+ +
+ Subject + {diagnosticState.results.certificateInfo.subject} +
+
+
+ +
+ Issuer + {diagnosticState.results.certificateInfo.issuer} +
+
+
+ +
+ Valid + + {new Date(diagnosticState.results.certificateInfo.validFrom).toLocaleDateString()} β†’ + {new Date(diagnosticState.results.certificateInfo.validTo).toLocaleDateString()} + +
+
+ {#if diagnosticState.results.certificateInfo.san} +
+ +
+ SANs + {diagnosticState.results.certificateInfo.san.length} domains +
+
+ {/if} +
+
+ {/if} + + +
+

Handshake Timeline

+
+ {#each diagnosticState.results.phases as phase (phase.phase)} +
+
+
+
+ {phase.phase} + {phase.duration}ms +
+
at {phase.timestamp}ms
+ {#if phase.details && Object.keys(phase.details).length > 0} +
+ {#each Object.entries(phase.details) as [key, value] (key)} + {key}: {value} + {/each} +
+ {/if} +
+
+ {/each} +
+
+
+
+
+ {/if} + + + + +
+
+

About TLS Handshakes

+
+
+
+
+

{tlsHandshakeContent.sections.whatIsHandshake.title}

+

{tlsHandshakeContent.sections.whatIsHandshake.content}

+
+ +
+

{tlsHandshakeContent.sections.tlsVersions.title}

+
    + {#each tlsHandshakeContent.sections.tlsVersions.versions as version (version.version)} +
  • + {version.version} ({version.status}): + {version.description} +
  • + {/each} +
+
+ +
+

{tlsHandshakeContent.sections.performanceFactors.title}

+
    + {#each tlsHandshakeContent.sections.performanceFactors.factors as factor (factor.factor)} +
  • + {factor.factor}: + {factor.description} +
  • + {/each} +
+
+ +
+

{tlsHandshakeContent.sections.optimization.title}

+
    + {#each tlsHandshakeContent.sections.optimization.techniques.slice(0, 3) as technique (technique.technique)} +
  • + {technique.technique}: + {technique.benefit} +
  • + {/each} +
+
+
+ +
+

Quick Tips

+
    + {#each tlsHandshakeContent.quickTips as tip, idx (idx)} +
  • {tip}
  • + {/each} +
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/tls/ocsp-stapling/+page.svelte b/src/routes/[lang]/diagnostics/tls/ocsp-stapling/+page.svelte new file mode 100644 index 00000000..c7d735df --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/ocsp-stapling/+page.svelte @@ -0,0 +1,616 @@ + + +
+
+

{$t('diagnostics.ocsp-stapling.title')}

+

{$t('diagnostics.ocsp-stapling.description')}

+
+ + + `${ex.host}:${ex.port}`} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `Check OCSP stapling for ${ex.host}:${ex.port}`} + /> + + +
+
+

{$t('diagnostics.ocsp-stapling.form.title')}

+
+
+
+ +
+ examples.clear()} + onkeydown={(e) => e.key === 'Enter' && checkOCSP()} + class="flex-grow" + /> + examples.clear()} + onkeydown={(e) => e.key === 'Enter' && checkOCSP()} + class="port-input" + /> + +
+
+
+
+ + + + {#if diagnosticState.loading} +
+
+
+ +
+

{$t('diagnostics.ocsp-stapling.loading.title')}

+

{$t('diagnostics.ocsp-stapling.loading.description')}

+
+
+
+
+ {/if} + + {#if diagnosticState.results} +
+
+

{$t('diagnostics.ocsp-stapling.results.title')}

+
+
+
+ +
+
+

{$t('diagnostics.ocsp-stapling.results.status.title')}

+
+
+ {#if diagnosticState.results.staplingEnabled} +
+ +
+

{$t('diagnostics.ocsp-stapling.results.status.enabled.title')}

+

{$t('diagnostics.ocsp-stapling.results.status.enabled.description')}

+
+
+ {:else} +
+ +
+

{$t('diagnostics.ocsp-stapling.results.status.disabled.title')}

+

{$t('diagnostics.ocsp-stapling.results.status.disabled.description')}

+
+
+ {/if} +
+
+ + + {#if diagnosticState.results.staplingEnabled && diagnosticState.results.ocspResponse} +
+
+

{$t('diagnostics.ocsp-stapling.results.details.title')}

+
+
+
+
+
{$t('diagnostics.ocsp-stapling.results.details.certificateStatus')}
+
+ + {diagnosticState.results.ocspResponse.certStatus} +
+
+ +
+
{$t('diagnostics.ocsp-stapling.results.details.responseStatus')}
+
+ + {diagnosticState.results.ocspResponse.responseStatus} +
+
+ + {#if diagnosticState.results.ocspResponse.thisUpdate} +
+
{$t('diagnostics.ocsp-stapling.results.details.thisUpdate')}
+
{formatDate(diagnosticState.results.ocspResponse.thisUpdate)}
+
+ {/if} + + {#if diagnosticState.results.ocspResponse.nextUpdate} +
+
{$t('diagnostics.ocsp-stapling.results.details.nextUpdate')}
+
{formatDate(diagnosticState.results.ocspResponse.nextUpdate)}
+
+ {/if} + + {#if diagnosticState.results.ocspResponse.producedAt} +
+
{$t('diagnostics.ocsp-stapling.results.details.producedAt')}
+
{formatDate(diagnosticState.results.ocspResponse.producedAt)}
+
+ {/if} + + {#if diagnosticState.results.ocspResponse.responderUrl} +
+
{$t('diagnostics.ocsp-stapling.results.details.responderUrl')}
+
{diagnosticState.results.ocspResponse.responderUrl}
+
+ {/if} +
+
+
+ + + {#if diagnosticState.results.ocspResponse.validity} +
+
+

{$t('diagnostics.ocsp-stapling.results.validity.title')}

+
+
+
+
+
+
{$t('diagnostics.ocsp-stapling.results.validity.validFor')}
+
{diagnosticState.results.ocspResponse.validity.validFor}
+
+ + {#if diagnosticState.results.ocspResponse.validity.expiresIn} +
+
{$t('diagnostics.ocsp-stapling.results.validity.expiresIn')}
+
+ + {diagnosticState.results.ocspResponse.validity.expiresIn} +
+
+ {/if} +
+ + {#if diagnosticState.results.ocspResponse.validity.percentage !== undefined} +
+
+ {$t('diagnostics.ocsp-stapling.results.validity.progressLabel')} + {diagnosticState.results.ocspResponse.validity.percentage}% +
+
+
+
+
+ {/if} +
+
+
+ {/if} + {/if} + + + {#if diagnosticState.results.certificate} +
+
+

{$t('diagnostics.ocsp-stapling.results.certificate.title')}

+
+
+
+
+
{$t('diagnostics.ocsp-stapling.results.certificate.subject')}
+
{diagnosticState.results.certificate.subject}
+
+ +
+
{$t('diagnostics.ocsp-stapling.results.certificate.issuer')}
+
{diagnosticState.results.certificate.issuer}
+
+ + {#if diagnosticState.results.certificate.ocspUrls && diagnosticState.results.certificate.ocspUrls.length > 0} +
+
{$t('diagnostics.ocsp-stapling.results.certificate.ocspUrls')}
+
+ {#each diagnosticState.results.certificate.ocspUrls as url (url)} +
{url}
+ {/each} +
+
+ {/if} +
+
+
+ {/if} + + + {#if diagnosticState.results.recommendations && diagnosticState.results.recommendations.length > 0} +
+
+

{$t('diagnostics.ocsp-stapling.results.recommendations.title')}

+
+
+
+ {#each diagnosticState.results.recommendations as rec (rec)} +
+ + {rec} +
+ {/each} +
+
+
+ {/if} +
+
+
+ {/if} + + +
+
+

{$t('diagnostics.ocsp-stapling.educational.title')}

+
+
+
+
+

{$t('diagnostics.ocsp-stapling.educational.whatIs.title')}

+

+ {$t('diagnostics.ocsp-stapling.educational.whatIs.description')} +

+
+ +
+

{$t('diagnostics.ocsp-stapling.educational.whyImportant.title')}

+
    +
  • {$t('diagnostics.ocsp-stapling.educational.whyImportant.privacy')}
  • +
  • {$t('diagnostics.ocsp-stapling.educational.whyImportant.performance')}
  • +
  • {$t('diagnostics.ocsp-stapling.educational.whyImportant.reliability')}
  • +
  • {$t('diagnostics.ocsp-stapling.educational.whyImportant.security')}
  • +
+
+ +
+

{$t('diagnostics.ocsp-stapling.educational.howItWorks.title')}

+

+ {$t('diagnostics.ocsp-stapling.educational.howItWorks.description')} +

+
+ +
+

{$t('diagnostics.ocsp-stapling.educational.checkingStatus.title')}

+

+ {$t('diagnostics.ocsp-stapling.educational.checkingStatus.description')} +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/diagnostics/tls/versions/+page.svelte b/src/routes/[lang]/diagnostics/tls/versions/+page.svelte new file mode 100644 index 00000000..de0e76d3 --- /dev/null +++ b/src/routes/[lang]/diagnostics/tls/versions/+page.svelte @@ -0,0 +1,581 @@ + + +
+
+

{$t('diagnostics.tls-versions.title')}

+

+ {$t('diagnostics.tls-versions.description')} +

+
+ + + ex.host} + getDescription={(ex) => ex.description} + getTooltip={(ex) => `${$t('diagnostics.tls-versions.form.probe')} ${ex.host} (${ex.description})`} + /> + + +
+
+

{$t('diagnostics.tls-versions.form.title')}

+
+
+
+
+ +
+
+ +
+
+ + {#if useCustomServername} + { + examples.clear(); + if (isInputValid()) probeTLSVersions(); + }} + /> + {/if} +
+
+ +
+ +
+
+
+ + + {#if diagnosticState.results} +
+
+

{$t('diagnostics.tls-versions.results.title')}

+ +
+
+ + {#if diagnosticState.results.supported} + {@const security = getOverallSecurity()} + +
+
+
+ +
+ {$t('diagnostics.tls-versions.results.security.title')}: {security.level} +

{security.description}

+
+
+
+ +
+ {diagnosticState.results.totalSupported} + {$t('diagnostics.tls-versions.results.versions.supported')} +

+ {$t('diagnostics.tls-versions.results.summary.outOfTested', { count: tlsVersions.length })} +

+
+
+
+
+ + +
+

{$t('diagnostics.tls-versions.results.versions.title')}

+
+ {#each tlsVersions as tlsVer (tlsVer.version)} + {@const supported = diagnosticState.results.supported[tlsVer.version]} + {@const status = getVersionStatus(tlsVer.version, supported, tlsVer.deprecated)} + +
+
+
+ +
+ {tlsVer.name} + ({tlsVer.version}) +
+
+ {status.status} +
+ + {#if !supported && diagnosticState.results.errors[tlsVer.version]} +
+ {diagnosticState.results.errors[tlsVer.version]} +
+ {/if} + + {#if tlsVer.deprecated} +
+ + {$t('diagnostics.tls-versions.warnings.deprecated')} +
+ {/if} +
+ {/each} +
+
+ + + {#if diagnosticState.results.minVersion || diagnosticState.results.maxVersion} +
+

{$t('diagnostics.tls-versions.results.summary.title')}

+
+
+ {$t('diagnostics.tls-versions.results.summary.minVersion')}: + {diagnosticState.results.minVersion || $t('common.common.unknown')} +
+
+ {$t('diagnostics.tls-versions.results.summary.maxVersion')}: + {diagnosticState.results.maxVersion || $t('common.common.unknown')} +
+
+
+ {/if} + {/if} +
+
+ {/if} + + + + +
+
+

{$t('diagnostics.tls-versions.educational.title')}

+
+
+
+
+

{$t('diagnostics.tls-versions.educational.versionSecurity.title')}

+
    +
  • {$t('diagnostics.tls-versions.educational.versionSecurity.tls13')}
  • +
  • {$t('diagnostics.tls-versions.educational.versionSecurity.tls12')}
  • +
  • {$t('diagnostics.tls-versions.educational.versionSecurity.tls11')}
  • +
  • {$t('diagnostics.tls-versions.educational.versionSecurity.tls10')}
  • +
+
+ +
+

{$t('diagnostics.tls-versions.educational.bestPractices.title')}

+
    +
  • {$t('diagnostics.tls-versions.educational.bestPractices.enableModern')}
  • +
  • {$t('diagnostics.tls-versions.educational.bestPractices.disableDeprecated')}
  • +
  • {$t('diagnostics.tls-versions.educational.bestPractices.updateRegularly')}
  • +
  • {$t('diagnostics.tls-versions.educational.bestPractices.strongCiphers')}
  • +
+
+ +
+

{$t('diagnostics.tls-versions.educational.compliance.title')}

+

+ {$t('diagnostics.tls-versions.educational.compliance.description')} +

+
+
+
+
+
+ + diff --git a/src/routes/[lang]/dns/+page.svelte b/src/routes/[lang]/dns/+page.svelte new file mode 100644 index 00000000..16ad63cf --- /dev/null +++ b/src/routes/[lang]/dns/+page.svelte @@ -0,0 +1,38 @@ + + +
+ + + +
+ + diff --git a/src/routes/[lang]/dns/dnssec/cds-cdnskey-builder/+page.svelte b/src/routes/[lang]/dns/dnssec/cds-cdnskey-builder/+page.svelte new file mode 100644 index 00000000..4c70c2e8 --- /dev/null +++ b/src/routes/[lang]/dns/dnssec/cds-cdnskey-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/dnssec/dnskey-tag/+page.svelte b/src/routes/[lang]/dns/dnssec/dnskey-tag/+page.svelte new file mode 100644 index 00000000..ec50238d --- /dev/null +++ b/src/routes/[lang]/dns/dnssec/dnskey-tag/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/dnssec/ds-generator/+page.svelte b/src/routes/[lang]/dns/dnssec/ds-generator/+page.svelte new file mode 100644 index 00000000..91d9ea06 --- /dev/null +++ b/src/routes/[lang]/dns/dnssec/ds-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/dnssec/nsec3-hash/+page.svelte b/src/routes/[lang]/dns/dnssec/nsec3-hash/+page.svelte new file mode 100644 index 00000000..a23d6188 --- /dev/null +++ b/src/routes/[lang]/dns/dnssec/nsec3-hash/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/dnssec/rrsig-planner/+page.svelte b/src/routes/[lang]/dns/dnssec/rrsig-planner/+page.svelte new file mode 100644 index 00000000..49e4d1ae --- /dev/null +++ b/src/routes/[lang]/dns/dnssec/rrsig-planner/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/edns-size-estimator/+page.svelte b/src/routes/[lang]/dns/edns-size-estimator/+page.svelte new file mode 100644 index 00000000..f9e2fa06 --- /dev/null +++ b/src/routes/[lang]/dns/edns-size-estimator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/+page.svelte b/src/routes/[lang]/dns/generators/+page.svelte new file mode 100644 index 00000000..14a6e234 --- /dev/null +++ b/src/routes/[lang]/dns/generators/+page.svelte @@ -0,0 +1,38 @@ + + +
+ + + +
diff --git a/src/routes/[lang]/dns/generators/a-aaaa-bulk/+page.svelte b/src/routes/[lang]/dns/generators/a-aaaa-bulk/+page.svelte new file mode 100644 index 00000000..7924110a --- /dev/null +++ b/src/routes/[lang]/dns/generators/a-aaaa-bulk/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/generators/caa-builder/+page.svelte b/src/routes/[lang]/dns/generators/caa-builder/+page.svelte new file mode 100644 index 00000000..399cf346 --- /dev/null +++ b/src/routes/[lang]/dns/generators/caa-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/cname-builder/+page.svelte b/src/routes/[lang]/dns/generators/cname-builder/+page.svelte new file mode 100644 index 00000000..d984766f --- /dev/null +++ b/src/routes/[lang]/dns/generators/cname-builder/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/generators/dkim-keygen/+page.svelte b/src/routes/[lang]/dns/generators/dkim-keygen/+page.svelte new file mode 100644 index 00000000..a08da4c9 --- /dev/null +++ b/src/routes/[lang]/dns/generators/dkim-keygen/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/dmarc-builder/+page.svelte b/src/routes/[lang]/dns/generators/dmarc-builder/+page.svelte new file mode 100644 index 00000000..a330bb28 --- /dev/null +++ b/src/routes/[lang]/dns/generators/dmarc-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/idn-punycode/+page.svelte b/src/routes/[lang]/dns/generators/idn-punycode/+page.svelte new file mode 100644 index 00000000..25259b0a --- /dev/null +++ b/src/routes/[lang]/dns/generators/idn-punycode/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/loc-builder/+page.svelte b/src/routes/[lang]/dns/generators/loc-builder/+page.svelte new file mode 100644 index 00000000..91e6d4c9 --- /dev/null +++ b/src/routes/[lang]/dns/generators/loc-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/mx-planner/+page.svelte b/src/routes/[lang]/dns/generators/mx-planner/+page.svelte new file mode 100644 index 00000000..d91b3717 --- /dev/null +++ b/src/routes/[lang]/dns/generators/mx-planner/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/generators/naptr-builder/+page.svelte b/src/routes/[lang]/dns/generators/naptr-builder/+page.svelte new file mode 100644 index 00000000..ecd58a1a --- /dev/null +++ b/src/routes/[lang]/dns/generators/naptr-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/ptr-generator/+page.svelte b/src/routes/[lang]/dns/generators/ptr-generator/+page.svelte new file mode 100644 index 00000000..22fc1782 --- /dev/null +++ b/src/routes/[lang]/dns/generators/ptr-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/rp-builder/+page.svelte b/src/routes/[lang]/dns/generators/rp-builder/+page.svelte new file mode 100644 index 00000000..aca520f7 --- /dev/null +++ b/src/routes/[lang]/dns/generators/rp-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/spf-builder/+page.svelte b/src/routes/[lang]/dns/generators/spf-builder/+page.svelte new file mode 100644 index 00000000..778a9641 --- /dev/null +++ b/src/routes/[lang]/dns/generators/spf-builder/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/generators/srv-builder/+page.svelte b/src/routes/[lang]/dns/generators/srv-builder/+page.svelte new file mode 100644 index 00000000..92c41e35 --- /dev/null +++ b/src/routes/[lang]/dns/generators/srv-builder/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/generators/sshfp-generator/+page.svelte b/src/routes/[lang]/dns/generators/sshfp-generator/+page.svelte new file mode 100644 index 00000000..3442b179 --- /dev/null +++ b/src/routes/[lang]/dns/generators/sshfp-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/svcb-https-builder/+page.svelte b/src/routes/[lang]/dns/generators/svcb-https-builder/+page.svelte new file mode 100644 index 00000000..3c17431b --- /dev/null +++ b/src/routes/[lang]/dns/generators/svcb-https-builder/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/tlsa-generator/+page.svelte b/src/routes/[lang]/dns/generators/tlsa-generator/+page.svelte new file mode 100644 index 00000000..779f6f2d --- /dev/null +++ b/src/routes/[lang]/dns/generators/tlsa-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/generators/txt-escape/+page.svelte b/src/routes/[lang]/dns/generators/txt-escape/+page.svelte new file mode 100644 index 00000000..c4b6e8d5 --- /dev/null +++ b/src/routes/[lang]/dns/generators/txt-escape/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/dns/label-normalizer/+page.svelte b/src/routes/[lang]/dns/label-normalizer/+page.svelte new file mode 100644 index 00000000..f3f953c5 --- /dev/null +++ b/src/routes/[lang]/dns/label-normalizer/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/record-validator/+page.svelte b/src/routes/[lang]/dns/record-validator/+page.svelte new file mode 100644 index 00000000..1f65f214 --- /dev/null +++ b/src/routes/[lang]/dns/record-validator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/reverse/ptr-generator/+page.svelte b/src/routes/[lang]/dns/reverse/ptr-generator/+page.svelte new file mode 100644 index 00000000..d8926e4a --- /dev/null +++ b/src/routes/[lang]/dns/reverse/ptr-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/reverse/ptr-sweep-planner/+page.svelte b/src/routes/[lang]/dns/reverse/ptr-sweep-planner/+page.svelte new file mode 100644 index 00000000..5fe3a15c --- /dev/null +++ b/src/routes/[lang]/dns/reverse/ptr-sweep-planner/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/reverse/reverse-zones/+page.svelte b/src/routes/[lang]/dns/reverse/reverse-zones/+page.svelte new file mode 100644 index 00000000..26ae7231 --- /dev/null +++ b/src/routes/[lang]/dns/reverse/reverse-zones/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/reverse/zone-generator/+page.svelte b/src/routes/[lang]/dns/reverse/zone-generator/+page.svelte new file mode 100644 index 00000000..8b1dc1aa --- /dev/null +++ b/src/routes/[lang]/dns/reverse/zone-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/ttl-calculator/+page.svelte b/src/routes/[lang]/dns/ttl-calculator/+page.svelte new file mode 100644 index 00000000..121c82ca --- /dev/null +++ b/src/routes/[lang]/dns/ttl-calculator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/zone/+page.svelte b/src/routes/[lang]/dns/zone/+page.svelte new file mode 100644 index 00000000..8675c5f5 --- /dev/null +++ b/src/routes/[lang]/dns/zone/+page.svelte @@ -0,0 +1,407 @@ + + +
+
+
+

DNS Zone File Tools

+

+ Professional tools for DNS zone file analysis, validation, and management. Built for network administrators, DNS + engineers, and automation workflows. +

+
+
+ +
+ {#each zoneTools as tool (tool.href)} + +
+ +
+
+

{tool.label}

+

{tool.description}

+
+
+ +
+
+ {/each} +
+ +
+
+
+
+ +
+
+

Zone Validation

+

+ Comprehensive RFC compliance checking with detailed error reporting and recommendations for fixing common + issues. +

+
+
+ +
+
+ +
+
+

Change Tracking

+

+ Compare zone file versions to track DNS changes, plan migrations, and audit modifications with unified diff + support. +

+
+
+ +
+
+ +
+
+

Analytics & Insights

+

+ Deep analysis of record distribution, TTL patterns, name lengths, and zone health metrics for optimization. +

+
+
+ +
+
+ +
+
+

Standards Compliance

+

+ Ensure DNS names meet RFC length limits and zone files follow best practices for reliable DNS operation. +

+
+
+
+
+ +
+

Common Use Cases

+
+
+

DNS Migration Planning

+

+ Compare existing and target zones to understand exactly what changes during migrations and ensure nothing is + missed. +

+
+ +
+

Zone File Cleanup

+

+ Normalize messy zone files with consistent formatting, proper ordering, duplicate removal, and error + correction. +

+
+ +
+

Compliance Auditing

+

+ Validate zone files against DNS standards to identify potential issues before they cause resolution failures. +

+
+ +
+

Configuration Analysis

+

+ Understand zone structure, identify optimization opportunities, and track DNS infrastructure growth over time. +

+
+
+
+ +
+

Zone File Best Practices

+
+
+ + Always include SOA and NS records for proper delegation +
+ +
+ + Use fully qualified domain names ending with dots +
+ +
+ + Maintain consistent TTL values based on change frequency +
+ +
+ + Keep domain names under 63 characters per label +
+ +
+ + Remove duplicate records to avoid confusion +
+ +
+ + Validate zones after changes before deployment +
+
+
+
+ + diff --git a/src/routes/[lang]/dns/zone/diff/+page.svelte b/src/routes/[lang]/dns/zone/diff/+page.svelte new file mode 100644 index 00000000..824000f4 --- /dev/null +++ b/src/routes/[lang]/dns/zone/diff/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/zone/linter/+page.svelte b/src/routes/[lang]/dns/zone/linter/+page.svelte new file mode 100644 index 00000000..d9f8ecca --- /dev/null +++ b/src/routes/[lang]/dns/zone/linter/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/zone/name-length-checker/+page.svelte b/src/routes/[lang]/dns/zone/name-length-checker/+page.svelte new file mode 100644 index 00000000..1b71883d --- /dev/null +++ b/src/routes/[lang]/dns/zone/name-length-checker/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/dns/zone/stats/+page.svelte b/src/routes/[lang]/dns/zone/stats/+page.svelte new file mode 100644 index 00000000..8507a5e3 --- /dev/null +++ b/src/routes/[lang]/dns/zone/stats/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/+page.svelte b/src/routes/[lang]/ip-address-convertor/+page.svelte new file mode 100644 index 00000000..59ce3f3a --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/+page.svelte @@ -0,0 +1,36 @@ + + +
+ + + +
+ + diff --git a/src/routes/[lang]/ip-address-convertor/distance/+page.svelte b/src/routes/[lang]/ip-address-convertor/distance/+page.svelte new file mode 100644 index 00000000..b082e8e4 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/distance/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/ip-address-convertor/enumerate/+page.svelte b/src/routes/[lang]/ip-address-convertor/enumerate/+page.svelte new file mode 100644 index 00000000..1a03afca --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/enumerate/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/eui64/+page.svelte b/src/routes/[lang]/ip-address-convertor/eui64/+page.svelte new file mode 100644 index 00000000..112a1477 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/eui64/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/ip-address-convertor/families/+layout.svelte b/src/routes/[lang]/ip-address-convertor/families/+layout.svelte new file mode 100644 index 00000000..b9f9e0b2 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/families/+layout.svelte @@ -0,0 +1,205 @@ + + +
+ + + +
+
+

+ + {$t('pages.ipConverter.families.understanding.title')} +

+
+
+ +
+

{$t('pages.ipConverter.families.understanding.ipv4.title')}

+

{$t('pages.ipConverter.families.understanding.ipv4.addressLength')}

+

{$t('pages.ipConverter.families.understanding.ipv4.format')}

+

{$t('pages.ipConverter.families.understanding.ipv4.totalAddresses')}

+

+ {$t('pages.ipConverter.families.understanding.ipv4.example')} 203.0.113.45 +

+

{$t('pages.ipConverter.families.understanding.ipv4.status')}

+
+ + +
+

{$t('pages.ipConverter.families.understanding.ipv6.title')}

+

{$t('pages.ipConverter.families.understanding.ipv6.addressLength')}

+

{$t('pages.ipConverter.families.understanding.ipv6.format')}

+

{$t('pages.ipConverter.families.understanding.ipv6.totalAddresses')}

+

+ {$t('pages.ipConverter.families.understanding.ipv6.example')} + 2001:0db8:85a3:0000:0000:8a2e:0370:7334 +

+

{$t('pages.ipConverter.families.understanding.ipv6.status')}

+
+ + +
+

+ {$t('pages.ipConverter.families.understanding.ipv4Mapped.title')} +

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.purpose')}

+

+ {$t('pages.ipConverter.families.understanding.ipv4Mapped.format')} + ::ffff:192.0.2.1 + or ::ffff:c000:0201 +

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.usage')}

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.structure')}

+
+
+
+
+ +
+

+ + {$t('pages.ipConverter.families.conversionMethods.title')} +

+
+
+
+

{$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.title')}

+
    +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.mapped')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.dualStack')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.tunneling')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.migration')}
  • +
+
+ +
+

{$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.title')}

+
    +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.legacySupport')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.compatibility')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.debugging')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.analysis')}
  • +
+
+ +
+

{$t('pages.ipConverter.families.conversionMethods.realWorldApps.title')}

+
    +
  • {$t('pages.ipConverter.families.conversionMethods.realWorldApps.webServers')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.realWorldApps.loadBalancers')}
  • +
  • + {$t('pages.ipConverter.families.conversionMethods.realWorldApps.networkMonitoring')} +
  • +
  • + {$t('pages.ipConverter.families.conversionMethods.realWorldApps.apiIntegration')} +
  • +
+
+
+
+
+ +
+

+ + {$t('pages.ipConverter.families.considerations.title')} +

+
+
+

{$t('pages.ipConverter.families.considerations.limitations.title')}

+
    +
  • + {$t('pages.ipConverter.families.considerations.limitations.ipv4MappedLimit')} +
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.security')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.performance')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.compatibility')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.bestPractice')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.futureProofing')}
  • +
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/ip-address-convertor/families/ipv4-to-ipv6/+page.svelte b/src/routes/[lang]/ip-address-convertor/families/ipv4-to-ipv6/+page.svelte new file mode 100644 index 00000000..2af2ae68 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/families/ipv4-to-ipv6/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/families/ipv6-to-ipv4/+page.svelte b/src/routes/[lang]/ip-address-convertor/families/ipv6-to-ipv4/+page.svelte new file mode 100644 index 00000000..3f4f8a4b --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/families/ipv6-to-ipv4/+page.svelte @@ -0,0 +1,9 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/ipv6/nat64/+page.svelte b/src/routes/[lang]/ip-address-convertor/ipv6/nat64/+page.svelte new file mode 100644 index 00000000..3408217c --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/ipv6/nat64/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/ipv6/solicited-node/+page.svelte b/src/routes/[lang]/ip-address-convertor/ipv6/solicited-node/+page.svelte new file mode 100644 index 00000000..2dbec166 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/ipv6/solicited-node/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/ipv6/teredo/+page.svelte b/src/routes/[lang]/ip-address-convertor/ipv6/teredo/+page.svelte new file mode 100644 index 00000000..f83e0ed8 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/ipv6/teredo/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/mac-address/+page.svelte b/src/routes/[lang]/ip-address-convertor/mac-address/+page.svelte new file mode 100644 index 00000000..2ac4f784 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/mac-address/+page.svelte @@ -0,0 +1,1409 @@ + + +
+
+

{$t('tools/mac-address.title')}

+

+ {$t('tools/mac-address.description')} +

+
+ +
+
+
+

+ {isBulkMode ? $t('tools/mac-address.form.bulkMode.title') : $t('tools/mac-address.form.singleMode.title')} +

+ +
+ +
+ {#if isBulkMode} + + +
+ {$t('tools/mac-address.form.helpText.bulk', { + formats: $t('tools/mac-address.form.helpText.formats', { + colon: '00:1A:2B:3C:4D:5E', + hyphen: '00-1A-2B-3C-4D-5E', + cisco: '001A.2B3C.4D5E', + bare: '001A2B3C4D5E', + }), + })} +
+ + {:else} + +
+ { + if (e.key === 'Enter') { + handleSubmit(); + } + }} + /> + +
+
+ {$t('tools/mac-address.form.helpText.single', { + formats: $t('tools/mac-address.form.helpText.formats', { + colon: '00:1A:2B:3C:4D:5E', + hyphen: '00-1A-2B-3C-4D-5E', + cisco: '001A.2B3C.4D5E', + bare: '001A2B3C4D5E', + }), + })} +
+ {/if} +
+
+
+ + +
+
+ + +

{$t('tools/mac-address.examples.title')}

+
+
+ {#each examples as example, i (i)} + + {/each} +
+
+
+ + {#if result} +
+ {#if result.conversions.length > 0} +
+
+

+ {result.conversions.length === 1 + ? $t('tools/mac-address.results.conversion') + : $t('tools/mac-address.results.conversions')} +

+ {#if result.conversions.length > 1} +
+ + +
+ {/if} +
+ + {#each result.conversions as conversion (conversion.input)} +
+ {#if result.conversions.length > 1} +
+
+ + {conversion.input} +
+ {#if conversion.error} +
{conversion.error}
+ {/if} +
+ {:else if !conversion.isValid} +
+
+ + {$t('tools/mac-address.results.invalidAddress')} +
+ {#if conversion.error} +
{conversion.error}
+ {/if} +
+ {/if} + + {#if conversion.isValid} + +
+

{$t('tools/mac-address.results.oui.title')}

+
+ {#each ouiFields as field (field.key)} + {@const value = field.render(conversion)} + {#if !field.condition || field.condition(conversion)} + {#if value !== undefined && value !== null && value !== ''} +
+ +
+ {field.label} + {#if field.code} + {#if field.tooltip} + + {value} + + {:else} + + {value} + + {/if} + {:else if field.tooltip} + + {value} + + {:else} + + {value} + + {/if} +
+
+ {/if} + {/if} + {/each} +
+
+ + +
+

{$t('tools/mac-address.results.details.title')}

+
+ {#each detailFields as field (field.label)} + {@const active = field.invert ? !conversion.details[field.key] : conversion.details[field.key]} +
+ + {field.label} +
+ {/each} +
+
+ + +
+

{$t('tools/mac-address.results.formats.title')}

+
+ {#each formatFields as field (field.key)} + {@const value = field.binary + ? conversion.details.binary + : conversion.formats[field.key as keyof MACFormat]} + {@const copyId = `${field.key}-${conversion.input}`} +
+ {field.label} +
+ {field.binary ? value.match(/.{1,8}/g)?.join(' ') : value} + +
+
+ {/each} +
+
+ {/if} +
+ {/each} +
+ + {#if result.conversions.length > 1} +
+

{$t('tools/mac-address.results.summary.title')}

+
+
+ {result.summary.total} + {$t('tools/mac-address.results.summary.stats.total')} +
+
+ {result.summary.valid} + {$t('tools/mac-address.results.summary.stats.valid')} +
+
+ {result.summary.invalid} + {$t('tools/mac-address.results.summary.stats.invalid')} +
+
+ {result.summary.withOUI} + {$t('tools/mac-address.results.summary.stats.withOui')} +
+
+
+ {/if} + {/if} +
+ {/if} +
+ + +
+
+

{$t('tools/mac-address.education.title')}

+
+
+
+ + +

{macAddressContent.sections.whatIsMAC.title}

+
+
+

{macAddressContent.sections.whatIsMAC.content}

+
+
+ +
+ + +

{macAddressContent.sections.structure.title}

+
+
+
    + {#each macAddressContent.sections.structure.components as comp (comp.component)} +
  • {comp.component}: {comp.description}
  • + {/each} +
+

+ Example: {macAddressContent.sections.structure.example.address}
+ + {#each macAddressContent.sections.structure.example.breakdown as line (line)} + β€’ {line}
+ {/each} +
+

+
+
+ +
+ + +

{macAddressContent.sections.addressTypes.title}

+
+
+
    + {#each macAddressContent.sections.addressTypes.types as type (type.type)} +
  • {type.type}: {type.description}
  • + {/each} +
+
+
+ +
+ + +

{macAddressContent.sections.formats.title}

+
+
+
    + {#each macAddressContent.sections.formats.formats as fmt (fmt.format)} +
  • {fmt.format}: {fmt.example} - {fmt.usage}
  • + {/each} +
+
+
+ +
+ + +

{macAddressContent.sections.ouiLookup.title}

+
+
+

{macAddressContent.sections.ouiLookup.content}

+
    + {#each macAddressContent.sections.ouiLookup.blockTypes as block (block.type)} +
  • {block.type}: {block.description}
  • + {/each} +
+

{macAddressContent.sections.ouiLookup.lookupInfo}

+
+
+ +
+ + +

{macAddressContent.sections.specialAddresses.title}

+
+
+
    + {#each macAddressContent.sections.specialAddresses.addresses as addr (addr.type)} +
  • {addr.type}: {addr.address} - {addr.description}
  • + {/each} +
+
+
+ +
+ + +

{macAddressContent.sections.useCases.title}

+
+
+
    + {#each macAddressContent.sections.useCases.cases as useCase (useCase.useCase)} +
  • {useCase.useCase}: {useCase.description}
  • + {/each} +
+
+
+ +
+ + +

{$t('tools/mac-address.education.quickTips')}

+
+
+
    + {#each macAddressContent.quickTips as tip (tip)} +
  • {tip}
  • + {/each} +
+
+
+
+
+ + diff --git a/src/routes/[lang]/ip-address-convertor/notation/+layout.svelte b/src/routes/[lang]/ip-address-convertor/notation/+layout.svelte new file mode 100644 index 00000000..8a300ce9 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/notation/+layout.svelte @@ -0,0 +1,371 @@ + + +
+ + + +
+
+

+ + {t('pages.ipv6-notation.title')} +

+
+
+ +
+

{t('pages.ipv6-notation.formats.expanded.title')}

+

+ {t('pages.ipv6-notation.terms.structure')} + {t('pages.ipv6-notation.formats.expanded.structure')} +

+

{t('common.labels.example')} 2001:0db8:85a3:0000:0000:8a2e:0370:7334

+

{t('common.labels.usage')} {t('pages.ipv6-notation.formats.expanded.usage')}

+

+ {t('pages.ipv6-notation.terms.benefits')} + {t('pages.ipv6-notation.formats.expanded.benefits')} +

+
+ + +
+

{t('pages.ipv6-notation.formats.compressed.title')}

+

+ {t('pages.ipv6-notation.terms.structure')} + {t('pages.ipv6-notation.formats.compressed.structure')} +

+

{t('common.labels.example')} 2001:db8:85a3::8a2e:370:7334

+

{t('common.labels.usage')} {t('pages.ipv6-notation.formats.compressed.usage')}

+

+ {t('pages.ipv6-notation.terms.benefits')} + {t('pages.ipv6-notation.formats.compressed.benefits')} +

+
+ + +
+

{t('pages.ipv6-notation.formats.rules.title')}

+

+ {t('pages.ipv6-notation.terms.doubleColonLabel')} + {t('pages.ipv6-notation.formats.rules.doubleColon')} +

+

+ {t('pages.ipv6-notation.terms.singleUseLabel')} + {t('pages.ipv6-notation.formats.rules.singleUse')} +

+

+ {t('pages.ipv6-notation.terms.leadingZerosLabel')} + {t('pages.ipv6-notation.formats.rules.leadingZeros')} +

+

+ {t('pages.ipv6-notation.terms.preferenceLabel')} + {t('pages.ipv6-notation.formats.rules.preference')} +

+
+
+
+
+ +
+

+ + {t('pages.ipv6-notation.conversions.title')} +

+
+
+
+

{t('pages.ipv6-notation.useCases.expand.title')}

+
    +
  • + {t('pages.ipv6-notation.terms.networkAnalysisLabel')} + {t('pages.ipv6-notation.useCases.expand.networkAnalysis')} +
  • +
  • + {t('pages.ipv6-notation.terms.databaseStorageLabel')} + {t('pages.ipv6-notation.useCases.expand.databaseStorage')} +
  • +
  • + {t('pages.ipv6-notation.terms.debuggingLabel')} + {t('pages.ipv6-notation.useCases.expand.debugging')} +
  • +
  • + {t('pages.ipv6-notation.terms.programmingLabel')} + {t('pages.ipv6-notation.useCases.expand.programming')} +
  • +
  • + {t('pages.ipv6-notation.terms.securityLabel')} + {t('pages.ipv6-notation.useCases.expand.security')} +
  • +
+
+ +
+

{t('pages.ipv6-notation.useCases.compress.title')}

+
    +
  • + {t('pages.ipv6-notation.terms.userInterfaceLabel')} + {t('pages.ipv6-notation.useCases.compress.userInterface')} +
  • +
  • + {t('pages.ipv6-notation.terms.configurationLabel')} + {t('pages.ipv6-notation.useCases.compress.configuration')} +
  • +
  • + {t('pages.ipv6-notation.terms.documentationLabel')} + {t('pages.ipv6-notation.useCases.compress.documentation')} +
  • +
  • + {t('pages.ipv6-notation.terms.urlsLabel')} + {t('pages.ipv6-notation.useCases.compress.urls')} +
  • +
  • + {t('pages.ipv6-notation.terms.networkEquipmentLabel')} + {t('pages.ipv6-notation.useCases.compress.networkEquipment')} +
  • +
+
+ +
+

{t('pages.ipv6-notation.useCases.scenarios.title')}

+
    +
  • + {t('pages.ipv6-notation.terms.networkMonitoringLabel')} + {t('pages.ipv6-notation.useCases.scenarios.networkMonitoring')} +
  • +
  • + {t('pages.ipv6-notation.terms.apiIntegrationLabel')} + {t('pages.ipv6-notation.useCases.scenarios.apiIntegration')} +
  • +
  • + {t('pages.ipv6-notation.terms.dataMigrationLabel')} + {t('pages.ipv6-notation.useCases.scenarios.dataMigration')} +
  • +
  • + {t('pages.ipv6-notation.terms.educationalToolsLabel')} + {t('pages.ipv6-notation.useCases.scenarios.educationalTools')} +
  • +
  • + {t('pages.ipv6-notation.terms.qualityAssuranceLabel')} + {t('pages.ipv6-notation.useCases.scenarios.qualityAssurance')} +
  • +
+
+
+
+
+ +
+

+ + {t('pages.ipv6-notation.conversions.examples.title')} +

+
+
+
+

{t('pages.ipv6-notation.examples.commonTypes')}

+
+
+
{t('pages.ipv6-notation.examples.loopback')}
+
+ ::1 + ↔ + 0000:0000:0000:0000:0000:0000:0000:0001 +
+
+
+
{t('pages.ipv6-notation.examples.linkLocal')}
+
+ fe80::1 + ↔ + fe80:0000:0000:0000:0000:0000:0000:0001 +
+
+
+
{t('pages.ipv6-notation.examples.documentation')}
+
+ 2001:db8:: + ↔ + 2001:0db8:0000:0000:0000:0000:0000:0000 +
+
+
+
+ +
+

{t('pages.ipv6-notation.examples.bestPractices.title')}

+
    +
  • + {t('pages.ipv6-notation.terms.rfc5952Label')} + {t('pages.ipv6-notation.examples.bestPractices.rfc5952')} +
  • +
  • + {t('pages.ipv6-notation.terms.consistencyLabel')} + {t('pages.ipv6-notation.examples.bestPractices.consistency')} +
  • +
  • + {t('pages.ipv6-notation.terms.validationLabel')} + {t('pages.ipv6-notation.examples.bestPractices.validation')} +
  • +
  • + {t('pages.ipv6-notation.terms.caseSensitivityLabel')} + {t('pages.ipv6-notation.examples.bestPractices.caseSensitivity')} +
  • +
  • + {t('pages.ipv6-notation.terms.leadingZerosRuleLabel')} + {t('pages.ipv6-notation.examples.bestPractices.leadingZeros')} +
  • +
+
+
+
+
+
+
+ + diff --git a/src/routes/[lang]/ip-address-convertor/notation/ipv6-compress/+page.svelte b/src/routes/[lang]/ip-address-convertor/notation/ipv6-compress/+page.svelte new file mode 100644 index 00000000..00aa0428 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/notation/ipv6-compress/+page.svelte @@ -0,0 +1,45 @@ + + + + + diff --git a/src/routes/[lang]/ip-address-convertor/notation/ipv6-expand/+page.svelte b/src/routes/[lang]/ip-address-convertor/notation/ipv6-expand/+page.svelte new file mode 100644 index 00000000..f0048083 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/notation/ipv6-expand/+page.svelte @@ -0,0 +1,45 @@ + + + + + diff --git a/src/routes/[lang]/ip-address-convertor/notation/normalize/+page.svelte b/src/routes/[lang]/ip-address-convertor/notation/normalize/+page.svelte new file mode 100644 index 00000000..4513c823 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/notation/normalize/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/notation/zone-id/+page.svelte b/src/routes/[lang]/ip-address-convertor/notation/zone-id/+page.svelte new file mode 100644 index 00000000..f7271c44 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/notation/zone-id/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/nth-ip/+page.svelte b/src/routes/[lang]/ip-address-convertor/nth-ip/+page.svelte new file mode 100644 index 00000000..648b98d5 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/nth-ip/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/ip-address-convertor/random/+page.svelte b/src/routes/[lang]/ip-address-convertor/random/+page.svelte new file mode 100644 index 00000000..341124f7 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/random/+page.svelte @@ -0,0 +1,7 @@ + + +
+ +
diff --git a/src/routes/[lang]/ip-address-convertor/regex/+page.svelte b/src/routes/[lang]/ip-address-convertor/regex/+page.svelte new file mode 100644 index 00000000..1dd01fd0 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/regex/+page.svelte @@ -0,0 +1,241 @@ + + + + +
+
+
+

{ipAddressValidationContent.title}

+

{ipAddressValidationContent.description}

+
+ +
+

{ipAddressValidationContent.sections.overview.title}

+

{ipAddressValidationContent.sections.overview.content}

+
+ +
+

{ipAddressValidationContent.sections.ipv4.title}

+

+ + {@html ipAddressValidationContent.sections.ipv4.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/β€’/g, '•')} +

+
+ +
+

{ipAddressValidationContent.sections.ipv6.title}

+

+ + {@html ipAddressValidationContent.sections.ipv6.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/β€’/g, '•')} +

+
+ +
+

{ipAddressValidationContent.sections.regexValidation.title}

+

+ + {@html ipAddressValidationContent.sections.regexValidation.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/β€’/g, '•')} +

+
+ +
+

Example Patterns

+
+ {#each Object.values(ipAddressValidationContent.examples) as example (example.pattern)} +
+

{example.title}

+
+ {example.pattern} +
+

{example.description}

+
+
+ Matches: + {example.matches.join(', ')} +
+
+ Fails: + {example.fails.join(', ')} +
+
+ Limitation: + {example.limitation} +
+
+
+ {/each} +
+
+ +
+

{ipAddressValidationContent.sections.practicalTips.title}

+

+ + {@html ipAddressValidationContent.sections.practicalTips.content + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/β€’/g, '•')} +

+
+ +
+

Key Recommendations

+
+ {#each ipAddressValidationContent.recommendations as rec (rec.title)} +
+
+ +
+
+

{rec.title}

+

{rec.description}

+
+
+ {/each} +
+
+
+
+ + diff --git a/src/routes/[lang]/ip-address-convertor/representations/+page.svelte b/src/routes/[lang]/ip-address-convertor/representations/+page.svelte new file mode 100644 index 00000000..9abd2e3c --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/representations/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/ula-generator/+page.svelte b/src/routes/[lang]/ip-address-convertor/ula-generator/+page.svelte new file mode 100644 index 00000000..65bba6e9 --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/ula-generator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/ip-address-convertor/validator/+page.svelte b/src/routes/[lang]/ip-address-convertor/validator/+page.svelte new file mode 100644 index 00000000..5795366f --- /dev/null +++ b/src/routes/[lang]/ip-address-convertor/validator/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/offline/+page.server.ts b/src/routes/[lang]/offline/+page.server.ts new file mode 100644 index 00000000..cffca4f2 --- /dev/null +++ b/src/routes/[lang]/offline/+page.server.ts @@ -0,0 +1,16 @@ +// Server-side code for offline page +// This ensures the page can be statically generated and cached without server dependencies + +import type { PageServerLoad } from './$types'; + +export const prerender = true; +export const ssr = false; + +// Override layout load to prevent dependencies and provide fallback data +export const load: PageServerLoad = async () => { + return { + breadcrumbJsonLd: null, // No breadcrumbs needed for offline page + version: '0.2.5', // Hardcoded fallback version for offline page + isOfflinePage: true, // Flag to indicate this is the special offline page + }; +}; diff --git a/src/routes/[lang]/offline/+page.svelte b/src/routes/[lang]/offline/+page.svelte new file mode 100644 index 00000000..e95a75e1 --- /dev/null +++ b/src/routes/[lang]/offline/+page.svelte @@ -0,0 +1,106 @@ + + + + Offline - Networking Toolbox + + + +
+
+ + +

+ {isOnline ? 'Back Online' : "You're Offline"} +

+ +

+ {isOnline ? 'Connection restored! Redirecting...' : 'Your bookmarked tools work offline'} +

+
+ + {#if $bookmarks.length > 0} +
+ +
+ {:else} +
+

No bookmarked tools yet

+ Browse Tools +
+ {/if} +
+ + diff --git a/src/routes/[lang]/reference/+layout.svelte b/src/routes/[lang]/reference/+layout.svelte new file mode 100644 index 00000000..a442132d --- /dev/null +++ b/src/routes/[lang]/reference/+layout.svelte @@ -0,0 +1,128 @@ + + +{@render children?.()} + +{#if isRef} + +{/if} + + diff --git a/src/routes/[lang]/reference/+page.svelte b/src/routes/[lang]/reference/+page.svelte new file mode 100644 index 00000000..3acbdaed --- /dev/null +++ b/src/routes/[lang]/reference/+page.svelte @@ -0,0 +1,32 @@ + + +
+

Networking Pocket Reference

+

+ Offline quick guides, cheat sheets and reference info, for networking concepts, IP addressing, and common protocols +

+
+ + + + diff --git a/src/routes/[lang]/reference/arp-vs-ndp/+page.svelte b/src/routes/[lang]/reference/arp-vs-ndp/+page.svelte new file mode 100644 index 00000000..5446a31d --- /dev/null +++ b/src/routes/[lang]/reference/arp-vs-ndp/+page.svelte @@ -0,0 +1,217 @@ + + +
+
+
+

{arpVsNdpContent.title}

+

{arpVsNdpContent.description}

+
+ +
+

{arpVsNdpContent.sections.overview.title}

+

{arpVsNdpContent.sections.overview.content}

+
+ +
+

{$t('comparison.title')}

+ + + + + + + + + + {#each arpVsNdpContent.comparison.basic as item, index (`${item.aspect}-${index}`)} + + + + + + {/each} + +
Aspect{$t('comparison.headers.arp')}{$t('comparison.headers.ndp')}
{item.aspect}{item.arp}{item.ndp}
+
+ +
+

{arpVsNdpContent.arpDetails.title}

+ +

{$t('arp.messageTypes.title')}

+ {#each arpVsNdpContent.arpDetails.messageTypes as type, index (`${type.type}-${index}`)} +
+
{type.type}
+
+
{$t('arp.messageTypes.fields.description')} {type.description}
+
{$t('arp.messageTypes.fields.destination')} {type.destination}
+
{$t('arp.messageTypes.fields.response')} {type.response}
+
+
+ {/each} + +

{$t('arp.process.title')}

+
    + {#each arpVsNdpContent.arpDetails.process as step, index (`arp-process-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('arp.limitations.title')}

+
    + {#each arpVsNdpContent.arpDetails.limitations as limitation, index (`arp-limitation-${index}`)} +
  • {limitation}
  • + {/each} +
+
+ +
+

{arpVsNdpContent.ndpDetails.title}

+ +

{$t('ndp.messageTypes.title')}

+ {#each arpVsNdpContent.ndpDetails.messageTypes as type, index (`${type.type}-${index}`)} +
+
{type.type}
+
+
{$t('ndp.messageTypes.fields.icmpType')} {type.icmpType}
+
{$t('ndp.messageTypes.fields.description')} {type.description}
+
{$t('ndp.messageTypes.fields.destination')} {type.destination}
+
{$t('ndp.messageTypes.fields.purpose')} {type.purpose}
+
+
+ {/each} + +

{$t('ndp.process.title')}

+
    + {#each arpVsNdpContent.ndpDetails.process as step, index (`ndp-process-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('ndp.advantages.title')}

+
    + {#each arpVsNdpContent.ndpDetails.advantages as advantage, index (`ndp-advantage-${index}`)} +
  • {advantage}
  • + {/each} +
+
+ +
+

{$t('practical.title')}

+ {#each arpVsNdpContent.practicalDifferences as diff, index (`${diff.scenario}-${index}`)} +
+
{diff.scenario}
+
+
{$t('practical.fields.arp')} {diff.arp}
+
{$t('practical.fields.ndp')} {diff.ndp}
+
{$t('practical.fields.impact')} {diff.impact}
+
+
+ {/each} +
+ +
+

{$t('troubleshooting.title')}

+ + + + + + + + + + + {#each arpVsNdpContent.troubleshootingCommands as cmd, index (`${cmd.purpose}-${index}`)} + + + + + + + {/each} + +
Purpose{$t('troubleshooting.headers.ipv4')}{$t('troubleshooting.headers.ipv6')}Windows
{cmd.purpose}{cmd.ipv4}{cmd.ipv6}{cmd.windows}
+
+ +
+

{$t('issues.title')}

+ {#each arpVsNdpContent.commonIssues as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} ({issue.protocol}) +
+
+

{$t('issues.fields.description')} {issue.description}

+

{$t('issues.fields.detection')} {issue.detection}

+

{$t('issues.fields.mitigation')} {issue.mitigation}

+
+
+ {/each} +
+ +
+

{$t('bestPractices.title')}

+ {#each arpVsNdpContent.bestPractices as practices, index (`${practices.protocol}-${index}`)} +

{practices.protocol} Best Practices

+
    + {#each practices.practices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+ {/each} +
+ +
+

{$t('quickReference.title')}

+
+
+
{$t('quickReference.arp')}
+ {#each arpVsNdpContent.quickReference.arp as point, index (`arp-point-${index}`)} +
{point}
+ {/each} +
+ +
+
{$t('quickReference.ndp')}
+ {#each arpVsNdpContent.quickReference.ndp as point, index (`ndp-point-${index}`)} +
{point}
+ {/each} +
+
+
+ +
+

{$t('migration.title')}

+
+
{$t('migration.considerations')}
+ {#each arpVsNdpContent.migrationTips as tip, index (`migration-tip-${index}`)} +
+
{tip}
+
+ {/each} +
+ +
+
+ + Key Takeaway +
+
+ While NDP is more complex than ARP, it's also much more capable and efficient. Understanding both protocols is + essential for mixed IPv4/IPv6 environments. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/asn/+page.svelte b/src/routes/[lang]/reference/asn/+page.svelte new file mode 100644 index 00000000..11bf9eff --- /dev/null +++ b/src/routes/[lang]/reference/asn/+page.svelte @@ -0,0 +1,215 @@ + + +
+
+
+

{asnContent.title}

+

{asnContent.description}

+
+ +
+

{asnContent.sections.overview.title}

+

{asnContent.sections.overview.content}

+
+ +
+

{asnContent.sections.asn.title}

+

{asnContent.sections.asn.content}

+
+ +
+

ASN Number Ranges

+ {#each asnContent.asnTypes as type, index (`${type.name}-${index}`)} +
+
{type.name}
+
+
Range: {type.range}
+
Description: {type.description}
+
Usage: {type.usage}
+
Examples:
+ {#each type.examples as example, index (`example-${index}`)} +
{example}
+ {/each} +
+
+ {/each} +
+ +
+

{asnContent.bgpBasics.title}

+

{asnContent.bgpBasics.description}

+ +

Key BGP Concepts

+
+ {#each asnContent.bgpBasics.concepts as concept, index (`${concept.term}-${index}`)} +
+
{concept.term}
+
+ Definition: + {concept.definition}
+ Example: + {concept.example} +
+
+ {/each} +
+ +

BGP Types

+ + + + + + + + + + + {#each asnContent.bgpBasics.types as type, index (`${type.type}-${index}`)} + + + + + + + {/each} + +
TypeDescriptionUsagePort
{type.type}{type.description}{type.usage}{type.port}
+
+ +
+

{asnContent.ipToAsnMapping.title}

+

{asnContent.ipToAsnMapping.description}

+ +

How It Works

+
    + {#each asnContent.ipToAsnMapping.process as step, index (`mapping-process-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

Real-World Examples

+ + + + + + + + + + + {#each asnContent.ipToAsnMapping.examples as example, index (`${example.asn}-${index}`)} + + + + + + + {/each} + +
IP RangeASNOrganizationDescription
{example.ipRange}{example.asn}{example.organization}{example.description}
+
+ +
+

{asnContent.lookupTools.title}

+

{asnContent.lookupTools.description}

+ +
+ {#each asnContent.lookupTools.methods as method, index (`${method.method}-${index}`)} +
+
{method.method}
+
{method.command}
+
+ {method.description}
+ {method.example} +
+
+ {/each} +
+ +

Common Lookup Commands

+
+
Try These Commands
+ {#each asnContent.lookupTools.commonCommands as command, index (`command-${index}`)} +
+ {command} +
+ {/each} +
+
+ +
+

Real-World AS Examples

+ {#each asnContent.realWorldExamples as example, index (`${example.asn}-${index}`)} +
+
{example.scenario}: {example.asn}
+
+
Organization: {example.organization}
+
Role: {example.role}
+
IP Blocks: {example.ipBlocks}
+
Peering: {example.peers}
+
+
+ {/each} +
+ +
+

Benefits of the AS System

+
    + {#each asnContent.benefits as benefit, index (`benefit-${index}`)} +
  • {benefit}
  • + {/each} +
+
+ +
+

Troubleshooting with ASN Information

+ {#each asnContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

Likely Cause: {issue.cause}

+

Investigation: {issue.investigation}

+

Solution: {issue.solution}

+
+
+ {/each} +
+ +
+

Getting Started with ASN Knowledge

+ {#each asnContent.gettingStarted as step, index (`${step.step}-${index}`)} +
+
+ + {step.step} +
+
+

{step.description}

+

Action: {step.action}

+
+
+ {/each} +
+ +
+

Quick Facts to Remember

+
+
Key Points
+ {#each asnContent.quickFacts as fact, index (`fact-${index}`)} +
+
{fact}
+
+ {/each} +
+
+
+
diff --git a/src/routes/[lang]/reference/cgnat/+page.svelte b/src/routes/[lang]/reference/cgnat/+page.svelte new file mode 100644 index 00000000..de6a35e0 --- /dev/null +++ b/src/routes/[lang]/reference/cgnat/+page.svelte @@ -0,0 +1,246 @@ + + +
+
+
+

{cgnatContent.title}

+

{cgnatContent.description}

+
+ +
+

{cgnatContent.sections.overview.title}

+

{cgnatContent.sections.overview.content}

+
+ +
+

{cgnatContent.sections.why.title}

+

{cgnatContent.sections.why.content}

+
+ +
+

{$t('pages.cgnat.addressRange.title')}

+
+
+ + {$t('pages.cgnat.addressRange.sharedSpace')} +
+
+

+ {$t('pages.cgnat.addressRange.labels.range')}: + {cgnatContent.addressRange.range} +

+

+ {$t('pages.cgnat.addressRange.labels.fullRange')}: + {cgnatContent.addressRange.fullRange} +

+

+ {$t('pages.cgnat.addressRange.labels.totalAddresses')}: + {cgnatContent.addressRange.totalAddresses} +

+

{$t('pages.cgnat.addressRange.labels.rfc')}: {cgnatContent.addressRange.rfc}

+
+
+ +

{$t('pages.cgnat.addressRange.breakdown.title')}

+ + + + + + + + + + {#each cgnatContent.addressRange.breakdown as block, index (`${block.network}-${index}`)} + + + + + + {/each} + +
{$t('pages.cgnat.addressRange.breakdown.headers.network')}{$t('pages.cgnat.addressRange.breakdown.headers.addresses')}{$t('pages.cgnat.addressRange.breakdown.headers.use')}
{block.network}{block.addresses}{block.use}
+
+ +
+

{cgnatContent.howItWorks.title}

+

{cgnatContent.howItWorks.description}

+ +

{$t('pages.cgnat.natSystem.title')}

+ + + + + + + + + + + + {#each cgnatContent.howItWorks.layers as layer, index (`${layer.layer}-${index}`)} + + + + + + + + {/each} + +
{$t('pages.cgnat.natSystem.headers.layer')}{$t('pages.cgnat.natSystem.headers.location')}{$t('pages.cgnat.natSystem.headers.insideAddress')}{$t('pages.cgnat.natSystem.headers.outsideAddress')}{$t('pages.cgnat.natSystem.headers.purpose')}
{layer.layer}{layer.location}{layer.inside}{layer.outside}{layer.purpose}
+ +

{$t('pages.cgnat.trafficFlow.title')}

+
    + {#each cgnatContent.howItWorks.flow as step, index (`flow-step-${index}`)} +
  1. {step}
  2. + {/each} +
+
+ +
+

{cgnatContent.identification.title}

+ {#each cgnatContent.identification.methods as method, index (`${method.method}-${index}`)} +
+
{method.method}
+
+
{$t('pages.cgnat.identification.labels.description')}: {method.description}
+
+ {$t('pages.cgnat.identification.labels.cgnatIndicator')}: + {method.cgnatIndicator} +
+
+ {$t('pages.cgnat.identification.labels.normalIndicator')}: + {method.normalIndicator} +
+
+
+ {/each} +
+ +
+

{$t('pages.cgnat.impacts.title')}

+ +

{$t('pages.cgnat.impacts.negative.title')}

+ {#each cgnatContent.impacts.negative as impact, index (`${impact.impact}-${index}`)} +
+
+ + {impact.impact} +
+
+

{$t('pages.cgnat.impacts.negative.labels.description')}: {impact.description}

+

+ {$t('pages.cgnat.impacts.negative.labels.affectedServices')}: + {impact.affectedServices.join(', ')} +

+

{$t('pages.cgnat.impacts.negative.labels.workaround')}: {impact.workaround}

+
+
+ {/each} + +

{$t('pages.cgnat.impacts.positive.title')}

+
    + {#each cgnatContent.impacts.positive as positive, index (`positive-${index}`)} +
  • {positive}
  • + {/each} +
+
+ +
+

{$t('pages.cgnat.workarounds.title')}

+ {#each cgnatContent.workarounds as solution, index (`${solution.solution}-${index}`)} +
+
{solution.solution}
+
+
{$t('pages.cgnat.workarounds.labels.description')}: {solution.description}
+
{$t('pages.cgnat.workarounds.labels.effectiveness')}: {solution.effectiveness}
+
{$t('pages.cgnat.workarounds.labels.cost')}: {solution.cost}
+
+
+ {/each} +
+ +
+

{$t('pages.cgnat.troubleshooting.title')}

+ {#each cgnatContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

{$t('pages.cgnat.troubleshooting.labels.cause')}: {issue.cause}

+

{$t('pages.cgnat.troubleshooting.labels.diagnosis')}: {issue.diagnosis}

+

{$t('pages.cgnat.troubleshooting.labels.solution')}: {issue.solution}

+
+
+ {/each} +
+ +
+

{$t('pages.cgnat.quickCheck.title')}

+ +
+
+
{$t('pages.cgnat.quickCheck.stepsTitle')}
+
    + {#each cgnatContent.quickCheck.steps as step, index (`quickcheck-step-${index}`)} +
  1. {step}
  2. + {/each} +
+
+ +
+
{$t('pages.cgnat.quickCheck.nextStepsTitle')}
+
    + {#each cgnatContent.quickCheck.whatToDo as action, index (`whatToDo-${index}`)} +
  • {action}
  • + {/each} +
+
+
+
+ +
+

{$t('pages.cgnat.bestPractices.title')}

+
    + {#each cgnatContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

{$t('pages.cgnat.ispPerspective.title')}

+
+
{$t('pages.cgnat.ispPerspective.whyTitle')}
+ {#each cgnatContent.ispPerspective as reason, index (`isp-reason-${index}`)} +
+
{reason}
+
+ {/each} +
+ +
+
+ + {$t('pages.cgnat.ispPerspective.tradeoffTitle')} +
+
+ {$t('pages.cgnat.ispPerspective.tradeoffDescription')} +
+
+
+
+
diff --git a/src/routes/[lang]/reference/cidr/+page.svelte b/src/routes/[lang]/reference/cidr/+page.svelte new file mode 100644 index 00000000..b0a86123 --- /dev/null +++ b/src/routes/[lang]/reference/cidr/+page.svelte @@ -0,0 +1,99 @@ + + +
+
+
+

{cidrContent.title}

+

{cidrContent.description}

+
+ +
+

{cidrContent.sections.whatIs.title}

+

{cidrContent.sections.whatIs.content}

+ +
+
+ + Quick Example +
+
+ In 192.168.1.0/24, the network is 192.168.1.0 and there are 254 usable host addresses + (192.168.1.1 through 192.168.1.254). +
+
+
+ +
+

{cidrContent.sections.whyReplaced.title}

+

{cidrContent.sections.whyReplaced.content}

+
+ +
+

{cidrContent.sections.howToRead.title}

+

{cidrContent.sections.howToRead.content}

+
+ +
+

Common Examples

+
+
Network Examples
+ {#each cidrContent.examples as example, index (`${example.cidr}-${index}`)} +
+ {example.cidr} + β†’ + {example.hosts} +
{example.description}
+
+ {/each} +
+
+ +
+

Prefix Length Reference Table

+ + + + + + + + + + + {#each cidrContent.prefixTable as row, index (`${row.prefix}-${index}`)} + + + + + + + {/each} + +
PrefixSubnet MaskUsable HostsTypical Use
{row.prefix}{row.mask}{row.hosts}{row.typical}
+
+ +
+

Key Points to Remember

+
    + {#each cidrContent.keyPoints as point, index (`point-${index}`)} +
  • {point}
  • + {/each} +
+ +
+
+ + Remember +
+
+ The first and last addresses in any network are reserved (network address and broadcast address), so the + usable host count is always 2 less than the total addresses. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/common-subnets/+page.svelte b/src/routes/[lang]/reference/common-subnets/+page.svelte new file mode 100644 index 00000000..f571761e --- /dev/null +++ b/src/routes/[lang]/reference/common-subnets/+page.svelte @@ -0,0 +1,181 @@ + + + +
+
+

Common Subnets

+

Frequently used CIDR prefixes with masks, host counts, and typical usage.

+
+ +
+
+ CIDR + Subnet Mask + Hosts + Usage +
+ + {#each COMMON_SUBNETS as subnet (`${subnet.cidr}-${subnet.mask}`)} + +
+ /{subnet.cidr} + {subnet.mask} + {subnet.hosts.toLocaleString()} + + {#if subnet.cidr === 8} + Large ISPs + {:else if subnet.cidr === 16} + Universities + {:else if subnet.cidr === 24} + Small businesses + {:else if subnet.cidr === 25} + Departments + {:else if subnet.cidr === 26} + Teams + {:else if subnet.cidr === 27} + Small offices + {:else if subnet.cidr === 28} + Workgroups + {:else if subnet.cidr === 29} + Small groups + {:else if subnet.cidr === 30} + Point-to-point + {:else} + General use + {/if} + +
+
+ {/each} +
+
+ + diff --git a/src/routes/[lang]/reference/icmp/+page.svelte b/src/routes/[lang]/reference/icmp/+page.svelte new file mode 100644 index 00000000..d1bec66b --- /dev/null +++ b/src/routes/[lang]/reference/icmp/+page.svelte @@ -0,0 +1,176 @@ + + +
+
+
+

{icmpContent.title}

+

{icmpContent.description}

+
+ +
+

{icmpContent.sections.overview.title}

+

{icmpContent.sections.overview.content}

+
+ +
+

Common ICMPv4 Types

+ {#each icmpContent.icmpv4Types as type, index (`${type.type}-${index}`)} +
+
Type {type.type}: {type.name}
+
+
Description: {type.description}
+
Common Use: {type.commonUse}
+
Example: {type.example}
+
Troubleshooting: {type.troubleshooting}
+ {#if type.codes} +
Common Codes:
+
    + {#each type.codes as code, index (`code-${code.code}-${index}`)} +
  • Code {code.code}: {code.meaning}
  • + {/each} +
+ {/if} +
+
+ {/each} +
+ +
+

Common ICMPv6 Types

+ {#each icmpContent.icmpv6Types as type, index (`${type.type}-${index}`)} +
+
Type {type.type}: {type.name}
+
+
Description: {type.description}
+
Common Use: {type.commonUse}
+
Example: {type.example}
+
Troubleshooting: {type.troubleshooting}
+ {#if type.codes} +
Common Codes:
+
    + {#each type.codes as code, index (`code-${code.code}-${index}`)} +
  • Code {code.code}: {code.meaning}
  • + {/each} +
+ {/if} +
+
+ {/each} +
+ +
+

Practical Troubleshooting Scenarios

+ {#each icmpContent.practicalExamples as scenario, index (`${scenario.scenario}-${index}`)} +
+
+ + {scenario.scenario} +
+
+

ICMP Types Involved: {scenario.icmpTypes.join(', ')}

+

What to Check:

+
    + {#each scenario.whatToCheck as check, index (`check-${index}`)} +
  • {check}
  • + {/each} +
+

Common Causes: {scenario.commonCauses.join(', ')}

+
+
+ {/each} +
+ +
+

Common ICMP Filtering Issues

+ {#each icmpContent.filteringIssues as issue, index (`${issue.issue}-${index}`)} +
+
{issue.issue}
+
+
Problem: {issue.problem}
+
Solution: {issue.solution}
+
Recommendation: {issue.recommendation}
+
+
+ {/each} +
+ +
+

Troubleshooting Commands

+ + + + + + + + + + {#each icmpContent.troubleshootingCommands as cmd, index (`${cmd.purpose}-${index}`)} + + + + + + {/each} + +
CommandPurposeICMP Type Used
{cmd.command}{cmd.purpose}{cmd.icmpType}
+
+ +
+

Best Practices for ICMP

+
    + {#each icmpContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

ICMP Quick Reference

+ +
+
+
Always Allow These
+ {#each icmpContent.quickReference.mustAllow as type, index (`must-${index}`)} +
{type}
+ {/each} +
+ +
+
Never Filter These
+ {#each icmpContent.quickReference.neverFilter as type, index (`never-${index}`)} +
{type}
+ {/each} +
Critical for proper network operation
+
+
+
+ +
+

Common Mistakes to Avoid

+
+
Don't Do These
+ {#each icmpContent.commonMistakes as mistake, index (`mistake-${index}`)} +
+
{mistake}
+
+ {/each} +
+ +
+
+ + Security vs Functionality +
+
+ Don't block all ICMP for security. Instead, use rate limiting and allow essential types. Blocking ICMP + completely breaks critical network functions like Path MTU Discovery and IPv6 Neighbor Discovery. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/ipv6-address-types/+page.svelte b/src/routes/[lang]/reference/ipv6-address-types/+page.svelte new file mode 100644 index 00000000..3abdf530 --- /dev/null +++ b/src/routes/[lang]/reference/ipv6-address-types/+page.svelte @@ -0,0 +1,134 @@ + + +
+
+
+

{ipv6AddressTypesContent.title}

+

{ipv6AddressTypesContent.description}

+
+ +
+

{ipv6AddressTypesContent.sections.overview.title}

+

{ipv6AddressTypesContent.sections.overview.content}

+
+ +
+

Unicast Address Types

+ {#each ipv6AddressTypesContent.unicastTypes as type, index (`${type.type}-${index}`)} +
+
{type.type}
+
+
Prefix: {type.prefix}
+
Range: {type.range}
+
Description: {type.description}
+
Usage: {type.usage}
+
Example: {type.example}
+
+
+ {/each} +
+ +
+

Special Addresses

+ + + + + + + + + + + {#each ipv6AddressTypesContent.specialAddresses as addr, index (`${addr.address}-${index}`)} + + + + + + + {/each} + +
AddressNameDescriptionUsage
{addr.address}{addr.name}{addr.description}{addr.usage}
+
+ +
+

Multicast Address Scopes

+

All multicast addresses start with ff. The second byte indicates scope:

+ + {#each ipv6AddressTypesContent.multicastTypes as scope, index (`${scope.scope}-${index}`)} +
+
{scope.scope} Scope
+
+
Prefix: {scope.prefix}
+
Description: {scope.description}
+ {#if scope.examples.length > 0} +
Common Addresses:
+ {#each scope.examples as example, index (`example-${index}`)} +
{example}
+ {/each} + {/if} +
+
+ {/each} +
+ +
+

{ipv6AddressTypesContent.anycast.title}

+

{ipv6AddressTypesContent.anycast.description}

+ +
+
+ + Example +
+
+ {ipv6AddressTypesContent.anycast.example} +
+
+ +

Common Anycast Uses

+
    + {#each ipv6AddressTypesContent.anycast.commonUses as use, index (`use-${index}`)} +
  • {use}
  • + {/each} +
+
+ +
+

Reserved Address Ranges

+ + + + + + + + + {#each ipv6AddressTypesContent.reservedRanges as range, index (`${range.prefix}-${index}`)} + + + + + {/each} + +
PrefixPurpose
{range.prefix}{range.purpose}
+
+ +
+

Quick Recognition Tips

+
+
Remember These Patterns
+ {#each ipv6AddressTypesContent.quickTips as tip, index (`tip-${index}`)} +
+
{tip}
+
+ {/each} +
+
+
+
diff --git a/src/routes/[lang]/reference/ipv6-embedded-ipv4/+page.svelte b/src/routes/[lang]/reference/ipv6-embedded-ipv4/+page.svelte new file mode 100644 index 00000000..5a36f77c --- /dev/null +++ b/src/routes/[lang]/reference/ipv6-embedded-ipv4/+page.svelte @@ -0,0 +1,157 @@ + + +
+
+
+

{ipv6EmbeddedIPv4Content.title}

+

{ipv6EmbeddedIPv4Content.description}

+
+ +
+

{ipv6EmbeddedIPv4Content.sections.overview.title}

+

{ipv6EmbeddedIPv4Content.sections.overview.content}

+
+ +
+

IPv4-in-IPv6 Mechanisms

+ {#each ipv6EmbeddedIPv4Content.mechanisms as mechanism, index (`${mechanism.name}-${index}`)} +
+
+ {mechanism.name} + [{mechanism.status}] +
+
+
Prefix: {mechanism.prefix}
+
Purpose: {mechanism.purpose}
+
Format: {mechanism.format}
+
Examples:
+ {#each mechanism.examples as example, index (`example-${index}`)} +
{example}
+ {/each} +
Usage:
+
    + {#each mechanism.usage as use, index (`use-${index}`)} +
  • {use}
  • + {/each} +
+
Note: {mechanism.notes}
+
+
+ {/each} +
+ +
+

{ipv6EmbeddedIPv4Content.recognition.title}

+ + + + + + + + + + {#each ipv6EmbeddedIPv4Content.recognition.patterns as pattern, index (`${pattern.pattern}-${index}`)} + + + + + + {/each} + +
PatternMeaningWhat to Do
{pattern.pattern}{pattern.meaning}{pattern.action}
+
+ +
+

{ipv6EmbeddedIPv4Content.conversion.title}

+

To understand embedded addresses, you need to convert IPv4 addresses to hexadecimal:

+ + + + + + + + + + + {#each ipv6EmbeddedIPv4Content.conversion.examples as example, index (`${example.ipv4}-${index}`)} + + + + + + {/each} + +
IPv4 AddressHex EquivalentBreakdown
{example.ipv4}{example.hex}{example.breakdown}
+ +
+
+ + Quick Tip +
+
+ Each IPv4 octet becomes 2 hex digits. For example: 192 = C0, 168 = A8, so 192.168.1.1 becomes C0A8:0101. +
+
+
+ +
+

Modern Usage Guidelines

+
    + {#each ipv6EmbeddedIPv4Content.modernUsage as guideline, index (`guideline-${index}`)} +
  • {guideline}
  • + {/each} +
+
+ +
+

Common Troubleshooting Issues

+ {#each ipv6EmbeddedIPv4Content.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

Cause: {issue.cause}

+

Solution: {issue.solution}

+
+
+ {/each} +
+ +
+

Security Considerations

+
+
Important Security Notes
+ {#each ipv6EmbeddedIPv4Content.securityNotes as note, index (`note-${index}`)} +
+
{note}
+
+ {/each} +
+ +
+
+ + Security Warning +
+
+ Many IPv4-in-IPv6 transition mechanisms have known security vulnerabilities. Disable unused mechanisms and + monitor for unexpected embedded address patterns in your network. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/ipv6-prefix-lengths/+page.svelte b/src/routes/[lang]/reference/ipv6-prefix-lengths/+page.svelte new file mode 100644 index 00000000..b27f8d1c --- /dev/null +++ b/src/routes/[lang]/reference/ipv6-prefix-lengths/+page.svelte @@ -0,0 +1,143 @@ + + +
+
+
+

{ipv6PrefixLengthsContent.title}

+

{ipv6PrefixLengthsContent.description}

+
+ +
+

{ipv6PrefixLengthsContent.sections.overview.title}

+

{ipv6PrefixLengthsContent.sections.overview.content}

+
+ +
+

Common IPv6 Prefix Lengths

+ {#each ipv6PrefixLengthsContent.commonPrefixes as prefix, index (`${prefix.prefix}-${index}`)} +
+
{prefix.prefix} - {prefix.name}
+
+
Capacity: {prefix.hosts}
+
Typical Use: {prefix.typical}
+
Description: {prefix.description}
+
Examples:
+ {#each prefix.examples as example, index (`prefix-example-${index}`)} +
{example}
+ {/each} +
+
+ {/each} +
+ +
+

Usage Guidelines

+ +
+
+
{ipv6PrefixLengthsContent.usageGuidelines.residential.title}
+ {#each ipv6PrefixLengthsContent.usageGuidelines.residential.allocations as alloc, index (`residential-${index}`)} +
{alloc.size}
+
{alloc.description}
+ {/each} +
+ +
+
{ipv6PrefixLengthsContent.usageGuidelines.enterprise.title}
+ {#each ipv6PrefixLengthsContent.usageGuidelines.enterprise.allocations as alloc, index (`enterprise-${index}`)} +
{alloc.size}
+
{alloc.description}
+ {/each} +
+ +
+
{ipv6PrefixLengthsContent.usageGuidelines.subnets.title}
+ {#each ipv6PrefixLengthsContent.usageGuidelines.subnets.allocations as alloc, index (`subnets-${index}`)} +
{alloc.size}
+
{alloc.description}
+ {/each} +
+
+
+ +
+

IPv4 vs IPv6 Comparison

+ + + + + + + + + + {#each ipv6PrefixLengthsContent.comparison.mappings as mapping, index (`${mapping.ipv4}-${index}`)} + + + + + + {/each} + +
IPv4 EquivalentIPv6 UsageNote
{mapping.ipv4}{mapping.ipv6}{mapping.note}
+
+ +
+

Quick Reference Table

+ + + + + + + + + + {#each ipv6PrefixLengthsContent.quickReference as row, index (`${row.prefix}-${index}`)} + + + + + + {/each} + +
PrefixAvailable /64 SubnetsTypical Use
{row.prefix}{row.subnets}{row.note}
+
+ +
+

Best Practices

+
    + {#each ipv6PrefixLengthsContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+ +
+
+ + Key Rule +
+
+ Always use /64 for end-user networks. This is required for SLAAC (Stateless Address Autoconfiguration) and + many IPv6 features. +
+
+
+ +
+

Planning Tips

+
+
Remember These
+ {#each ipv6PrefixLengthsContent.tips as tip, index (`tip-${index}`)} +
+
{tip}
+
+ {/each} +
+
+
+
diff --git a/src/routes/[lang]/reference/ipv6-privacy-addresses/+page.svelte b/src/routes/[lang]/reference/ipv6-privacy-addresses/+page.svelte new file mode 100644 index 00000000..abc79508 --- /dev/null +++ b/src/routes/[lang]/reference/ipv6-privacy-addresses/+page.svelte @@ -0,0 +1,364 @@ + + + + {ipv6PrivacyContent ? ipv6PrivacyContent.title : ''}{$t('common.meta.titleSeparator')}{$t( + 'common.meta.titleSuffix', + )} + + + +
+
+ {#if ipv6PrivacyContent} +
+

{ipv6PrivacyContent.title}

+

{ipv6PrivacyContent.description}

+
+ +
+

{ipv6PrivacyContent.sections.overview.title}

+

{ipv6PrivacyContent.sections.overview.content}

+
+ +
+

{ipv6PrivacyContent.sections.problem.title}

+

{ipv6PrivacyContent.sections.problem.content}

+
+ +
+

{ipv6PrivacyContent.addressTypes.title}

+ {#each ipv6PrivacyContent.addressTypes.types as type, index (`${type.type}-${index}`)} +
+
{type.type}
+
+
{$t('pages.ipv6Privacy.labels.formation')}: {type.formation}
+
{$t('pages.ipv6Privacy.labels.example')}: {type.example}
+
{$t('pages.ipv6Privacy.labels.privacyLevel')}: {type.privacy}
+ +

{$t('pages.ipv6Privacy.labels.characteristics')}:

+
    + {#each type.characteristics as characteristic, index (`characteristic-${index}`)} +
  • {characteristic}
  • + {/each} +
+
+
+ {/each} +
+ +
+

{ipv6PrivacyContent.howItWorks.title}

+ +

{$t('pages.ipv6Privacy.howItWorks.addressGenerationTitle')}

+
    + {#each ipv6PrivacyContent.howItWorks.addressGeneration as step, index (`gen-step-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('pages.ipv6Privacy.howItWorks.temporaryLifecycleTitle')}

+
    + {#each ipv6PrivacyContent.howItWorks.temporaryLifecycle as step, index (`lifecycle-step-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('pages.ipv6Privacy.howItWorks.defaultBehaviorTitle')}

+
    + {#each ipv6PrivacyContent.howItWorks.defaultBehavior as behavior, index (`behavior-${index}`)} +
  • {behavior}
  • + {/each} +
+
+ +
+

{ipv6PrivacyContent.lifetimes.title}

+ +
+
+
{$t('pages.ipv6Privacy.lifetimes.preferredLifetime.title')}
+
{ipv6PrivacyContent.lifetimes.preferredLifetime.description}
+
+ {$t('pages.ipv6Privacy.labels.typical')}: + {ipv6PrivacyContent.lifetimes.preferredLifetime.typical} +
+
+ {$t('pages.ipv6Privacy.labels.behavior')}: + {ipv6PrivacyContent.lifetimes.preferredLifetime.behavior} +
+
+ +
+
{$t('pages.ipv6Privacy.lifetimes.validLifetime.title')}
+
{ipv6PrivacyContent.lifetimes.validLifetime.description}
+
+ {$t('pages.ipv6Privacy.labels.typical')}: + {ipv6PrivacyContent.lifetimes.validLifetime.typical} +
+
+ {$t('pages.ipv6Privacy.labels.behavior')}: + {ipv6PrivacyContent.lifetimes.validLifetime.behavior} +
+
+ +
+
{$t('pages.ipv6Privacy.lifetimes.regenerationInterval.title')}
+
{ipv6PrivacyContent.lifetimes.regenerationInterval.description}
+
+ {$t('pages.ipv6Privacy.labels.typical')}: + {ipv6PrivacyContent.lifetimes.regenerationInterval.typical} +
+
+ {$t('pages.ipv6Privacy.labels.behavior')}: + {ipv6PrivacyContent.lifetimes.regenerationInterval.behavior} +
+
+ +
+
{$t('pages.ipv6Privacy.lifetimes.maxTempAddresses.title')}
+
{ipv6PrivacyContent.lifetimes.maxTempAddresses.description}
+
+ {$t('pages.ipv6Privacy.labels.typical')}: + {ipv6PrivacyContent.lifetimes.maxTempAddresses.typical} +
+
+ {$t('pages.ipv6Privacy.labels.behavior')}: + {ipv6PrivacyContent.lifetimes.maxTempAddresses.behavior} +
+
+
+
+ +
+

{ipv6PrivacyContent.osImplementations.title}

+ + {#each Object.entries(ipv6PrivacyContent.osImplementations) as [key, os] (key)} + {#if isOSImplementation(os)} +
+
{os.os}
+
+
+ {$t('pages.ipv6Privacy.labels.defaultBehavior')}: + {os.defaultBehavior} +
+ +

{$t('pages.ipv6Privacy.labels.configuration')}:

+ {#each os.configuration || [] as config, index (`config-${index}`)} + {config} + {/each} + + {#if (os as OSImplementation).values} +

{$t('pages.ipv6Privacy.labels.values')}:

+
    + {#each (os as OSImplementation).values! as value, index (`value-${index}`)} +
  • {value}
  • + {/each} +
+ {/if} + +

{$t('pages.ipv6Privacy.labels.commands')}:

+ {#each (os as OSImplementation).commands as command, index (`command-${index}`)} + {command} + {/each} + + {#if (os as OSImplementation).behavior} +
+ {$t('pages.ipv6Privacy.labels.behavior')}: + {(os as OSImplementation).behavior} +
+ {/if} +
+
+ {/if} + {/each} +
+ +
+

{ipv6PrivacyContent.identifyingAddresses.title}

+ + + + + + + + + + + {#each ipv6PrivacyContent.identifyingAddresses.methods as method, index (`method-${index}`)} + + + + + + + {/each} + +
{$t('pages.ipv6Privacy.labels.method')}{$t('pages.ipv6Privacy.labels.stable')}{$t('pages.ipv6Privacy.labels.temporary')}{$t('pages.ipv6Privacy.labels.example')}
{method.method}{method.stable}{method.temporary}{method.example}
+
+ +
+

{ipv6PrivacyContent.troubleshooting.title}

+ {#each ipv6PrivacyContent.troubleshooting.issues as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

{$t('pages.ipv6Privacy.labels.symptoms')}: {issue.symptoms.join(', ')}

+

{$t('pages.ipv6Privacy.labels.diagnosis')}: {issue.diagnosis}

+
{$t('pages.ipv6Privacy.labels.solutions')}:
+
    + {#each issue.solutions as solution, index (`solution-${index}`)} +
  • {solution}
  • + {/each} +
+
+
+ {/each} +
+ +
+

{ipv6PrivacyContent.securityConsiderations.title}

+ {#each ipv6PrivacyContent.securityConsiderations.aspects as security, index (`${security.aspect}-${index}`)} +
+
{security.aspect}
+
+

{$t('pages.ipv6Privacy.labels.benefits')}:

+
    + {#each security.benefits as benefit, index (`benefit-${index}`)} +
  • {benefit}
  • + {/each} +
+ +

+ {security.limitations + ? $t('pages.ipv6Privacy.labels.limitations') + : $t('pages.ipv6Privacy.labels.challenges')}: +

+
    + {#each security.limitations || security.challenges as item, index (`limitation-${index}`)} +
  • {item}
  • + {/each} +
+
+
+ {/each} +
+ +
+

{ipv6PrivacyContent.whenToUse.title}

+ {#each ipv6PrivacyContent.whenToUse.scenarios as scenario, index (`${scenario.scenario}-${index}`)} +
+
{scenario.scenario}
+
+
{$t('pages.ipv6Privacy.labels.recommendation')}: {scenario.recommendation}
+
{$t('pages.ipv6Privacy.labels.reasoning')}: {scenario.reasoning}
+
{$t('pages.ipv6Privacy.labels.configuration')}: {scenario.configuration}
+
+
+ {/each} +
+ +
+

{ipv6PrivacyContent.bestPractices.title}

+
    + {#each ipv6PrivacyContent.bestPractices.practices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

{ipv6PrivacyContent.commonMistakes.title}

+
    + {#each ipv6PrivacyContent.commonMistakes.mistakes as mistake, index (`mistake-${index}`)} +
  • {mistake}
  • + {/each} +
+
+ +
+

{ipv6PrivacyContent.quickReference.title}

+ +
+
+
{ipv6PrivacyContent.quickReference.addressTypesTitle}
+ {#each ipv6PrivacyContent.quickReference.addressTypes as type, index (`qr-type-${index}`)} +
{type}
+ {/each} +
+ +
+
{ipv6PrivacyContent.quickReference.identificationTitle}
+ {#each ipv6PrivacyContent.quickReference.identification as tip, index (`qr-id-${index}`)} +
{tip}
+ {/each} +
+
+ +
+
+
{ipv6PrivacyContent.quickReference.configurationTitle}
+ {#each ipv6PrivacyContent.quickReference.configuration as config, index (`qr-config-${index}`)} +
{config}
+ {/each} +
+ +
+
{ipv6PrivacyContent.quickReference.troubleshootingTitle}
+ {#each ipv6PrivacyContent.quickReference.troubleshooting as tip, index (`qr-trouble-${index}`)} +
{tip}
+ {/each} +
+
+ +
+
+ + {ipv6PrivacyContent.quickReference.keyRuleTitle} +
+
+ {ipv6PrivacyContent.quickReference.keyRule} +
+
+
+ +
+

{ipv6PrivacyContent.tools.title}

+
+ {#each ipv6PrivacyContent.tools.tools as tool, index (`${tool.tool}-${index}`)} +
+
{tool.tool}
+
{tool.purpose}
+
+ {/each} +
+
+ {/if} +
+
diff --git a/src/routes/[lang]/reference/link-local-apipa/+page.svelte b/src/routes/[lang]/reference/link-local-apipa/+page.svelte new file mode 100644 index 00000000..af71e65a --- /dev/null +++ b/src/routes/[lang]/reference/link-local-apipa/+page.svelte @@ -0,0 +1,280 @@ + + +
+
+
+

{linkLocalApipaContent.title}

+

{linkLocalApipaContent.description}

+
+ +
+

{linkLocalApipaContent.sections.overview.title}

+

{linkLocalApipaContent.sections.overview.content}

+
+ +
+

{linkLocalApipaContent.apipa.title}

+ +
+
{$t('pages.linkLocalApipa.apipa.addressRange.title')}
+
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.network')}: + {linkLocalApipaContent.apipa.range} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.fullRange')}: + {linkLocalApipaContent.apipa.fullRange} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.usableRange')}: + {linkLocalApipaContent.apipa.usableRange} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.reserved')}: + {linkLocalApipaContent.apipa.reservedAddresses.join(', ')} +
+
+
+ +

{linkLocalApipaContent.apipa.description}

+ +

{$t('pages.linkLocalApipa.apipa.whenUsedTitle')}

+
    + {#each linkLocalApipaContent.apipa.whenUsed as reason, index (`apipa-reason-${index}`)} +
  • {reason}
  • + {/each} +
+ +

{$t('pages.linkLocalApipa.apipa.howItWorksTitle')}

+
    + {#each linkLocalApipaContent.apipa.howItWorks as step, index (`apipa-step-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('pages.linkLocalApipa.apipa.characteristicsTitle')}

+
    + {#each linkLocalApipaContent.apipa.characteristics as characteristic, index (`apipa-char-${index}`)} +
  • {characteristic}
  • + {/each} +
+ +

{$t('pages.linkLocalApipa.apipa.troubleshootingTitle')}

+ {#each linkLocalApipaContent.apipa.troubleshooting as issue, index (`${issue.symptom}-${index}`)} +
+
+ + {issue.symptom} +
+
+

{$t('pages.linkLocalApipa.apipa.troubleshootingLabels.meaning')}: {issue.meaning}

+

{$t('pages.linkLocalApipa.apipa.troubleshootingLabels.solution')}: {issue.solution}

+
+
+ {/each} +
+ +
+

{linkLocalApipaContent.ipv6LinkLocal.title}

+ +
+
{$t('pages.linkLocalApipa.ipv6.addressRange.title')}
+
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.network')}: + {linkLocalApipaContent.ipv6LinkLocal.range} +
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.fullRange')}: + {linkLocalApipaContent.ipv6LinkLocal.fullRange} +
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.commonFormat')}: + {linkLocalApipaContent.ipv6LinkLocal.commonFormat} +
+
+
+ +

{linkLocalApipaContent.ipv6LinkLocal.description}

+ +

{$t('pages.linkLocalApipa.ipv6.addressFormationTitle')}

+
    + {#each linkLocalApipaContent.ipv6LinkLocal.formation as step, index (`ipv6-formation-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('pages.linkLocalApipa.ipv6.whenUsedTitle')}

+
    + {#each linkLocalApipaContent.ipv6LinkLocal.whenUsed as use, index (`ipv6-use-${index}`)} +
  • {use}
  • + {/each} +
+ +

{$t('pages.linkLocalApipa.ipv6.characteristicsTitle')}

+
    + {#each linkLocalApipaContent.ipv6LinkLocal.characteristics as characteristic, index (`ipv6-char-${index}`)} +
  • {characteristic}
  • + {/each} +
+ +

{$t('pages.linkLocalApipa.ipv6.typesTitle')}

+
+ {#each linkLocalApipaContent.ipv6LinkLocal.types as type, index (`${type.type}-${index}`)} +
+
{type.type}
+
{type.description}
+
+ {$t('pages.linkLocalApipa.ipv6.typeLabels.example')}: {type.example} +
+
{$t('pages.linkLocalApipa.ipv6.typeLabels.privacy')}: {type.privacy}
+
+ {/each} +
+
+ +
+

{$t('pages.linkLocalApipa.comparison.title')}

+ + + + + + + + + + + {#each linkLocalApipaContent.comparison as row, index (`${row.aspect}-${index}`)} + + + + + + {/each} + +
{$t('pages.linkLocalApipa.comparison.headers.aspect')}{$t('pages.linkLocalApipa.comparison.headers.ipv4Apipa')}{$t('pages.linkLocalApipa.comparison.headers.ipv6LinkLocal')}
{row.aspect}{row.ipv4}{row.ipv6}
+
+ +
+

{$t('pages.linkLocalApipa.practicalExamples.title')}

+ {#each linkLocalApipaContent.practicalExamples as example, index (`${example.scenario}-${index}`)} +
+
{example.scenario}
+
+
+ {$t('pages.linkLocalApipa.practicalExamples.labels.ipv4Behavior')}: + {example.ipv4Behavior} +
+
+ {$t('pages.linkLocalApipa.practicalExamples.labels.ipv6Behavior')}: + {example.ipv6Behavior} +
+
{$t('pages.linkLocalApipa.practicalExamples.labels.impact')}: {example.impact}
+
+
+ {/each} +
+ +
+

{$t('pages.linkLocalApipa.troubleshootingCommands.title')}

+ + + + + + + + + + + + {#each linkLocalApipaContent.troubleshootingCommands as cmd, index (`${cmd.purpose}-${index}`)} + + + + + + + {/each} + +
{$t('pages.linkLocalApipa.troubleshootingCommands.headers.purpose')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.windows')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.linux')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.macOS')}
{cmd.purpose}{cmd.windows}{cmd.linux}{cmd.macOS}
+
+ +
+

{$t('pages.linkLocalApipa.whenToWorry.title')}

+ {#each linkLocalApipaContent.whenToWorry as situation, index (`${situation.situation}-${index}`)} +
+
{situation.situation}
+
+
+ {$t('pages.linkLocalApipa.whenToWorry.labels.concernLevel')}: + {situation.concern} +
+
{$t('pages.linkLocalApipa.whenToWorry.labels.action')}: {situation.action}
+
+
+ {/each} +
+ +
+

{$t('pages.linkLocalApipa.bestPractices.title')}

+
    + {#each linkLocalApipaContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

{$t('pages.linkLocalApipa.commonMistakes.title')}

+
    + {#each linkLocalApipaContent.commonMistakes as mistake, index (`mistake-${index}`)} +
  • {mistake}
  • + {/each} +
+
+ +
+

{$t('pages.linkLocalApipa.quickReference.title')}

+ +
+
+
{$t('pages.linkLocalApipa.quickReference.recognitionTitle')}
+ {#each linkLocalApipaContent.quickReference.recognition as item, index (`recognition-${index}`)} +
{item}
+ {/each} +
+ +
+
{$t('pages.linkLocalApipa.quickReference.troubleshootingTitle')}
+ {#each linkLocalApipaContent.quickReference.troubleshooting as item, index (`qr-trouble-${index}`)} +
{item}
+ {/each} +
+
+ +
+
+ + {$t('pages.linkLocalApipa.quickReference.keyDifferenceTitle')} +
+
+ {$t('pages.linkLocalApipa.quickReference.keyDifferenceText')} +
+
+
+
+
diff --git a/src/routes/[lang]/reference/mtu-mss/+page.svelte b/src/routes/[lang]/reference/mtu-mss/+page.svelte new file mode 100644 index 00000000..8542665e --- /dev/null +++ b/src/routes/[lang]/reference/mtu-mss/+page.svelte @@ -0,0 +1,216 @@ + + +
+
+
+

{mtuMssContent.title}

+

{mtuMssContent.description}

+
+ +
+

{mtuMssContent.sections.overview.title}

+

{mtuMssContent.sections.overview.content}

+ +
+
+ + Key Formula +
+
+ MSS = MTU - IP Header - TCP Header
+ For IPv4: MSS = MTU - 20 - 20 = MTU - 40 bytes +
+
+
+ +
+

Common MTU/MSS Values

+ + + + + + + + + + + + {#each mtuMssContent.commonValues as value, index (`${value.medium}-${index}`)} + + + + + + + + {/each} + +
MediumMTUMSSUsageNotes
{value.medium}{value.mtu}{value.mss}{value.usage}{value.notes}
+
+ +
+

{mtuMssContent.calculations.title}

+ {#each mtuMssContent.calculations.examples as example, index (`${example.scenario}-${index}`)} +
+
{example.scenario}
+
+
MTU: {example.mtu} bytes
+
IP Header: {example.ipHeader} bytes
+
TCP Header: {example.tcpHeader} bytes
+
Resulting MSS: {example.mss} bytes
+
Calculation: {example.calculation}
+
+
+ {/each} +
+ +
+

Protocol Overheads

+ + + + + + + + + + {#each mtuMssContent.overheads as overhead, index (`${overhead.protocol}-${index}`)} + + + + + + {/each} + +
Protocol/HeaderOverhead (Bytes)Notes
{overhead.protocol}{overhead.overhead}{overhead.notes}
+
+ +
+

{mtuMssContent.discovery.title}

+

{mtuMssContent.discovery.description}

+ +

PMTU Discovery Process

+
    + {#each mtuMssContent.discovery.process as step, index (`discovery-step-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

Common Issues

+
    + {#each mtuMssContent.discovery.issues as issue, index (`discovery-issue-${index}`)} +
  • {issue}
  • + {/each} +
+
+ +
+

Troubleshooting Common Issues

+ {#each mtuMssContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

Cause: {issue.cause}

+

Solution: {issue.solution}

+
+
+ {/each} +
+ +
+

Useful Commands

+ +

Checking MTU Settings

+ + + + + + + + + + {#each mtuMssContent.commands as cmd, index (`${cmd.platform}-${index}`)} + + + + + + {/each} + +
PlatformCommandPurpose
{cmd.platform}{cmd.command}{cmd.purpose}
+ +

Testing MTU Size

+ + + + + + + + + + {#each mtuMssContent.testCommands as cmd, index (`${cmd.purpose}-${index}`)} + + + + + + {/each} + +
PlatformCommandPurpose
{cmd.platform}{cmd.command}{cmd.purpose}
+
+ +
+

Best Practices

+
    + {#each mtuMssContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+ +
+
+ + Performance Tip +
+
+ Mismatched MTU sizes can cause significant performance issues. Always ensure consistent MTU values across your + network path, especially for high-throughput applications. +
+
+
+ +
+

Quick Reference

+
+
Common Values to Remember
+ {#each mtuMssContent.quickReference as value, index (`value-${index}`)} +
+
{value}
+
+ {/each} +
+ +
+
+ + Important Note +
+
+ When troubleshooting connectivity issues, especially with VPNs or tunnels, MTU/MSS mismatches are often the + culprit. Test with smaller packet sizes if large transfers fail but small ones succeed. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/multicast/+page.svelte b/src/routes/[lang]/reference/multicast/+page.svelte new file mode 100644 index 00000000..9ec93bbf --- /dev/null +++ b/src/routes/[lang]/reference/multicast/+page.svelte @@ -0,0 +1,189 @@ + + +
+
+
+

{multicastContent.title}

+

{multicastContent.description}

+
+ +
+

{multicastContent.sections.overview.title}

+

{multicastContent.sections.overview.content}

+
+ +
+

{multicastContent.ipv4Multicast.title}

+

Range: {multicastContent.ipv4Multicast.range}

+ + {#each multicastContent.ipv4Multicast.classes as multicastClass, index (`${multicastClass.name}-${index}`)} +
+
{multicastClass.name}
+
+
Range: {multicastClass.range}
+
Description: {multicastClass.description}
+
Scope: {multicastClass.scope}
+
Examples:
+ {#each multicastClass.examples as example, index (`example-${index}`)} +
{example}
+ {/each} +
+
+ {/each} +
+ +
+

{multicastContent.ipv6Multicast.title}

+

Range: {multicastContent.ipv6Multicast.range}

+ +

Address Structure

+

Format: {multicastContent.ipv6Multicast.structure.format}

+ +
+
+
Flag Bits
+ {#each multicastContent.ipv6Multicast.structure.flags as flag, index (`flag-${index}`)} +
{flag.bit} - {flag.meaning}
+ {/each} +
+ +
+
Scope Values
+ {#each multicastContent.ipv6Multicast.structure.scopes as scope, index (`scope-${index}`)} +
{scope.code} - {scope.name}
+ {/each} +
+
+ +

Well-Known IPv6 Multicast Addresses

+ + + + + + + + + + {#each multicastContent.ipv6Multicast.wellKnown as addr, index (`${addr.address}-${index}`)} + + + + + + {/each} + +
AddressNameDescription
{addr.address}{addr.name}{addr.description}
+
+ +
+

Common Protocol Multicast Addresses

+ + + + + + + + + + + {#each multicastContent.commonProtocols as protocol, index (`${protocol.protocol}-${index}`)} + + + + + + + {/each} + +
ProtocolIPv4IPv6Purpose
{protocol.protocol}{protocol.ipv4}{protocol.ipv6}{protocol.purpose}
+
+ +
+

Important Limitations

+ {#each multicastContent.limitations as limitation, index (`${limitation.title}-${index}`)} +
+
+ + {limitation.title} +
+
+

{limitation.description}

+
    + {#each limitation.details as detail, index (`detail-${index}`)} +
  • {detail}
  • + {/each} +
+
+
+ {/each} +
+ +
+

Troubleshooting Common Issues

+ {#each multicastContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
{issue.issue}
+
+
Common Causes:
+
    + {#each issue.causes as cause, index (`cause-${index}`)} +
  • {cause}
  • + {/each} +
+
Solutions:
+
    + {#each issue.solutions as solution, index (`solution-${index}`)} +
  • {solution}
  • + {/each} +
+
+
+ {/each} +
+ +
+

Best Practices

+
    + {#each multicastContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

Quick Reference

+
+
+
IPv4 Quick List
+ {#each multicastContent.quickReference.ipv4 as addr, index (`ipv4-${index}`)} +
{addr}
+ {/each} +
+ +
+
IPv6 Quick List
+ {#each multicastContent.quickReference.ipv6 as addr, index (`ipv6-${index}`)} +
{addr}
+ {/each} +
+
+ +
+
+ + Key Remember +
+
+ Most multicast addresses are designed for local subnet use only. Without proper multicast routing + configuration, traffic won't cross router boundaries. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/network-classes/+page.svelte b/src/routes/[lang]/reference/network-classes/+page.svelte new file mode 100644 index 00000000..9404170c --- /dev/null +++ b/src/routes/[lang]/reference/network-classes/+page.svelte @@ -0,0 +1,144 @@ + + + +
+
+

Network Classes

+

Class A/B/C overview with default masks, ranges, and typical usage.

+
+ +
+ {#each Object.entries(NETWORK_CLASSES) as [className, classInfo] (className)} + +
+
+
+
+ {className} +
+
+

Class {className}

+ + {classInfo.defaultMask} (/{classInfo.cidr}) + +
+
+ + {classInfo.range.split(' - ')[0]} - {classInfo.range.split(' - ')[1]} + +
+ +

+ {classInfo.description} +

+ +

+ Typical Usage: + {classInfo.usage} +

+
+
+ {/each} +
+
+ + diff --git a/src/routes/[lang]/reference/ports/+page.svelte b/src/routes/[lang]/reference/ports/+page.svelte new file mode 100644 index 00000000..b2fbbd0d --- /dev/null +++ b/src/routes/[lang]/reference/ports/+page.svelte @@ -0,0 +1,165 @@ + + +
+
+
+

{commonPortsContent.title}

+

{commonPortsContent.description}

+
+ +
+

Port Ranges

+ + + + + + + + + + {#each commonPortsContent.ranges as range, index (index)} + + + + + + {/each} + +
RangeNameDescription
{range.range}{range.name}{range.description}
+
+ +
+

Well-Known Ports (0-1023)

+ + + + + + + + + + + {#each commonPortsContent.wellKnown as port (port.port)} + + + + + + + {/each} + +
PortProtocolServiceDescription
{port.port}{port.protocol}{port.service}{port.description}
+
+ +
+

Registered Ports (1024-49151)

+ + + + + + + + + + + {#each commonPortsContent.registered as port (port.port)} + + + + + + + {/each} + +
PortProtocolServiceDescription
{port.port}{port.protocol}{port.service}{port.description}
+
+ +
+

Common Service Categories

+ +
+
+
Web Services
+ {#each commonPortsContent.categories.web as service, index (index)} +
{service.ports} - {service.service}
+
+ {service.secure ? 'Secure' : 'Not secure'} +
+ {/each} +
+ +
+
Email Services
+ {#each commonPortsContent.categories.email as service, index (index)} +
{service.ports} - {service.service}
+
+ {service.secure ? 'Secure' : 'Not secure'} +
+ {/each} +
+ +
+
Remote Access
+ {#each commonPortsContent.categories.remote as service, index (index)} +
{service.ports} - {service.service}
+
+ {service.secure ? 'Secure' : 'Not secure'} +
+ {/each} +
+ +
+
Database Services
+ {#each commonPortsContent.categories.database as service, index (index)} +
{service.ports} - {service.service}
+
+ {service.secure ? 'Secure' : 'Not secure'} +
+ {/each} +
+
+
+ +
+

Important Security Tips

+
+
Remember These
+ {#each commonPortsContent.tips as tip, index (index)} +
+
{tip}
+
+ {/each} +
+ +
+
+ + Security Note +
+
+ Many services have both secure and insecure versions. Always use the secure versions (HTTPS, SSH, FTPS, etc.) + when possible, especially over untrusted networks. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/private-vs-public-ip/+page.svelte b/src/routes/[lang]/reference/private-vs-public-ip/+page.svelte new file mode 100644 index 00000000..8ca80e7a --- /dev/null +++ b/src/routes/[lang]/reference/private-vs-public-ip/+page.svelte @@ -0,0 +1,240 @@ + + +
+
+
+

{$t('title')}

+

{$t('description')}

+
+ +
+

{$t('sections.overview.title')}

+

{$t('sections.overview.content')}

+
+ +
+

{$t('privateRanges.title')}

+ {#each privateVsPublicContent.privateRanges as range, index (`${range.range}-${index}`)} +
+
{range.range} - {range.class}
+
+
{$t('privateRanges.labels.fullRange')} {range.fullRange}
+
{$t('privateRanges.labels.totalAddresses')} {range.addresses}
+
{$t('privateRanges.labels.commonUse')} {range.commonUse}
+
{$t('privateRanges.labels.examples')}
+ {#each range.examples as example, index (`example-${index}`)} + {example} + {/each} +
+
+ {/each} +
+ +
+

{$t('publicRanges.title')}

+

{$t('publicRanges.description')}

+ +

{$t('publicRanges.characteristics.title')}

+
    + {#each $t('publicRanges.characteristics.items') as characteristic, index (`char-${index}`)} +
  • {characteristic}
  • + {/each} +
+ +

{$t('publicRanges.examples.title')}

+ + + + + + + + + {#each privateVsPublicContent.publicRanges.examples as example, index (`public-example-${index}`)} + + + + + {/each} + +
{$t('publicRanges.examples.headers.publicIp')}{$t('publicRanges.examples.headers.ownerService')}
{example.ip}{example.owner}
+
+ +
+

{$t('natImplications.title')}

+ +
+
+
{$t('natImplications.privateToPublic.title')}
+
{$t('natImplications.privateToPublic.description')}
+ +

{$t('natImplications.privateToPublic.process.title')}

+
    + {#each $t('natImplications.privateToPublic.process.steps') as step, index (`nat-step-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

{$t('natImplications.privateToPublic.benefits.title')}

+
    + {#each $t('natImplications.privateToPublic.benefits.items') as benefit, index (`benefit-${index}`)} +
  • {benefit}
  • + {/each} +
+
+ +
+
{$t('natImplications.publicToPrivate.title')}
+
{$t('natImplications.publicToPrivate.description')}
+ +

{$t('natImplications.publicToPrivate.challenges.title')}

+
    + {#each $t('natImplications.publicToPrivate.challenges.items') as challenge, index (`challenge-${index}`)} +
  • {challenge}
  • + {/each} +
+ +

{$t('natImplications.publicToPrivate.solutions.title')}

+
    + {#each $t('natImplications.publicToPrivate.solutions.items') as solution, index (`solution-${index}`)} +
  • {solution}
  • + {/each} +
+
+
+
+ +
+

{$t('identification.title')}

+ + + + + + + + + + + + {#each privateVsPublicContent.identification.quickCheck as method, index (`method-${index}`)} + + + + + + + {/each} + +
{$t('identification.headers.method')}Description{$t('identification.headers.privateIndicator')}{$t('identification.headers.publicIndicator')}
{method.method}{method.description}{method.private}{method.public}
+ +

{$t('tools.title')}

+
+ {#each privateVsPublicContent.identification.tools as tool, index (`${tool.tool}-${index}`)} +
+
{tool.tool}
+
{tool.purpose}
+
+ {/each} +
+
+ +
+

{$t('commonScenarios.title')}

+ {#each privateVsPublicContent.commonScenarios as scenario, index (`${scenario.scenario}-${index}`)} +
+
{scenario.scenario}
+
+
{$t('commonScenarios.labels.setup')} {scenario.setup}
+
{$t('commonScenarios.labels.privateIps')} {scenario.privateIPs}
+
{$t('commonScenarios.labels.publicIp')} {scenario.publicIP}
+
{$t('commonScenarios.labels.natBehavior')} {scenario.natBehavior}
+
+
+ {/each} +
+ +
+

{$t('troubleshooting.title')}

+ {#each privateVsPublicContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

{$t('troubleshooting.labels.possibleCauses')} {issue.possibleCauses.join(', ')}

+

{$t('troubleshooting.labels.diagnosis')} {issue.diagnosis}

+

{$t('troubleshooting.labels.solution')} {issue.solution}

+
+
+ {/each} +
+ +
+

{$t('security.title')}

+ {#each privateVsPublicContent.securityConsiderations as security, index (`${security.aspect}-${index}`)} +
+
{security.aspect}
+
+
    + {#each security.considerations as consideration, index (`consideration-${index}`)} +
  • {consideration}
  • + {/each} +
+
+
+ {/each} +
+ +
+

{$t('bestPractices.title')}

+
    + {#each privateVsPublicContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

{$t('quickReference.title')}

+ +
+
+
{$t('quickReference.privateRanges.title')}
+ {#each privateVsPublicContent.quickReference.privateRanges as range, index (`qr-range-${index}`)} +
{range}
+ {/each} +
+ +
+
{$t('quickReference.identificationTips.title')}
+ {#each privateVsPublicContent.quickReference.identificationTips as tip, index (`qr-tip-${index}`)} +
{tip}
+ {/each} +
+
+ +
+
+ + Key Rule +
+
+ If an IP starts with 10, 172.16-31, or 192.168, it's private. Everything else (except other reserved ranges) + is public. Private IPs need NAT to reach the internet. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/reserved-ranges/+page.svelte b/src/routes/[lang]/reference/reserved-ranges/+page.svelte new file mode 100644 index 00000000..00ac49dd --- /dev/null +++ b/src/routes/[lang]/reference/reserved-ranges/+page.svelte @@ -0,0 +1,99 @@ + + + +
+
+

Reserved Ranges

+

Special-purpose IPv4 ranges (loopback, private, link-local, multicast, etc.).

+
+ +
+ {#each Object.entries(RESERVED_RANGES) as [rangeName, rangeInfo] (rangeName)} + +
+
+
+

{rangeInfo.range}

+ {rangeInfo.description} +
+ {rangeInfo.rfc} +
+ + {#if rangeName.includes('PRIVATE')} +
+ Private Network: Not routed on the public Internet +
+ {/if} +
+
+ {/each} +
+
+ + diff --git a/src/routes/[lang]/reference/reverse-dns/+page.svelte b/src/routes/[lang]/reference/reverse-dns/+page.svelte new file mode 100644 index 00000000..32a24906 --- /dev/null +++ b/src/routes/[lang]/reference/reverse-dns/+page.svelte @@ -0,0 +1,192 @@ + + +
+
+
+

{reverseDnsContent.title}

+

{reverseDnsContent.description}

+
+ +
+

{reverseDnsContent.sections.overview.title}

+

{reverseDnsContent.sections.overview.content}

+
+ +
+

{reverseDnsContent.sections.howWorks.title}

+

{reverseDnsContent.sections.howWorks.content}

+
+ +
+

{reverseDnsContent.ipv4Reverse.title}

+ +

Process Steps

+
    + {#each reverseDnsContent.ipv4Reverse.process as step, index (`ipv4-process-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

IPv4 Examples

+ {#each reverseDnsContent.ipv4Reverse.examples as example, index (`${example.ip}-${index}`)} +
+
IP Address: {example.ip}
+
+
Reverse Name: {example.reversed}
+
PTR Record: {example.ptrRecord}
+
Explanation: {example.explanation}
+
+
+ {/each} + +

Network Delegation

+

{reverseDnsContent.ipv4Reverse.delegation.explanation}

+ + + + + + + + + + {#each reverseDnsContent.ipv4Reverse.delegation.examples as example, index (`delegation-${index}`)} + + + + + + {/each} + +
NetworkReverse ZoneDescription
{example.network}{example.zone}{example.description}
+
+ +
+

{reverseDnsContent.ipv6Reverse.title}

+ +

Process Steps

+
    + {#each reverseDnsContent.ipv6Reverse.process as step, index (`ipv6-process-${index}`)} +
  1. {step}
  2. + {/each} +
+ +

IPv6 Examples

+ {#each reverseDnsContent.ipv6Reverse.examples as example, index (`${example.ip}-${index}`)} +
+
IP Address: {example.ip}
+
+
Expanded: {example.expanded}
+
+ Reverse Name: + {example.nibbles} +
+
PTR Record: {example.ptrRecord}
+
Note: {example.explanation}
+
+
+ {/each} + +
+
+ + IPv6 Complexity +
+
+ IPv6 reverse DNS names are much longer than IPv4 because each hex digit becomes a separate label. A single + IPv6 address creates a 72-character reverse DNS name! +
+
+
+ +
+

{reverseDnsContent.practicalExamples.title}

+ +

Command Examples

+ + + + + + + + + + {#each reverseDnsContent.practicalExamples.digExamples as example, index (`dig-${index}`)} + + + + + + {/each} + +
CommandDescriptionExpected Result
{example.command}{example.description}{example.expectedResult}
+ +

Common Use Cases

+
    + {#each reverseDnsContent.practicalExamples.commonChecks as check, index (`check-${index}`)} +
  • {check}
  • + {/each} +
+
+ +
+

Troubleshooting Common Issues

+ {#each reverseDnsContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

Causes: {issue.causes.join(', ')}

+

Solutions: {issue.solutions.join(', ')}

+
+
+ {/each} +
+ +
+

Best Practices

+
    + {#each reverseDnsContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

Quick Reference & Tools

+ +
+
+
IPv4 Quick Examples
+ {#each reverseDnsContent.quickReference.ipv4 as example, index (`qr-ipv4-${index}`)} +
{example}
+ {/each} +
+ +
+
IPv6 Quick Examples
+ {#each reverseDnsContent.quickReference.ipv6 as example, index (`qr-ipv6-${index}`)} +
{example}
+ {/each} +
+
+ +

Useful Tools

+
+ {#each reverseDnsContent.tools as tool, index (`${tool.name}-${index}`)} +
+
{tool.name}
+
{tool.description}
+
+ {/each} +
+
+
+
diff --git a/src/routes/[lang]/reference/reverse-zones/+page.svelte b/src/routes/[lang]/reference/reverse-zones/+page.svelte new file mode 100644 index 00000000..2b8f1756 --- /dev/null +++ b/src/routes/[lang]/reference/reverse-zones/+page.svelte @@ -0,0 +1,297 @@ + + +
+
+ {#if reverseZonesContent} +
+

{reverseZonesContent.title}

+

{reverseZonesContent.description}

+
+ +
+

{reverseZonesContent.sections.overview.title}

+

{reverseZonesContent.sections.overview.content}

+
+ +
+

{reverseZonesContent.sections.delegation.title}

+

{reverseZonesContent.sections.delegation.content}

+
+ +
+

{reverseZonesContent.ipv4Zones.title}

+ +

{$t('pages.reverseZones.ipv4Zones.classfullBoundariesTitle')}

+ + + + + + + + + + + + {#each reverseZonesContent.ipv4Zones.classfullBoundaries as boundary, index (`${boundary.cidr}-${index}`)} + + + + + + + + {/each} + +
{$t('pages.reverseZones.ipv4Zones.tableHeaders.cidr')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.example')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.reverseZone')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.description')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.delegation')}
{boundary.cidr}{boundary.example}{boundary.reverseZone}{boundary.description}{boundary.delegation}
+ +

{$t('pages.reverseZones.ipv4Zones.classlessDelegationTitle')}

+ {#each reverseZonesContent.ipv4Zones.classlessDelegation as delegation, index (`${delegation.cidr}-${index}`)} +
+
{delegation.cidr} - {delegation.example}
+
+
{$t('pages.reverseZones.labels.addresses')}: {delegation.addresses}
+
{$t('pages.reverseZones.labels.problem')}: {delegation.problem}
+
{$t('pages.reverseZones.labels.solution')}: {delegation.solution}
+
{$t('pages.reverseZones.labels.zoneNames')}:
+ {#each delegation.zones as zone, index (`zone-${index}`)} + {zone} + {/each} +
+
+ {/each} + +

{$t('pages.reverseZones.ipv4Zones.practicalExamplesTitle')}

+ {#each reverseZonesContent.ipv4Zones.practicalExamples as example, index (`${example.network}-${index}`)} +
+
{example.scenario}
+
+
{$t('pages.reverseZones.labels.network')}: {example.network}
+
+ {$t('pages.reverseZones.labels.reverseZone')}: {example.reverseZone} +
+ {#if example.reverseZones} +
{$t('pages.reverseZones.labels.reverseZones')}:
+ {#each example.reverseZones as zone, index (`rz-${index}`)} + {zone} + {/each} +
{$t('pages.reverseZones.labels.description')}: {example.description}
+ {:else} +
{$t('pages.reverseZones.labels.ptrRecords')}:
+ {#each example.ptrRecords as record, index (`ptr-${index}`)} + {record} + {/each} + {/if} +
{$t('pages.reverseZones.labels.delegation')}: {example.delegation}
+
+
+ {/each} +
+ +
+

{reverseZonesContent.ipv6Zones.title}

+ +

{$t('pages.reverseZones.ipv6Zones.nibbleBoundariesTitle')}

+ + + + + + + + + + + + {#each reverseZonesContent.ipv6Zones.nibbleBoundaries as boundary, index (`${boundary.cidr}-${index}`)} + + + + + + + + {/each} + +
{$t('pages.reverseZones.ipv4Zones.tableHeaders.cidr')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.example')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.reverseZone')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.description')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.delegation')}
{boundary.cidr}{boundary.example}{boundary.reverseZone}{boundary.description}{boundary.delegation}
+ +

{$t('pages.reverseZones.ipv6Zones.practicalExamplesTitle')}

+ {#each reverseZonesContent.ipv6Zones.practicalExamples as example, index (`${example.network}-${index}`)} +
+
{example.scenario}
+
+
{$t('pages.reverseZones.labels.network')}: {example.network}
+
+ {$t('pages.reverseZones.labels.masterZone')}: {example.reverseZone} +
+
{$t('pages.reverseZones.labels.subZones')}:
+ {#each example.subZones as zone, index (`subzone-${index}`)} + {zone} + {/each} +
{$t('pages.reverseZones.labels.management')}: {example.management}
+
+
+ {/each} +
+ +
+

{reverseZonesContent.zoneCreation.title}

+ +
+
+
IPv4 Example ({reverseZonesContent.zoneCreation.ipv4Example.network})
+
+ {$t('pages.reverseZones.labels.zoneName')}: + {reverseZonesContent.zoneCreation.ipv4Example.zoneName} +
+ +

{$t('pages.reverseZones.labels.zoneFile')}:

+
{reverseZonesContent.zoneCreation.ipv4Example.zoneFile}
+ +

{$t('pages.reverseZones.labels.explanation')}:

+
    + {#each reverseZonesContent.zoneCreation.ipv4Example.explanation as point, index (`ipv4-point-${index}`)} +
  • {point}
  • + {/each} +
+
+ +
+
IPv6 Example ({reverseZonesContent.zoneCreation.ipv6Example.network})
+
+ {$t('pages.reverseZones.labels.zoneName')}: + {reverseZonesContent.zoneCreation.ipv6Example.zoneName} +
+ +

{$t('pages.reverseZones.labels.zoneFile')}:

+
{reverseZonesContent.zoneCreation.ipv6Example.zoneFile}
+ +

{$t('pages.reverseZones.labels.explanation')}:

+
    + {#each reverseZonesContent.zoneCreation.ipv6Example.explanation as point, index (`ipv6-point-${index}`)} +
  • {point}
  • + {/each} +
+
+
+
+ +
+

{$t('pages.reverseZones.delegationScenarios.title')}

+ {#each reverseZonesContent.delegationScenarios as scenario, index (`${scenario.scenario}-${index}`)} +
+
{scenario.scenario}
+
+
{$t('pages.reverseZones.labels.delegation')}: {scenario.delegation}
+ + {#if scenario.customerActions} +
{$t('pages.reverseZones.labels.customerActions')}:
+
    + {#each scenario.customerActions as action, index (`customer-${index}`)} +
  • {action}
  • + {/each} +
+ +
{$t('pages.reverseZones.labels.ispActions')}:
+
    + {#each scenario.ispActions as action, index (`isp-${index}`)} +
  • {action}
  • + {/each} +
+ {:else} +
{$t('pages.reverseZones.labels.process')}:
+
    + {#each scenario.process as step, index (`process-${index}`)} +
  1. {step}
  2. + {/each} +
+ {/if} +
+
+ {/each} +
+ +
+

{$t('pages.reverseZones.troubleshooting.title')}

+ {#each reverseZonesContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

{$t('pages.reverseZones.labels.possibleCauses')}: {issue.causes.join(', ')}

+

{$t('pages.reverseZones.labels.diagnosis')}: {issue.diagnosis}

+

{$t('pages.reverseZones.labels.solution')}: {issue.solution}

+
+
+ {/each} +
+ +
+

{$t('pages.reverseZones.bestPractices.title')}

+
    + {#each reverseZonesContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

{$t('pages.reverseZones.quickReference.title')}

+ +
+
+
{$t('pages.reverseZones.quickReference.zoneFormulasTitle')}
+ {#each reverseZonesContent.quickReference.zoneFormulas as formula, index (`formula-${index}`)} +
{formula}
+ {/each} +
+ +
+
{$t('pages.reverseZones.quickReference.essentialRecordsTitle')}
+ {#each reverseZonesContent.quickReference.essentialRecords as record, index (`record-${index}`)} +
{record}
+ {/each} +
+
+ +
+
+ + {$t('pages.reverseZones.labels.keyRuleTitle')} +
+
+ {$t('pages.reverseZones.quickReference.keyRule')} +
+
+
+ +
+

{$t('pages.reverseZones.testingTools.title')}

+
+ {#each reverseZonesContent.tools as tool, index (`${tool.tool}-${index}`)} +
+
{tool.tool}
+
{tool.purpose}
+
+ {/each} +
+
+ {/if} +
+
diff --git a/src/routes/[lang]/reference/special-use-ipv4/+page.svelte b/src/routes/[lang]/reference/special-use-ipv4/+page.svelte new file mode 100644 index 00000000..0994f6d1 --- /dev/null +++ b/src/routes/[lang]/reference/special-use-ipv4/+page.svelte @@ -0,0 +1,107 @@ + + +
+
+
+

{specialIPv4Content.title}

+

{specialIPv4Content.description}

+
+ +
+

Complete Special-Use IPv4 Ranges

+ + + + + + + + + + + + {#each specialIPv4Content.ranges as range, rangeIdx (`${range.network}-${rangeIdx}`)} + + + + + + + + {/each} + +
NetworkPurposeRFCRoutableDescription
{range.network}{range.purpose}{range.rfc} + {#if range.routable} + Yes + {:else} + No + {/if} + {range.description}
+
+ +
+

Common Address Categories

+ +
+
+
Private Networks (RFC 1918)
+ {#each specialIPv4Content.categories.private as network, privIdx (`${network}-${privIdx}`)} +
{network}
+ {/each} +
Never routed on the public internet
+
+ +
+
Test Networks (RFC 5737)
+ {#each specialIPv4Content.categories.testing as network, testIdx (`${network}-${testIdx}`)} +
{network}
+ {/each} +
Safe for documentation and examples
+
+ +
+
Carrier-Grade NAT
+ {#each specialIPv4Content.categories.cgnat as network, cgnatIdx (`${network}-${cgnatIdx}`)} +
{network}
+ {/each} +
ISP shared addressing space
+
+ +
+
Special Purpose
+ {#each specialIPv4Content.categories.special as network, specIdx (`${network}-${specIdx}`)} +
{network}
+ {/each} +
Loopback, link-local, multicast
+
+
+
+ +
+

Quick Recognition Tips

+
+
What Each Range Means
+ {#each specialIPv4Content.quickTips as tip, tipIdx (`${tip}-${tipIdx}`)} +
+
{tip}
+
+ {/each} +
+ +
+
+ + Important Note +
+
+ If you see 100.64.x.x addresses, your ISP is using Carrier-Grade NAT (CGNAT). This can cause issues with port + forwarding, gaming, and some applications that require direct connectivity. +
+
+
+
+
diff --git a/src/routes/[lang]/reference/supernetting/+page.svelte b/src/routes/[lang]/reference/supernetting/+page.svelte new file mode 100644 index 00000000..d4d5062f --- /dev/null +++ b/src/routes/[lang]/reference/supernetting/+page.svelte @@ -0,0 +1,140 @@ + + +
+
+
+

{supernetContent.title}

+

{supernetContent.description}

+
+ +
+

{supernetContent.sections.whatIs.title}

+

{supernetContent.sections.whatIs.content}

+ +
+
+ + Main Goal +
+
+ Reduce the number of routes in routing tables while maintaining connectivity to all networks. +
+
+
+ +
+

{supernetContent.sections.requirements.title}

+

{supernetContent.sections.requirements.content}

+
+ +
+

Summarization Examples

+ {#each supernetContent.examples as example, exIdx (`${example.title}-${exIdx}`)} +
+
{example.title}
+
+
Individual Networks:
+ {#each example.networks as network, netIdx (`${network}-${netIdx}`)} +
{network}
+ {/each} +
↓ Summarizes to ↓
+
{example.summary}
+
{example.explanation} - {example.addresses}
+
+
+ {/each} +
+ +
+

{supernetContent.stepByStep.title}

+
    + {#each supernetContent.stepByStep.steps as step, stepIdx (`${step}-${stepIdx}`)} +
  1. {step}
  2. + {/each} +
+
+ +
+

{supernetContent.binaryExample.title}

+

{supernetContent.binaryExample.scenario}

+ + + + + + + + + + {#each supernetContent.binaryExample.binary as row, rowIdx (`${row.network}-${rowIdx}`)} + + + + + {/each} + +
NetworkBinary Representation
{row.network}{row.binary}
+ +
+
+ + Analysis +
+
+ {supernetContent.binaryExample.analysis} +
+
+
+ +
+

Benefits of Route Summarization

+
    + {#each supernetContent.benefits as benefit, benIdx (`${benefit}-${benIdx}`)} +
  • {benefit}
  • + {/each} +
+
+ +
+

Common Pitfalls

+ {#each supernetContent.pitfalls as pitfall, pitIdx (`${pitfall.title}-${pitIdx}`)} +
+
+ + {pitfall.title} +
+
+

Problem: {pitfall.problem}

+

Example: {pitfall.example}

+
+
+ {/each} +
+ +
+

Quick Reference Table

+ + + + + + + + + + {#each supernetContent.quickReference as row, refIdx (`${row.networks}-${refIdx}`)} + + + + + + {/each} + +
Input NetworksSummary PrefixRoutes Saved
{row.networks}{row.summary}{row.saves}
+
+
+
diff --git a/src/routes/[lang]/reference/vlsm/+page.svelte b/src/routes/[lang]/reference/vlsm/+page.svelte new file mode 100644 index 00000000..6ced2c47 --- /dev/null +++ b/src/routes/[lang]/reference/vlsm/+page.svelte @@ -0,0 +1,101 @@ + + +
+
+
+

{vlsmContent.title}

+

{vlsmContent.description}

+
+ +
+

{vlsmContent.sections.whatIs.title}

+

{vlsmContent.sections.whatIs.content}

+
+ +
+

{vlsmContent.sections.whenWhy.title}

+

{vlsmContent.sections.whenWhy.content}

+ +
+
+ + Key Benefit +
+
+ VLSM prevents IP address waste by letting you create subnets that are exactly the right size for each purpose. +
+
+
+ +
+

{vlsmContent.sections.howItWorks.title}

+

{vlsmContent.sections.howItWorks.content}

+
+ +
+

{vlsmContent.example.title}

+

{vlsmContent.example.scenario}

+ +
+
+
Requirements
+
    + {#each vlsmContent.example.requirements as req, reqIdx (`${req.name}-${reqIdx}`)} +
  • {req.name}: {req.hosts} hosts (needs {req.needsPrefix})
  • + {/each} +
+
+ +
+
VLSM Solution
+
    + {#each vlsmContent.example.solution as subnet, subIdx (`${subnet.subnet}-${subIdx}`)} +
  • {subnet.subnet} - {subnet.use} ({subnet.hosts})
  • + {/each} +
+
+
+
+ +
+

Common Pitfalls and Solutions

+ {#each vlsmContent.pitfalls as pitfall, pitIdx (`${pitfall.title}-${pitIdx}`)} +
+
+ + {pitfall.title} +
+
+

Problem: {pitfall.problem}

+

Solution: {pitfall.solution}

+
+
+ {/each} +
+ +
+

Best Practices

+
    + {#each vlsmContent.bestPractices as practice, pracIdx (`${practice}-${pracIdx}`)} +
  • {practice}
  • + {/each} +
+
+ +
+

Quick Tips

+
+
Remember These
+ {#each vlsmContent.tips as tip, tipIdx (`${tip}-${tipIdx}`)} +
+
{tip}
+
+ {/each} +
+
+
+
diff --git a/src/routes/[lang]/reference/wildcard-masks/+page.svelte b/src/routes/[lang]/reference/wildcard-masks/+page.svelte new file mode 100644 index 00000000..fdd65b1c --- /dev/null +++ b/src/routes/[lang]/reference/wildcard-masks/+page.svelte @@ -0,0 +1,203 @@ + + +
+
+
+

{wildcardMasksContent.title}

+

{wildcardMasksContent.description}

+
+ +
+

{wildcardMasksContent.sections.overview.title}

+

{wildcardMasksContent.sections.overview.content}

+
+ +
+

{wildcardMasksContent.sections.difference.title}

+

{wildcardMasksContent.sections.difference.content}

+ +
+
+ + Key Difference +
+
+ Wildcard masks are the bitwise inverse of subnet masks. If you know one, you can calculate the other by + subtracting from 255.255.255.255. +
+
+
+ +
+

Conversion Examples

+ {#each wildcardMasksContent.conversionExamples as example, exIdx (`${example.subnet}-${exIdx}`)} +
+
{example.description}
+
+
Subnet Mask: {example.subnet}
+
Subnet Binary: {example.subnetBinary}
+
Wildcard Mask: {example.wildcard}
+
Wildcard Binary: {example.wildcardBinary}
+
+
+ {/each} +
+ +
+

{wildcardMasksContent.quickConversion.title}

+

Formula: {wildcardMasksContent.quickConversion.formula}

+ +

Steps:

+
    + {#each wildcardMasksContent.quickConversion.steps as step, stepIdx (`${step}-${stepIdx}`)} +
  1. {step}
  2. + {/each} +
+ +

Examples:

+ + + + + + + + + + {#each wildcardMasksContent.quickConversion.examples as example, qexIdx (`${example.subnet}-${qexIdx}`)} + + + + + + {/each} + +
Subnet MaskCalculationWildcard Mask
{example.subnet}{example.calculation}{example.wildcard}
+
+ +
+

ACL Examples by Platform

+ {#each wildcardMasksContent.aclExamples as platform, pIdx (`${platform.title}-${pIdx}`)} +

{platform.title}

+ {#each platform.entries as entry, eIdx (`${entry}-${eIdx}`)} +
+
{entry.meaning}
+
+
ACL Entry: {entry.acl}
+
Explanation: {entry.explanation}
+
+
+ {/each} + {/each} +
+ +
+

Special Cases

+
+ {#each wildcardMasksContent.specialCases as specialCase, scIdx (`${specialCase.case}-${scIdx}`)} +
+
{specialCase.case}
+
Wildcard: {specialCase.wildcard}
+
+ Meaning: + {specialCase.meaning}
+ Usage: + {specialCase.usage} +
+
+ {/each} +
+
+ +
+

Platform Differences

+ + + + + + + + + + + {#each wildcardMasksContent.platformDifferences as platform, pdIdx (`${platform.platform}-${pdIdx}`)} + + + + + + + {/each} + +
PlatformFormatExampleNotes
{platform.platform}{platform.format}{platform.example}{platform.notes}
+
+ +
+

Quick Reference Table

+ + + + + + + + + + + {#each wildcardMasksContent.quickReference as row, refIdx (`${row.subnet}-${refIdx}`)} + + + + + + + {/each} + +
CIDRSubnet MaskWildcard MaskAddresses
{row.prefix}{row.subnet}{row.wildcard}{row.use}
+
+ +
+

Common Mistakes

+ {#each wildcardMasksContent.commonMistakes as mistake, mIdx (`${mistake.mistake}-${mIdx}`)} +
+
+ + {mistake.mistake} +
+
+

Problem: {mistake.problem}

+

Solution: {mistake.solution}

+
+
+ {/each} +
+ +
+

Tips for Success

+
+
Remember These
+ {#each wildcardMasksContent.tips as tip, tipIdx (`${tip}-${tipIdx}`)} +
+
{tip}
+
+ {/each} +
+ +
+
+ + Quick Memory Aid +
+
+ Wildcard 0 = "must match exactly", Wildcard 1 = "don't care". Think of it as a mask where 0 blocks changes and + 1 allows anything. +
+
+
+
+
diff --git a/src/routes/[lang]/search/+page.svelte b/src/routes/[lang]/search/+page.svelte new file mode 100644 index 00000000..41117db0 --- /dev/null +++ b/src/routes/[lang]/search/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/settings/+page.svelte b/src/routes/[lang]/settings/+page.svelte new file mode 100644 index 00000000..188e2cc5 --- /dev/null +++ b/src/routes/[lang]/settings/+page.svelte @@ -0,0 +1,101 @@ + + +
+
+

{$t('settings.title')}

+

{$t('settings.description')}

+
+ + {#if DISABLE_SETTINGS} +
+
+ +
+

{$t('settings.disabled.title')}

+

{$t('settings.disabled.message')}

+
+ {:else} + + {/if} +
+ + diff --git a/src/routes/[lang]/subnetting/+page.svelte b/src/routes/[lang]/subnetting/+page.svelte new file mode 100644 index 00000000..4988b988 --- /dev/null +++ b/src/routes/[lang]/subnetting/+page.svelte @@ -0,0 +1,494 @@ + + +
+ + + + +
+ + +

What's Subnetting?

+
+

+ Subnetting is the process of splitting a large network into smaller, easier-to-manage pieces. Each subnet has its + own network address and range of IPs, which helps organize devices, improve security, and reduce wasted addresses. + It's core to network planning (both small home labs, or managing a large office or campus). +

+

+ These tools aim to make this easier for you, handling the math and planning for you. They calculate network and + broadcast addresses, host ranges, and help design or summarize networks so you can focus on building, not IP + crunching. +

+
+ + +
+

Essential Concepts

+
+ {#each keyConcepts as concept (concept.title)} +
+
+ +

{concept.title}

+
+

{concept.description}

+ {#if concept.example} + {concept.example} + {/if} +
+ {/each} +
+
+ + +
+

Subnetting Techniques

+
+ {#each subnettingTechniques as technique (technique.name)} +
+
+ +

{technique.name}

+
+

{technique.description}

+
+ Best for: + {technique.useCase} +
+
+ {/each} +
+
+ + +
+

Common Subnet Masks

+
+ + + + + + + + + + + {#each commonSubnetMasks as mask (mask.cidr)} + + + + + + + {/each} + +
CIDRSubnet MaskHostsSubnets
/{mask.cidr}{mask.decimal}{formatNumber(mask.hosts)}{formatNumber(mask.networks)}
+
+
+ + +
+
+
+ +

Best Practices

+
+
    + {#each practicalTips as tip (tip)} +
  • {tip}
  • + {/each} +
+
+ +
+
+ +

Common Mistakes

+
+
    + {#each commonMistakes as mistake (mistake)} +
  • {mistake}
  • + {/each} +
+
+
+
+ + diff --git a/src/routes/[lang]/subnetting/ipv4-subnet-calculator/+page.svelte b/src/routes/[lang]/subnetting/ipv4-subnet-calculator/+page.svelte new file mode 100644 index 00000000..30e398f7 --- /dev/null +++ b/src/routes/[lang]/subnetting/ipv4-subnet-calculator/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/routes/[lang]/subnetting/ipv6-subnet-calculator/+page.svelte b/src/routes/[lang]/subnetting/ipv6-subnet-calculator/+page.svelte new file mode 100644 index 00000000..a1bc3fcb --- /dev/null +++ b/src/routes/[lang]/subnetting/ipv6-subnet-calculator/+page.svelte @@ -0,0 +1,217 @@ + + +
+ + +
+

About IPv6 Subnetting

+

+ IPv6 subnetting uses 128-bit addresses and hierarchical prefix-based allocation to provide virtually + unlimited address space. Unlike IPv4, IPv6 simplifies subnet planning with: +

+ +
+
+

Massive Address Space

+

128-bit addressing provides 2^128 addresses - virtually unlimited for any network

+
+
+

Simplified Subnetting

+

Standard /64 subnets eliminate complex subnet calculations

+
+
+

Hierarchical Design

+

Provider-independent addressing with clear network hierarchy

+
+
+

No Broadcast Domain

+

Uses multicast instead of broadcast, improving network efficiency

+
+
+ +
+

IPv6 Address Structure

+
+
+
Global Unicast (2000::/3)
+

Internet-routable addresses for global connectivity

+
+
+
Link-Local (fe80::/10)
+

Local network communication, automatically configured

+
+
+
Unique Local (fc00::/7)
+

Private addresses for internal networks (like RFC 1918)

+
+
+
Multicast (ff00::/8)
+

One-to-many communication replacing broadcast

+
+
+
+ +
+

IPv6 Subnetting Best Practices

+
+
+
Standard /64 Subnets
+

Use /64 for all LAN segments to ensure SLAAC and privacy extensions work properly

+
+
+
Hierarchical Allocation
+

Plan address space hierarchically: /48 sites, /56 small sites, /64 subnets

+
+
+
Address Compression
+

Use :: notation to compress consecutive zero groups for readability

+
+
+
Documentation Prefix
+

Use 2001:db8::/32 for examples and documentation

+
+
+
+ +
+

+ + IPv6 Planning Tip +

+

+ IPv6's massive address space eliminates the need for complex subnetting. Focus on logical network hierarchy + rather than conserving addresses. A single /64 subnet provides more addresses than the entire IPv4 internet. +

+
+
+
+ + diff --git a/src/routes/[lang]/subnetting/planner/+page.svelte b/src/routes/[lang]/subnetting/planner/+page.svelte new file mode 100644 index 00000000..d2942da1 --- /dev/null +++ b/src/routes/[lang]/subnetting/planner/+page.svelte @@ -0,0 +1,5 @@ + + + diff --git a/src/routes/[lang]/subnetting/supernet-calculator/+page.svelte b/src/routes/[lang]/subnetting/supernet-calculator/+page.svelte new file mode 100644 index 00000000..a3dad746 --- /dev/null +++ b/src/routes/[lang]/subnetting/supernet-calculator/+page.svelte @@ -0,0 +1,190 @@ + + +
+ + +
+

About Supernetting

+

+ Supernetting (also called route aggregation or CIDR block aggregation) is the process of combining + multiple smaller networks into a single larger network. This technique is essential for: +

+ +
+
+

Reduced Routing Tables

+

Fewer routes mean faster lookups and reduced memory usage in routers

+
+
+

Improved Scalability

+

Internet routing scales better with aggregated routes instead of individual subnets

+
+
+

Better Performance

+

Reduced route advertisements and faster convergence in routing protocols

+
+
+

Easier Management

+

Simplified network policies and access control lists

+
+
+ +
+

When to Use Supernetting

+
+
+
ISP Route Aggregation
+

Combining customer routes for BGP advertisements

+
+
+
Enterprise Networks
+

Summarizing branch office networks at headquarters

+
+
+
Data Centers
+

Aggregating server farm subnets for external advertisement

+
+
+
Network Redesign
+

Optimizing existing IP allocations for better summarization

+
+
+
+ +
+

+ + Pro Tip +

+

+ For optimal supernetting, design your IP allocation strategy from the beginning. Contiguous, power-of-2 sized + networks aggregate much more efficiently than scattered allocations. +

+
+
+
+ + diff --git a/src/routes/[lang]/subnetting/vlsm-calculator/+page.svelte b/src/routes/[lang]/subnetting/vlsm-calculator/+page.svelte new file mode 100644 index 00000000..db83cb27 --- /dev/null +++ b/src/routes/[lang]/subnetting/vlsm-calculator/+page.svelte @@ -0,0 +1,7 @@ + + + diff --git a/src/routes/about/legal/privacy/+page.svelte b/src/routes/about/legal/privacy/+page.svelte index cc26ae27..2896a86c 100644 --- a/src/routes/about/legal/privacy/+page.svelte +++ b/src/routes/about/legal/privacy/+page.svelte @@ -1,142 +1,136 @@ - Privacy Policy | Networking Toolbox - - - + {$t('pages.legal.privacy.title')}{$t('common.meta.titleSeparator')}{$t('common.meta.titleSuffix')} + + +
-

Privacy Policy

+

{$t('pages.legal.privacy.hero.title')}

- Your privacy matters. Actually, that's one of the reasons that I built this (and my other apps). I believe you - should have total transparency of how an app works (e.g. access to it's code), and full control over your data. So, - weather you're self-hosting Networking Toolbox, or using the public instance, you can be sure that your data is - handled with care. + {$t('pages.legal.privacy.hero.lead')}

-

Principles

+

{$t('pages.legal.privacy.principles.title')}

    -
  • We only collect the minimum data needed for things to work
  • -
  • We never store anything which isn't 100% necessary
  • -
  • Any stored data stays local in your browser or is encrypted with your own key (we can't access it)
  • -
  • We don't use cookies or tracking of any kind
  • -
  • We never share your data with anyone else
  • -
  • We're transparent about what's collected, stored, used and why
  • -
  • Our code is fully open source, so you can verify our claims for yourself
  • + {#each $t('pages.legal.privacy.principles.items') as principle, index (index)} +
  • {principle}
  • + {/each}
-

Overview

+

{$t('pages.legal.privacy.overview.title')}

- Networking Toolbox is designed with privacy in mind. All network calculations and most diagnostic tools run entirely - in your browser, meaning your data never leaves your device. + {$t('pages.legal.privacy.overview.description')}

-

Data Processing

+

{$t('pages.legal.privacy.dataProcessing.title')}

-

Client-Side Processing

+

{$t('pages.legal.privacy.dataProcessing.clientSide.title')}

- The majority of tools (CIDR calculators, IP converters, subnet planners, etc.) perform all calculations locally in - your browser. No data from these tools is transmitted to any server. + {$t('pages.legal.privacy.dataProcessing.clientSide.description')}

-

Server-Side Diagnostic Tools

+

{$t('pages.legal.privacy.dataProcessing.serverSide.title')}

- Some diagnostic tools (DNS lookups, DNSBL checks, TLS tests, etc.) require server-side processing to query external - services. For these tools: + {$t('pages.legal.privacy.dataProcessing.serverSide.description')}

    -
  • Only the specific domain or IP address you enter is sent to our server
  • -
  • Data is processed in real-time and not stored permanently
  • -
  • Requests may be temporarily logged for debugging purposes (logs are automatically purged)
  • -
  • No personally identifiable information is collected
  • + {#each $t('pages.legal.privacy.dataProcessing.serverSide.items') as item, index (index)} +
  • {item}
  • + {/each}
-

Local Storage

+

{$t('pages.legal.privacy.localStorage.title')}

- We use browser localStorage (not cookies) to save your preferences locally. This data never leaves your device and - can be cleared at any time through your browser settings. + {$t('pages.legal.privacy.localStorage.description')}

    -
  • Theme settings: Your chosen color theme and accessibility preferences
  • -
  • Layout preferences: Homepage and navigation layout choices
  • -
  • Bookmarked tools: Your saved favorite tools for quick access
  • + {#each $t('pages.legal.privacy.localStorage.items') as unknown as { title: string; description: string }[] as item, index (index)} +
  • {item.title} {item.description}
  • + {/each}
-

All localStorage usage is optional and strictly for your convenience. The application works without it.

+

{$t('pages.legal.privacy.localStorage.note')}

-

Analytics & Tracking

+

{$t('pages.legal.privacy.analytics.title')}

- Unless disabled, we log basic anonymous usage statistics using a self-hosted Plausible Analytics instance. This - helps us understand which features are most useful and improve the application. + {$t('pages.legal.privacy.analytics.description')}

    -
  • Privacy-focused: No cookies, no tracking across sites, no personal data collection
  • -
  • - Anonymous only: We only collect page views and referrer information - no IP addresses, user agents, - or any personally identifiable information -
  • -
  • - Self-hosted: Analytics data stays on our infrastructure and is never shared with third parties -
  • -
  • Opt-out: You can disable analytics by enabling "Do Not Track" in your browser settings
  • + {#each $t('pages.legal.privacy.analytics.features') as unknown as { title: string; description: string }[] as feature, index (index)} +
  • {feature.title} {feature.description}
  • + {/each}
-

We do not use any third-party advertising services or tracking scripts that follow you across the web.

+

{$t('pages.legal.privacy.analytics.note')}

-

Third-Party Services

-

When using diagnostic tools, your queries may be sent to third-party services:

+

{$t('pages.legal.privacy.thirdParty.title')}

+

{$t('pages.legal.privacy.thirdParty.description')}

    -
  • DNS queries: Public DNS resolvers (Google, Cloudflare, Quad9, etc.)
  • -
  • DNSBL checks: Spam blacklist providers (Spamhaus, SORBS, etc.)
  • -
  • WHOIS/RDAP: Regional Internet registries
  • -
  • Certificate checks: Certificate Transparency logs
  • + {#each $t('pages.legal.privacy.thirdParty.services') as unknown as { title: string; description: string }[] as service, index (index)} +
  • {service.title} {service.description}
  • + {/each}
-

These services have their own privacy policies and are outside our control.

+

{$t('pages.legal.privacy.thirdParty.note')}

-

Self-Hosting

+

{$t('pages.legal.privacy.selfHosting.title')}

- For maximum privacy, you can self-host Networking Toolbox. When self-hosted, you have complete control over all data - processing and server logs. See the Self-Hosting section - for instructions. + {$t('pages.legal.privacy.selfHosting.description', { + deployingLink: `${$t('pages.legal.privacy.selfHosting.deployingLinkText')}`, + })}

-

Open Source

-

Networking Toolbox is fully open source. You can review the code to verify our privacy practices:

+

{$t('pages.legal.privacy.openSource.title')}

+

{$t('pages.legal.privacy.openSource.description')}

-

Changes to This Policy

+

{$t('pages.legal.privacy.changes.title')}

- This privacy policy may be updated occasionally. Significant changes will be noted in the - changelog. + {$t('pages.legal.privacy.changes.description', { + changelogLink: `${$t('pages.legal.privacy.changes.changelogLinkText')}`, + })}

-

Last updated: January 2025

+

{$t('pages.legal.privacy.changes.lastUpdated')}

-

Questions?

+

{$t('pages.legal.privacy.contact.title')}

- If you have questions about this privacy policy, please open an issue on - GitHub. + {$t('pages.legal.privacy.contact.description', { + githubLink: `${$t('pages.legal.privacy.contact.githubLinkText')}`, + })}

diff --git a/src/routes/api/internal/diagnostics/http/+server.ts b/src/routes/api/internal/diagnostics/http/+server.ts index 62b83b16..ae29c2f1 100644 --- a/src/routes/api/internal/diagnostics/http/+server.ts +++ b/src/routes/api/internal/diagnostics/http/+server.ts @@ -547,12 +547,17 @@ export const POST: RequestHandler = async ({ request }) => { }; try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const preflightResponse = await fetch(url, { method: 'OPTIONS', headers: preflightHeaders, - signal: AbortSignal.timeout(timeout), + signal: controller.signal, }); + clearTimeout(timeoutId); + const corsHeaders: Record = {}; preflightResponse.headers.forEach((value, key) => { if (key.toLowerCase().startsWith('access-control-')) { diff --git a/src/routes/api/internal/diagnostics/network/+server.ts b/src/routes/api/internal/diagnostics/network/+server.ts index 78a1f2a5..bbafed9a 100644 --- a/src/routes/api/internal/diagnostics/network/+server.ts +++ b/src/routes/api/internal/diagnostics/network/+server.ts @@ -139,13 +139,17 @@ async function httpPing( try { const startTime = Date.now(); + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeout); + const response = await fetch(url, { method: method.toUpperCase(), - signal: AbortSignal.timeout(timeout), + signal: controller.signal, // Don't follow redirects for more consistent timing redirect: 'manual', }); + clearTimeout(timeoutId); const latency = Date.now() - startTime; latencies.push(latency); diff --git a/src/routes/cidr/summarize/+page.svelte b/src/routes/cidr/summarize/+page.svelte index d3217d27..42ed834c 100644 --- a/src/routes/cidr/summarize/+page.svelte +++ b/src/routes/cidr/summarize/+page.svelte @@ -1,58 +1,63 @@
-

About CIDR Summarization

+

{$t('pages/cidr-summarize.cidrSummarize.about.title')}

- CIDR Summarization optimizes network routing by combining multiple IP addresses, ranges, and CIDR - blocks into the minimal set of CIDR prefixes that covers the same address space. + {$t('pages/cidr-summarize.cidrSummarize.about.description')}

-

Route Table Optimization

-

Reduce routing table size by aggregating multiple routes into fewer, larger prefixes

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.routeTable.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.routeTable.description')}

-

Network Efficiency

-

Minimize routing protocol overhead and improve convergence times

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.networkEfficiency.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.networkEfficiency.description')}

-

Dual Protocol Support

-

Handle mixed IPv4 and IPv6 inputs with separate optimized outputs

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.dualProtocol.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.dualProtocol.description')}

-

Flexible Input Formats

-

Process single IPs, CIDR blocks, and explicit ranges in any combination

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.flexibleInput.title')}

+

{$t('pages/cidr-summarize.cidrSummarize.benefits.flexibleInput.description')}

-

Summarization Modes

+

{$t('pages/cidr-summarize.cidrSummarize.modes.title')}

-
Exact Merge
+
{$t('pages/cidr-summarize.cidrSummarize.modes.exactMerge.title')}

- Conservative approach: Merges overlapping ranges exactly without additional aggregation + {$t('pages/cidr-summarize.cidrSummarize.modes.exactMerge.description')}

- Example: + {$t('pages/cidr-summarize.cidrSummarize.modes.example.label')} 192.168.1.0/24 + 192.168.2.0/24 β†’ 192.168.1.0/24, 192.168.2.0/24
-
Minimal Cover
-

Aggressive optimization: Finds the smallest set of CIDR blocks that covers all inputs

+
{$t('pages/cidr-summarize.cidrSummarize.modes.minimalCover.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.modes.minimalCover.description')}

- Example: + {$t('pages/cidr-summarize.cidrSummarize.modes.example.label')} 192.168.1.0/24 + 192.168.2.0/24 β†’ 192.168.0.0/23 @@ -62,56 +67,56 @@
-

Common Use Cases

+

{$t('pages/cidr-summarize.cidrSummarize.useCases.title')}

-
BGP Route Aggregation
-

Optimize BGP advertisements by summarizing customer routes into provider prefixes

+
{$t('pages/cidr-summarize.cidrSummarize.useCases.bgp.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.bgp.description')}

-
Firewall Rule Optimization
-

Reduce ACL complexity by consolidating IP ranges into fewer CIDR rules

+
{$t('pages/cidr-summarize.cidrSummarize.useCases.firewall.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.firewall.description')}

-
Network Planning
-

Analyze address space utilization and optimize subnet allocations

+
{$t('pages/cidr-summarize.cidrSummarize.useCases.planning.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.planning.description')}

-
Migration Planning
-

Consolidate legacy network ranges during infrastructure modernization

+
{$t('pages/cidr-summarize.cidrSummarize.useCases.migration.title')}
+

{$t('pages/cidr-summarize.cidrSummarize.useCases.migration.description')}

-

Supported Input Formats

+

{$t('pages/cidr-summarize.cidrSummarize.inputFormats.title')}

-
Single IP Addresses
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.singleIP.title')}
192.168.1.100 2001:db8::1
-
CIDR Blocks
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.cidrBlocks.title')}
10.0.0.0/8 2001:db8::/32
-
IP Ranges
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.ipRanges.title')}
172.16.1.1-172.16.1.100 2001:db8::1-2001:db8::ffff
-
Mixed Lists
+
{$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.title')}
- One item per line - IPv4 and IPv6 together + {$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.example1')} + {$t('pages/cidr-summarize.cidrSummarize.inputFormats.mixedLists.example2')}
@@ -120,12 +125,10 @@

- Optimization Tips + {$t('pages/cidr-summarize.cidrSummarize.optimizationTips.title')}

- For maximum efficiency, align your network allocations to power-of-2 boundaries. Contiguous address blocks - summarize much more effectively than scattered allocations. Use the exact merge mode for conservative - summarization or minimal cover for aggressive optimization. + {$t('pages/cidr-summarize.cidrSummarize.optimizationTips.description')}

diff --git a/src/routes/diagnostics/dns/caa-effective/+page.svelte b/src/routes/diagnostics/dns/caa-effective/+page.svelte index 3e6b1a23..39a38d3b 100644 --- a/src/routes/diagnostics/dns/caa-effective/+page.svelte +++ b/src/routes/diagnostics/dns/caa-effective/+page.svelte @@ -1,6 +1,7 @@
@@ -13,37 +20,48 @@

- Understanding IPv4 and IPv6 + {$t('pages.ipConverter.families.understanding.title')}

-

IPv4 (Internet Protocol version 4)

-

Address Length: 32 bits (4 bytes)

-

Format: Dotted decimal notation (e.g., 192.168.1.1)

-

Total Addresses: ~4.3 billion addresses

-

Example: 203.0.113.45

-

Status: Widely deployed but address space exhausted

+

{$t('pages.ipConverter.families.understanding.ipv4.title')}

+

{$t('pages.ipConverter.families.understanding.ipv4.addressLength')}

+

{$t('pages.ipConverter.families.understanding.ipv4.format')}

+

{$t('pages.ipConverter.families.understanding.ipv4.totalAddresses')}

+

+ {$t('pages.ipConverter.families.understanding.ipv4.example')} 203.0.113.45 +

+

{$t('pages.ipConverter.families.understanding.ipv4.status')}

-

IPv6 (Internet Protocol version 6)

-

Address Length: 128 bits (16 bytes)

-

Format: Hexadecimal with colons (e.g., 2001:db8::1)

-

Total Addresses: ~340 undecillion addresses

-

Example: 2001:0db8:85a3:0000:0000:8a2e:0370:7334

-

Status: Modern standard with virtually unlimited address space

+

{$t('pages.ipConverter.families.understanding.ipv6.title')}

+

{$t('pages.ipConverter.families.understanding.ipv6.addressLength')}

+

{$t('pages.ipConverter.families.understanding.ipv6.format')}

+

{$t('pages.ipConverter.families.understanding.ipv6.totalAddresses')}

+

+ {$t('pages.ipConverter.families.understanding.ipv6.example')} + 2001:0db8:85a3:0000:0000:8a2e:0370:7334 +

+

{$t('pages.ipConverter.families.understanding.ipv6.status')}

-

IPv4-mapped IPv6

-

Purpose: Represent IPv4 addresses within IPv6 format

-

Format: ::ffff:192.0.2.1 or ::ffff:c000:0201

-

Usage: Transition mechanism and dual-stack implementations

-

Structure: 80 zero bits + 16 one bits (ffff) + 32-bit IPv4 address

+

+ {$t('pages.ipConverter.families.understanding.ipv4Mapped.title')} +

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.purpose')}

+

+ {$t('pages.ipConverter.families.understanding.ipv4Mapped.format')} + ::ffff:192.0.2.1 + or ::ffff:c000:0201 +

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.usage')}

+

{$t('pages.ipConverter.families.understanding.ipv4Mapped.structure')}

@@ -52,37 +70,41 @@

- Conversion Methods & Use Cases + {$t('pages.ipConverter.families.conversionMethods.title')}

-

IPv4 to IPv6 Conversion

+

{$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.title')}

    -
  • IPv4-mapped: Embed IPv4 addresses in IPv6 format
  • -
  • Dual-stack: Run both protocols simultaneously
  • -
  • Tunneling: Encapsulate IPv4 traffic in IPv6 packets
  • -
  • Migration: Gradual transition from IPv4 to IPv6
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.mapped')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.dualStack')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.tunneling')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv4ToIpv6.migration')}
-

IPv6 to IPv4 Extraction

+

{$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.title')}

    -
  • Legacy Support: Extract IPv4 from mapped addresses
  • -
  • Compatibility: Interface with IPv4-only systems
  • -
  • Debugging: Identify original IPv4 addresses
  • -
  • Analysis: Traffic analysis and monitoring
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.legacySupport')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.compatibility')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.debugging')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.ipv6ToIpv4.analysis')}
-

Real-world Applications

+

{$t('pages.ipConverter.families.conversionMethods.realWorldApps.title')}

    -
  • Web Servers: Handle both IPv4 and IPv6 clients
  • -
  • Load Balancers: Route traffic between IP versions
  • -
  • Network Monitoring: Unified logging and analysis
  • -
  • API Integration: Service compatibility layers
  • +
  • {$t('pages.ipConverter.families.conversionMethods.realWorldApps.webServers')}
  • +
  • {$t('pages.ipConverter.families.conversionMethods.realWorldApps.loadBalancers')}
  • +
  • + {$t('pages.ipConverter.families.conversionMethods.realWorldApps.networkMonitoring')} +
  • +
  • + {$t('pages.ipConverter.families.conversionMethods.realWorldApps.apiIntegration')} +
@@ -92,20 +114,20 @@

- Important Considerations + {$t('pages.ipConverter.families.considerations.title')}

-

Limitations & Best Practices

+

{$t('pages.ipConverter.families.considerations.limitations.title')}

  • - IPv4-mapped IPv6: Only works for representing IPv4 addresses, not true IPv6 migration + {$t('pages.ipConverter.families.considerations.limitations.ipv4MappedLimit')}
  • -
  • Security: IPv4-mapped addresses may bypass IPv6-specific security rules
  • -
  • Performance: Native IPv6 is preferred over IPv4-mapped when possible
  • -
  • Compatibility: Not all applications handle IPv4-mapped IPv6 correctly
  • -
  • Best Practice: Use dual-stack configuration rather than relying solely on mapping
  • -
  • Future-proofing: Plan for IPv6-native implementations
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.security')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.performance')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.compatibility')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.bestPractice')}
  • +
  • {$t('pages.ipConverter.families.considerations.limitations.futureProofing')}
diff --git a/src/routes/ip-address-convertor/mac-address/+page.svelte b/src/routes/ip-address-convertor/mac-address/+page.svelte index 7987199d..a53cb9cc 100644 --- a/src/routes/ip-address-convertor/mac-address/+page.svelte +++ b/src/routes/ip-address-convertor/mac-address/+page.svelte @@ -3,6 +3,13 @@ import { macAddressContent } from '$lib/content/mac-address.js'; import { tooltip } from '$lib/actions/tooltip.js'; import Icon from '$lib/components/global/Icon.svelte'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'tools/mac-address'); + }); interface OUIField { key: string; @@ -41,51 +48,60 @@ { mac: '00:1A:79:00:00:01', vendor: 'Telecomunication Technologies', - description: 'Ukrainian telecom equipment (Odessa)', + description: $t('examples.vendors.telecomunicationTech'), }, - { mac: '3C:22:FB:A1:B2:C3', vendor: 'Apple', description: 'Apple device (Cupertino, CA)' }, - { mac: 'DC:A6:32:A1:B2:C3', vendor: 'Raspberry Pi Trading', description: 'Raspberry Pi (Cambridge, UK)' }, - { mac: '00:16:3E:1F:4A:B1', vendor: 'Xensource', description: 'Xen virtual machine (Palo Alto, CA)' }, - { mac: '00:00:5E:00:01:01', vendor: 'ICANN IANA', description: 'IANA reserved addresses (special use)' }, - { mac: '00:0D:B9:A1:B2:C3', vendor: 'PC Engines', description: 'PC Engines embedded systems (Switzerland)' }, - { mac: 'FF:FF:FF:FF:FF:FF', vendor: '', description: 'Multicast/Broadcast' }, + { mac: '3C:22:FB:A1:B2:C3', vendor: 'Apple', description: $t('examples.vendors.apple') }, + { mac: 'DC:A6:32:A1:B2:C3', vendor: 'Raspberry Pi Trading', description: $t('examples.vendors.raspberryPi') }, + { mac: '00:16:3E:1F:4A:B1', vendor: 'Xensource', description: $t('examples.vendors.xensource') }, + { mac: '00:00:5E:00:01:01', vendor: 'ICANN IANA', description: $t('examples.vendors.icannIana') }, + { mac: '00:0D:B9:A1:B2:C3', vendor: 'PC Engines', description: $t('examples.vendors.pcEngines') }, + { mac: 'FF:FF:FF:FF:FF:FF', vendor: '', description: $t('examples.vendors.multicastBroadcast') }, ]; const ouiFields: OUIField[] = [ - { key: 'oui', label: 'OUI', icon: 'hash', render: (c: MACConversionResult) => c.oui.oui, code: true }, + { + key: 'oui', + label: $t('results.oui.fields.oui'), + icon: 'hash', + render: (c: MACConversionResult) => c.oui.oui, + code: true, + }, { key: 'manufacturer', - label: 'Manufacturer', + label: $t('results.oui.fields.manufacturer'), icon: (c: MACConversionResult) => (c.oui.found ? 'building' : 'help-circle'), - render: (c: MACConversionResult) => (c.oui.found ? c.oui.manufacturer : 'Unknown'), + render: (c: MACConversionResult) => (c.oui.found ? c.oui.manufacturer : $t('results.oui.values.unknown')), class: 'manufacturer-item', valueClass: (c: MACConversionResult) => (!c.oui.found ? 'unknown' : ''), }, { key: 'country', - label: 'Country', + label: $t('results.oui.fields.country'), icon: 'globe', - render: (c: MACConversionResult) => c.oui.country || 'N/A', + render: (c: MACConversionResult) => c.oui.country || $t('results.oui.values.na'), condition: (c: MACConversionResult) => !!c.oui.country, }, { key: 'blockType', - label: 'Block Type', + label: $t('results.oui.fields.blockType'), icon: 'layers', - render: (c: MACConversionResult) => c.oui.blockType || 'N/A', + render: (c: MACConversionResult) => c.oui.blockType || $t('results.oui.values.na'), tooltip: (c: MACConversionResult) => (c.oui.blockType ? getBlockTypeTooltip(c.oui.blockType) : ''), condition: (c: MACConversionResult) => !!c.oui.blockType, }, { key: 'blockSize', - label: 'Block Size', + label: $t('results.oui.fields.blockSize'), icon: 'database', - render: (c: MACConversionResult) => (c.oui.blockSize ? `${c.oui.blockSize.toLocaleString()} addresses` : 'N/A'), + render: (c: MACConversionResult) => + c.oui.blockSize + ? $t('results.oui.values.addresses', { count: c.oui.blockSize.toLocaleString() }) + : $t('results.oui.values.na'), condition: (c: MACConversionResult) => c.oui.blockSize != null, }, { key: 'blockRange', - label: 'Address Range', + label: $t('results.oui.fields.addressRange'), icon: 'server', render: (c: MACConversionResult) => `${c.oui.blockStart}-${c.oui.blockEnd}`, valueClass: () => 'range', @@ -93,83 +109,117 @@ }, { key: 'isPrivate', - label: 'Registry Status', + label: $t('results.oui.fields.registryStatus'), icon: 'shield', - render: (c: MACConversionResult) => (c.oui.isPrivate ? 'Private' : 'Public'), + render: (c: MACConversionResult) => + c.oui.isPrivate ? $t('results.oui.values.private') : $t('results.oui.values.public'), condition: (c: MACConversionResult) => c.oui.isPrivate != null, }, { key: 'updated', - label: 'Last Updated', + label: $t('results.oui.fields.lastUpdated'), icon: 'clock', - render: (c: MACConversionResult) => (c.oui.updated ? new Date(c.oui.updated).toLocaleDateString() : 'N/A'), + render: (c: MACConversionResult) => + c.oui.updated ? new Date(c.oui.updated).toLocaleDateString() : $t('results.oui.values.na'), condition: (c: MACConversionResult) => !!c.oui.updated, }, { key: 'address', - label: 'Address', + label: $t('results.oui.fields.address'), icon: 'map-pin', - render: (c: MACConversionResult) => c.oui.address || 'N/A', + render: (c: MACConversionResult) => c.oui.address || $t('results.oui.values.na'), class: 'address-item', condition: (c: MACConversionResult) => !!c.oui.address, }, ]; const detailFields: DetailField[] = [ - { label: 'Universal Address', key: 'isUniversal' }, - { label: 'Locally Administered', key: 'isUniversal', invert: true }, - { label: 'Unicast', key: 'isUnicast' }, - { label: 'Multicast/Broadcast', key: 'isUnicast', invert: true }, + { label: $t('results.details.fields.universalAddress'), key: 'isUniversal' }, + { label: $t('results.details.fields.locallyAdministered'), key: 'isUniversal', invert: true }, + { label: $t('results.details.fields.unicast'), key: 'isUnicast' }, + { label: $t('results.details.fields.multicastBroadcast'), key: 'isUnicast', invert: true }, ]; const formatFields: FormatField[] = [ - { key: 'colon', label: 'Colon Notation', tooltip: 'Standard IEEE notation; most Linux, BSD, macOS use this' }, - { key: 'hyphen', label: 'Hyphen Notation', tooltip: 'Common on Windows systems' }, - { key: 'cisco', label: 'Cisco (Dot) Notation', tooltip: 'Cisco IOS / NX-OS style' }, - { key: 'bareUppercase', label: 'Bare (Uppercase)', tooltip: 'Common in databases, APIs' }, - { key: 'bareLowercase', label: 'Bare (Lowercase)', tooltip: 'Common in scripts, JSON, etc.' }, + { + key: 'colon', + label: $t('results.formats.types.colonNotation.label'), + tooltip: $t('results.formats.types.colonNotation.tooltip'), + }, + { + key: 'hyphen', + label: $t('results.formats.types.hyphenNotation.label'), + tooltip: $t('results.formats.types.hyphenNotation.tooltip'), + }, + { + key: 'cisco', + label: $t('results.formats.types.ciscoNotation.label'), + tooltip: $t('results.formats.types.ciscoNotation.tooltip'), + }, + { + key: 'bareUppercase', + label: $t('results.formats.types.bareUppercase.label'), + tooltip: $t('results.formats.types.bareUppercase.tooltip'), + }, + { + key: 'bareLowercase', + label: $t('results.formats.types.bareLowercase.label'), + tooltip: $t('results.formats.types.bareLowercase.tooltip'), + }, { key: 'eui64', - label: 'EUI-64 (expanded form)', - tooltip: 'Used when converting MAC β†’ IPv6 Interface ID (adds FFFE in the middle, flips the U/L bit)', + label: $t('results.formats.types.eui64.label'), + tooltip: $t('results.formats.types.eui64.tooltip'), }, { key: 'ipv6Style', - label: 'Dot-separated 2-byte groups', - tooltip: 'Occasionally seen in debugging or tools that mimic IPv6 notation', + label: $t('results.formats.types.ipv6Style.label'), + tooltip: $t('results.formats.types.ipv6Style.tooltip'), + }, + { + key: 'spaceSeparated', + label: $t('results.formats.types.spaceSeparated.label'), + tooltip: $t('results.formats.types.spaceSeparated.tooltip'), }, - { key: 'spaceSeparated', label: 'Space-separated pairs', tooltip: 'Sometimes seen in hex dumps or firmware logs' }, { key: 'decimalOctets', - label: 'Decimal octets', - tooltip: 'Rare, but some diagnostic tools display MACs in decimal', + label: $t('results.formats.types.decimalOctets.label'), + tooltip: $t('results.formats.types.decimalOctets.tooltip'), + }, + { + key: 'prefixedMac', + label: $t('results.formats.types.prefixedMac.label'), + tooltip: $t('results.formats.types.prefixedMac.tooltip'), + }, + { + key: 'slashSeparated', + label: $t('results.formats.types.slashSeparated.label'), + tooltip: $t('results.formats.types.slashSeparated.tooltip'), }, - { key: 'prefixedMac', label: 'Prefixed (MAC=)', tooltip: 'Seen in configuration files or CLI outputs' }, - { key: 'slashSeparated', label: 'Slash-separated', tooltip: 'Seen in some telecom equipment or SNMP exports' }, { key: 'prefixedBare', - label: 'Prefixed bare (MAC)', - tooltip: 'Appears in certain JSON/CSV exports or proprietary APIs', + label: $t('results.formats.types.prefixedBare.label'), + tooltip: $t('results.formats.types.prefixedBare.tooltip'), }, { key: 'prefixedAddr', - label: 'Prefixed bare (addr)', - tooltip: 'Appears in certain JSON/CSV exports or proprietary APIs', + label: $t('results.formats.types.prefixedAddr.label'), + tooltip: $t('results.formats.types.prefixedAddr.tooltip'), }, { key: 'binary', - label: 'Binary (8-bit groups)', - tooltip: 'Rare, but useful for bit-level inspection', + label: $t('results.formats.types.binary.label'), + tooltip: $t('results.formats.types.binary.tooltip'), binary: true, class: 'binary-item', }, ]; const blockTypeTooltips = { - 'MA-L': 'Large block: 16.7 million addresses (24-bit prefix)', - 'MA-M': 'Medium block: 1 million addresses (28-bit prefix)', - 'MA-S': 'Small block: 4,096 addresses (36-bit prefix)', - CID: 'Company ID', + 'MA-L': $t('results.oui.blockTypes.maL'), + 'MA-M': $t('results.oui.blockTypes.maM'), + 'MA-S': $t('results.oui.blockTypes.maS'), + CID: $t('results.oui.blockTypes.cid'), } as const; async function convertAddresses() { @@ -238,20 +288,19 @@
-

MAC Address Converter & OUI Lookup

+

{$t('title')}

- Convert MAC addresses between different formats and identify the manufacturer using the Organizationally Unique - Identifier (OUI) + {$t('description')}

-

MAC Address{isBulkMode ? 'es' : ''}

+

{isBulkMode ? $t('form.bulkMode.title') : $t('form.singleMode.title')}

@@ -260,16 +309,16 @@
- Enter MAC addresses one per line. Supported formats: 00:1A:2B:3C:4D:5E, - 00-1A-2B-3C-4D-5E, 001A.2B3C.4D5E (Cisco), 001A2B3C4D5E + {$t('form.helpText.bulk', { + formats: $t('form.helpText.formats', { + colon: '00:1A:2B:3C:4D:5E', + hyphen: '00-1A-2B-3C-4D-5E', + cisco: '001A.2B3C.4D5E', + bare: '001A2B3C4D5E', + }), + })}
{:else}
{ @@ -311,13 +366,18 @@ />
- Supported formats: 00:1A:2B:3C:4D:5E, 00-1A-2B-3C-4D-5E, - 001A.2B3C.4D5E - (Cisco), 001A2B3C4D5E + {$t('form.helpText.single', { + formats: $t('form.helpText.formats', { + colon: '00:1A:2B:3C:4D:5E', + hyphen: '00-1A-2B-3C-4D-5E', + cisco: '001A.2B3C.4D5E', + bare: '001A2B3C4D5E', + }), + })}
{/if}
@@ -329,7 +389,7 @@
-

Quick Examples

+

{$t('examples.title')}

{#each examples as example, i (i)} @@ -352,16 +412,16 @@ {#if result.conversions.length > 0}
-

{result.conversions.length === 1 ? 'Address Conversion' : 'Address Conversions'}

+

{result.conversions.length === 1 ? $t('results.conversion') : $t('results.conversions')}

{#if result.conversions.length > 1}
{/if} @@ -383,7 +443,7 @@
- Invalid MAC Address + {$t('results.invalidAddress')}
{#if conversion.error}
{conversion.error}
@@ -394,7 +454,7 @@ {#if conversion.isValid}
-

OUI Information

+

{$t('results.oui.title')}

{#each ouiFields as field (field.key)} {@const value = field.render(conversion)} @@ -442,7 +502,7 @@
-

Address Details

+

{$t('results.details.title')}

{#each detailFields as field (field.label)} {@const active = field.invert ? !conversion.details[field.key] : conversion.details[field.key]} @@ -456,7 +516,7 @@
-

Formats

+

{$t('results.formats.title')}

{#each formatFields as field (field.key)} {@const value = field.binary @@ -474,7 +534,7 @@ @@ -490,23 +550,23 @@ {#if result.conversions.length > 1}
-

Conversion Summary

+

{$t('results.summary.title')}

{result.summary.total} - Total + {$t('results.summary.stats.total')}
{result.summary.valid} - Valid + {$t('results.summary.stats.valid')}
{result.summary.invalid} - Invalid + {$t('results.summary.stats.invalid')}
{result.summary.withOUI} - With OUI + {$t('results.summary.stats.withOui')}
@@ -519,7 +579,7 @@
-

Understanding MAC Addresses

+

{$t('education.title')}

@@ -629,7 +689,7 @@
-

Quick Tips

+

{$t('education.quickTips')}

    diff --git a/src/routes/ip-address-convertor/notation/+layout.svelte b/src/routes/ip-address-convertor/notation/+layout.svelte index 43a16a72..27ce4faf 100644 --- a/src/routes/ip-address-convertor/notation/+layout.svelte +++ b/src/routes/ip-address-convertor/notation/+layout.svelte @@ -3,6 +3,11 @@ import '../../../styles/converters.scss'; import '../../../styles/components.scss'; import Icon from '$lib/components/global/Icon.svelte'; + import { t } from '$lib/i18n'; + import { ensurePageTranslations } from '$lib/i18n/page-translations'; + + // Load translations immediately for SSR + ensurePageTranslations('en');
    @@ -13,35 +18,46 @@

    - Understanding IPv6 Address Notation + {t('ipv6Notation.title')}

    -

    Expanded (Full) Format

    -

    Structure: All 32 hexadecimal characters with colons every 4 digits

    -

    Example: 2001:0db8:85a3:0000:0000:8a2e:0370:7334

    -

    Usage: Debugging, detailed analysis, and when precision is required

    -

    Benefits: Shows complete address structure, easier to parse programmatically

    +

    {t('ipv6Notation.formats.expanded.title')}

    +

    {t('ipv6Notation.terms.structure')} {t('ipv6Notation.formats.expanded.structure')}

    +

    + {t('ipv6Notation.terms.example')} 2001:0db8:85a3:0000:0000:8a2e:0370:7334 +

    +

    {t('ipv6Notation.terms.usage')} {t('ipv6Notation.formats.expanded.usage')}

    +

    {t('ipv6Notation.terms.benefits')} {t('ipv6Notation.formats.expanded.benefits')}

    -

    Compressed (Shortened) Format

    -

    Structure: Uses :: to represent consecutive zero groups, removes leading zeros

    -

    Example: 2001:db8:85a3::8a2e:370:7334

    -

    Usage: Configuration files, user interfaces, documentation

    -

    Benefits: Shorter, more readable, standard representation

    +

    {t('ipv6Notation.formats.compressed.title')}

    +

    {t('ipv6Notation.terms.structure')} {t('ipv6Notation.formats.compressed.structure')}

    +

    {t('ipv6Notation.terms.example')} 2001:db8:85a3::8a2e:370:7334

    +

    {t('ipv6Notation.terms.usage')} {t('ipv6Notation.formats.compressed.usage')}

    +

    {t('ipv6Notation.terms.benefits')} {t('ipv6Notation.formats.compressed.benefits')}

    -

    Compression Rules

    -

    Double Colon (::): Represents one or more consecutive zero groups

    -

    Single Use: Only one :: allowed per address to avoid ambiguity

    -

    Leading Zeros: Remove leading zeros from each group (0001 β†’ 1)

    -

    Preference: Compress the longest sequence of consecutive zeros

    +

    {t('ipv6Notation.formats.rules.title')}

    +

    + {t('ipv6Notation.terms.doubleColonLabel')} + {t('ipv6Notation.formats.rules.doubleColon')} +

    +

    {t('ipv6Notation.terms.singleUseLabel')} {t('ipv6Notation.formats.rules.singleUse')}

    +

    + {t('ipv6Notation.terms.leadingZerosLabel')} + {t('ipv6Notation.formats.rules.leadingZeros')} +

    +

    + {t('ipv6Notation.terms.preferenceLabel')} + {t('ipv6Notation.formats.rules.preference')} +

    @@ -50,40 +66,82 @@

    - Conversion Use Cases & Applications + {t('ipv6Notation.conversions.title')}

    -

    Expand IPv6 Addresses

    +

    {t('ipv6Notation.useCases.expand.title')}

      -
    • Network Analysis: Compare addresses byte-by-byte
    • -
    • Database Storage: Consistent format for indexing
    • -
    • Debugging: See complete address structure
    • -
    • Programming: Easier parsing and manipulation
    • -
    • Security: Avoid address obfuscation issues
    • +
    • + {t('ipv6Notation.terms.networkAnalysisLabel')} + {t('ipv6Notation.useCases.expand.networkAnalysis')} +
    • +
    • + {t('ipv6Notation.terms.databaseStorageLabel')} + {t('ipv6Notation.useCases.expand.databaseStorage')} +
    • +
    • + {t('ipv6Notation.terms.debuggingLabel')} + {t('ipv6Notation.useCases.expand.debugging')} +
    • +
    • + {t('ipv6Notation.terms.programmingLabel')} + {t('ipv6Notation.useCases.expand.programming')} +
    • +
    • + {t('ipv6Notation.terms.securityLabel')} + {t('ipv6Notation.useCases.expand.security')} +
    -

    Compress IPv6 Addresses

    +

    {t('ipv6Notation.useCases.compress.title')}

      -
    • User Interface: Shorter, more readable addresses
    • -
    • Configuration: Cleaner config files and logs
    • -
    • Documentation: Standard format for examples
    • -
    • URLs: Shorter addresses in IPv6 URLs
    • -
    • Network Equipment: Standard display format
    • +
    • + {t('ipv6Notation.terms.userInterfaceLabel')} + {t('ipv6Notation.useCases.compress.userInterface')} +
    • +
    • + {t('ipv6Notation.terms.configurationLabel')} + {t('ipv6Notation.useCases.compress.configuration')} +
    • +
    • + {t('ipv6Notation.terms.documentationLabel')} + {t('ipv6Notation.useCases.compress.documentation')} +
    • +
    • {t('ipv6Notation.terms.urlsLabel')} {t('ipv6Notation.useCases.compress.urls')}
    • +
    • + {t('ipv6Notation.terms.networkEquipmentLabel')} + {t('ipv6Notation.useCases.compress.networkEquipment')} +
    -

    Real-world Scenarios

    +

    {t('ipv6Notation.useCases.scenarios.title')}

      -
    • Network Monitoring: Consistent address formatting
    • -
    • API Integration: Standardize input/output formats
    • -
    • Data Migration: Convert between address formats
    • -
    • Educational Tools: Demonstrate IPv6 structure
    • -
    • Quality Assurance: Validate address representations
    • +
    • + {t('ipv6Notation.terms.networkMonitoringLabel')} + {t('ipv6Notation.useCases.scenarios.networkMonitoring')} +
    • +
    • + {t('ipv6Notation.terms.apiIntegrationLabel')} + {t('ipv6Notation.useCases.scenarios.apiIntegration')} +
    • +
    • + {t('ipv6Notation.terms.dataMigrationLabel')} + {t('ipv6Notation.useCases.scenarios.dataMigration')} +
    • +
    • + {t('ipv6Notation.terms.educationalToolsLabel')} + {t('ipv6Notation.useCases.scenarios.educationalTools')} +
    • +
    • + {t('ipv6Notation.terms.qualityAssuranceLabel')} + {t('ipv6Notation.useCases.scenarios.qualityAssurance')} +
    @@ -93,15 +151,15 @@

    - Technical Examples & Standards + {t('ipv6Notation.conversions.examples.title')}

    -

    Common Address Types

    +

    {t('ipv6Notation.examples.commonTypes')}

    -
    Loopback:
    +
    {t('ipv6Notation.examples.loopback')}
    ::1 ↔ @@ -109,7 +167,7 @@
    -
    Link-Local:
    +
    {t('ipv6Notation.examples.linkLocal')}
    fe80::1 ↔ @@ -117,7 +175,7 @@
    -
    Documentation:
    +
    {t('ipv6Notation.examples.documentation')}
    2001:db8:: ↔ @@ -128,13 +186,28 @@
    -

    Best Practices

    +

    {t('ipv6Notation.examples.bestPractices.title')}

      -
    • RFC 5952: Follow standard compression guidelines
    • -
    • Consistency: Use same format throughout applications
    • -
    • Validation: Always validate both input and output
    • -
    • Case Sensitivity: Lowercase preferred (RFC 5952)
    • -
    • Leading Zeros: Always remove for compressed form
    • +
    • + {t('ipv6Notation.terms.rfc5952Label')} + {t('ipv6Notation.examples.bestPractices.rfc5952')} +
    • +
    • + {t('ipv6Notation.terms.consistencyLabel')} + {t('ipv6Notation.examples.bestPractices.consistency')} +
    • +
    • + {t('ipv6Notation.terms.validationLabel')} + {t('ipv6Notation.examples.bestPractices.validation')} +
    • +
    • + {t('ipv6Notation.terms.caseSensitivityLabel')} + {t('ipv6Notation.examples.bestPractices.caseSensitivity')} +
    • +
    • + {t('ipv6Notation.terms.leadingZerosRuleLabel')} + {t('ipv6Notation.examples.bestPractices.leadingZeros')} +
    diff --git a/src/routes/reference/arp-vs-ndp/+page.svelte b/src/routes/reference/arp-vs-ndp/+page.svelte index aeaf32eb..5446a31d 100644 --- a/src/routes/reference/arp-vs-ndp/+page.svelte +++ b/src/routes/reference/arp-vs-ndp/+page.svelte @@ -1,7 +1,13 @@
    @@ -17,13 +23,13 @@
    -

    Side-by-Side Comparison

    +

    {$t('comparison.title')}

    - - + + @@ -41,26 +47,26 @@

    {arpVsNdpContent.arpDetails.title}

    -

    ARP Message Types

    +

    {$t('arp.messageTypes.title')}

    {#each arpVsNdpContent.arpDetails.messageTypes as type, index (`${type.type}-${index}`)}
    {type.type}
    -
    Description: {type.description}
    -
    Destination: {type.destination}
    -
    Response: {type.response}
    +
    {$t('arp.messageTypes.fields.description')} {type.description}
    +
    {$t('arp.messageTypes.fields.destination')} {type.destination}
    +
    {$t('arp.messageTypes.fields.response')} {type.response}
    {/each} -

    ARP Process

    +

    {$t('arp.process.title')}

      {#each arpVsNdpContent.arpDetails.process as step, index (`arp-process-${index}`)}
    1. {step}
    2. {/each}
    -

    ARP Limitations

    +

    {$t('arp.limitations.title')}

      {#each arpVsNdpContent.arpDetails.limitations as limitation, index (`arp-limitation-${index}`)}
    • {limitation}
    • @@ -71,27 +77,27 @@

      {arpVsNdpContent.ndpDetails.title}

      -

      NDP Message Types

      +

      {$t('ndp.messageTypes.title')}

      {#each arpVsNdpContent.ndpDetails.messageTypes as type, index (`${type.type}-${index}`)}
      {type.type}
      -
      ICMP Type: {type.icmpType}
      -
      Description: {type.description}
      -
      Destination: {type.destination}
      -
      Purpose: {type.purpose}
      +
      {$t('ndp.messageTypes.fields.icmpType')} {type.icmpType}
      +
      {$t('ndp.messageTypes.fields.description')} {type.description}
      +
      {$t('ndp.messageTypes.fields.destination')} {type.destination}
      +
      {$t('ndp.messageTypes.fields.purpose')} {type.purpose}
      {/each} -

      NDP Process

      +

      {$t('ndp.process.title')}

        {#each arpVsNdpContent.ndpDetails.process as step, index (`ndp-process-${index}`)}
      1. {step}
      2. {/each}
      -

      NDP Advantages Over ARP

      +

      {$t('ndp.advantages.title')}

        {#each arpVsNdpContent.ndpDetails.advantages as advantage, index (`ndp-advantage-${index}`)}
      • {advantage}
      • @@ -100,27 +106,27 @@
      -

      Practical Differences

      +

      {$t('practical.title')}

      {#each arpVsNdpContent.practicalDifferences as diff, index (`${diff.scenario}-${index}`)}
      {diff.scenario}
      -
      ARP (IPv4): {diff.arp}
      -
      NDP (IPv6): {diff.ndp}
      -
      Impact: {diff.impact}
      +
      {$t('practical.fields.arp')} {diff.arp}
      +
      {$t('practical.fields.ndp')} {diff.ndp}
      +
      {$t('practical.fields.impact')} {diff.impact}
      {/each}
      -

      Troubleshooting Commands

      +

      {$t('troubleshooting.title')}

    AspectARP (IPv4)NDP (IPv6){$t('comparison.headers.arp')}{$t('comparison.headers.ndp')}
    - - + + @@ -138,7 +144,7 @@
    -

    Common Issues

    +

    {$t('issues.title')}

    {#each arpVsNdpContent.commonIssues as issue, index (`${issue.issue}-${index}`)}
    @@ -146,16 +152,16 @@ {issue.issue} ({issue.protocol})
    -

    Description: {issue.description}

    -

    Detection: {issue.detection}

    -

    Mitigation: {issue.mitigation}

    +

    {$t('issues.fields.description')} {issue.description}

    +

    {$t('issues.fields.detection')} {issue.detection}

    +

    {$t('issues.fields.mitigation')} {issue.mitigation}

    {/each}
    -

    Best Practices

    +

    {$t('bestPractices.title')}

    {#each arpVsNdpContent.bestPractices as practices, index (`${practices.protocol}-${index}`)}

    {practices.protocol} Best Practices

      @@ -167,17 +173,17 @@
    -

    Quick Reference

    +

    {$t('quickReference.title')}

    -
    ARP Key Points
    +
    {$t('quickReference.arp')}
    {#each arpVsNdpContent.quickReference.arp as point, index (`arp-point-${index}`)}
    {point}
    {/each}
    -
    NDP Key Points
    +
    {$t('quickReference.ndp')}
    {#each arpVsNdpContent.quickReference.ndp as point, index (`ndp-point-${index}`)}
    {point}
    {/each} @@ -186,9 +192,9 @@
    -

    IPv4 to IPv6 Migration Tips

    +

    {$t('migration.title')}

    -
    Important Considerations
    +
    {$t('migration.considerations')}
    {#each arpVsNdpContent.migrationTips as tip, index (`migration-tip-${index}`)}
    {tip}
    diff --git a/src/routes/reference/cgnat/+page.svelte b/src/routes/reference/cgnat/+page.svelte index 6df13bca..de6a35e0 100644 --- a/src/routes/reference/cgnat/+page.svelte +++ b/src/routes/reference/cgnat/+page.svelte @@ -1,7 +1,13 @@
    @@ -22,27 +28,36 @@
    -

    CGNAT Address Range

    +

    {$t('pages.cgnat.addressRange.title')}

    - Shared Address Space + {$t('pages.cgnat.addressRange.sharedSpace')}
    -

    Range: {cgnatContent.addressRange.range}

    -

    Full Range: {cgnatContent.addressRange.fullRange}

    -

    Total Addresses: {cgnatContent.addressRange.totalAddresses}

    -

    RFC: {cgnatContent.addressRange.rfc}

    +

    + {$t('pages.cgnat.addressRange.labels.range')}: + {cgnatContent.addressRange.range} +

    +

    + {$t('pages.cgnat.addressRange.labels.fullRange')}: + {cgnatContent.addressRange.fullRange} +

    +

    + {$t('pages.cgnat.addressRange.labels.totalAddresses')}: + {cgnatContent.addressRange.totalAddresses} +

    +

    {$t('pages.cgnat.addressRange.labels.rfc')}: {cgnatContent.addressRange.rfc}

    -

    Address Breakdown

    +

    {$t('pages.cgnat.addressRange.breakdown.title')}

    PurposeIPv4 (ARP)IPv6 (NDP){$t('troubleshooting.headers.ipv4')}{$t('troubleshooting.headers.ipv6')} Windows
    - - - + + + @@ -61,15 +76,15 @@

    {cgnatContent.howItWorks.title}

    {cgnatContent.howItWorks.description}

    -

    Two-Layer NAT System

    +

    {$t('pages.cgnat.natSystem.title')}

    Network BlockAvailable AddressesTypical Use{$t('pages.cgnat.addressRange.breakdown.headers.network')}{$t('pages.cgnat.addressRange.breakdown.headers.addresses')}{$t('pages.cgnat.addressRange.breakdown.headers.use')}
    - - - - - + + + + + @@ -85,7 +100,7 @@
    LayerLocationInside AddressOutside AddressPurpose{$t('pages.cgnat.natSystem.headers.layer')}{$t('pages.cgnat.natSystem.headers.location')}{$t('pages.cgnat.natSystem.headers.insideAddress')}{$t('pages.cgnat.natSystem.headers.outsideAddress')}{$t('pages.cgnat.natSystem.headers.purpose')}
    -

    Traffic Flow

    +

    {$t('pages.cgnat.trafficFlow.title')}

      {#each cgnatContent.howItWorks.flow as step, index (`flow-step-${index}`)}
    1. {step}
    2. @@ -99,12 +114,13 @@
      {method.method}
      -
      Description: {method.description}
      +
      {$t('pages.cgnat.identification.labels.description')}: {method.description}
      - CGNAT Indicator: {method.cgnatIndicator} + {$t('pages.cgnat.identification.labels.cgnatIndicator')}: + {method.cgnatIndicator}
      - Normal Indicator: + {$t('pages.cgnat.identification.labels.normalIndicator')}: {method.normalIndicator}
      @@ -113,9 +129,9 @@
      -

      Impact on Services

      +

      {$t('pages.cgnat.impacts.title')}

      -

      Negative Impacts

      +

      {$t('pages.cgnat.impacts.negative.title')}

      {#each cgnatContent.impacts.negative as impact, index (`${impact.impact}-${index}`)}
      @@ -123,14 +139,17 @@ {impact.impact}
      -

      Description: {impact.description}

      -

      Affected Services: {impact.affectedServices.join(', ')}

      -

      Workaround: {impact.workaround}

      +

      {$t('pages.cgnat.impacts.negative.labels.description')}: {impact.description}

      +

      + {$t('pages.cgnat.impacts.negative.labels.affectedServices')}: + {impact.affectedServices.join(', ')} +

      +

      {$t('pages.cgnat.impacts.negative.labels.workaround')}: {impact.workaround}

      {/each} -

      Positive Aspects

      +

      {$t('pages.cgnat.impacts.positive.title')}

        {#each cgnatContent.impacts.positive as positive, index (`positive-${index}`)}
      • {positive}
      • @@ -139,21 +158,21 @@
      -

      Workarounds and Solutions

      +

      {$t('pages.cgnat.workarounds.title')}

      {#each cgnatContent.workarounds as solution, index (`${solution.solution}-${index}`)}
      {solution.solution}
      -
      Description: {solution.description}
      -
      Effectiveness: {solution.effectiveness}
      -
      Cost: {solution.cost}
      +
      {$t('pages.cgnat.workarounds.labels.description')}: {solution.description}
      +
      {$t('pages.cgnat.workarounds.labels.effectiveness')}: {solution.effectiveness}
      +
      {$t('pages.cgnat.workarounds.labels.cost')}: {solution.cost}
      {/each}
      -

      Troubleshooting Common Issues

      +

      {$t('pages.cgnat.troubleshooting.title')}

      {#each cgnatContent.troubleshooting as issue, index (`${issue.issue}-${index}`)}
      @@ -161,20 +180,20 @@ {issue.issue}
      -

      Cause: {issue.cause}

      -

      Diagnosis: {issue.diagnosis}

      -

      Solution: {issue.solution}

      +

      {$t('pages.cgnat.troubleshooting.labels.cause')}: {issue.cause}

      +

      {$t('pages.cgnat.troubleshooting.labels.diagnosis')}: {issue.diagnosis}

      +

      {$t('pages.cgnat.troubleshooting.labels.solution')}: {issue.solution}

      {/each}
      -

      Quick CGNAT Check

      +

      {$t('pages.cgnat.quickCheck.title')}

      -
      Steps to Check
      +
      {$t('pages.cgnat.quickCheck.stepsTitle')}
        {#each cgnatContent.quickCheck.steps as step, index (`quickcheck-step-${index}`)}
      1. {step}
      2. @@ -183,7 +202,7 @@
      -
      What to Do Next
      +
      {$t('pages.cgnat.quickCheck.nextStepsTitle')}
        {#each cgnatContent.quickCheck.whatToDo as action, index (`whatToDo-${index}`)}
      • {action}
      • @@ -194,7 +213,7 @@
      -

      Best Practices

      +

      {$t('pages.cgnat.bestPractices.title')}

        {#each cgnatContent.bestPractices as practice, index (`practice-${index}`)}
      • {practice}
      • @@ -203,9 +222,9 @@
      -

      ISP Perspective

      +

      {$t('pages.cgnat.ispPerspective.title')}

      -
      Why ISPs Use CGNAT
      +
      {$t('pages.cgnat.ispPerspective.whyTitle')}
      {#each cgnatContent.ispPerspective as reason, index (`isp-reason-${index}`)}
      {reason}
      @@ -216,11 +235,10 @@
      - Understanding the Trade-off + {$t('pages.cgnat.ispPerspective.tradeoffTitle')}
      - CGNAT is a necessary compromise. It allows ISPs to provide affordable internet service during IPv4 exhaustion, - but at the cost of some functionality. The long-term solution is IPv6 adoption. + {$t('pages.cgnat.ispPerspective.tradeoffDescription')}
      diff --git a/src/routes/reference/ipv6-privacy-addresses/+page.svelte b/src/routes/reference/ipv6-privacy-addresses/+page.svelte index 72b6289b..6be0ff04 100644 --- a/src/routes/reference/ipv6-privacy-addresses/+page.svelte +++ b/src/routes/reference/ipv6-privacy-addresses/+page.svelte @@ -1,312 +1,334 @@ + + {ipv6PrivacyContent ? ipv6PrivacyContent.title : ''}{$t('common.meta.titleSeparator')}{$t( + 'common.meta.titleSuffix', + )} + + +
      -
      -

      {ipv6PrivacyContent.title}

      -

      {ipv6PrivacyContent.description}

      -
      - -
      -

      {ipv6PrivacyContent.sections.overview.title}

      -

      {ipv6PrivacyContent.sections.overview.content}

      -
      - -
      -

      {ipv6PrivacyContent.sections.problem.title}

      -

      {ipv6PrivacyContent.sections.problem.content}

      -
      - -
      -

      IPv6 Address Types

      - {#each ipv6PrivacyContent.addressTypes as type, index (`${type.type}-${index}`)} -
      -
      {type.type}
      -
      -
      Formation: {type.formation}
      -
      Example: {type.example}
      -
      Privacy Level: {type.privacy}
      - -

      Characteristics:

      -
        - {#each type.characteristics as characteristic, index (`characteristic-${index}`)} -
      • {characteristic}
      • - {/each} -
      -
      -
      - {/each} -
      + {#if ipv6PrivacyContent} +
      +

      {ipv6PrivacyContent.title}

      +

      {ipv6PrivacyContent.description}

      +
      -
      -

      {ipv6PrivacyContent.howItWorks.title}

      +
      +

      {ipv6PrivacyContent.sections.overview.title}

      +

      {ipv6PrivacyContent.sections.overview.content}

      +
      -

      Address Generation Process

      -
        - {#each ipv6PrivacyContent.howItWorks.addressGeneration as step, index (`gen-step-${index}`)} -
      1. {step}
      2. - {/each} -
      +
      +

      {ipv6PrivacyContent.sections.problem.title}

      +

      {ipv6PrivacyContent.sections.problem.content}

      +
      -

      Temporary Address Lifecycle

      -
        - {#each ipv6PrivacyContent.howItWorks.temporaryLifecycle as step, index (`lifecycle-step-${index}`)} -
      1. {step}
      2. +
        +

        {ipv6PrivacyContent.addressTypes.title}

        + {#each ipv6PrivacyContent.addressTypes.types as type, index (`${type.type}-${index}`)} +
        +
        {type.type}
        +
        +
        Formation: {type.formation}
        +
        Example: {type.example}
        +
        Privacy Level: {type.privacy}
        + +

        Characteristics:

        +
          + {#each type.characteristics as characteristic, index (`characteristic-${index}`)} +
        • {characteristic}
        • + {/each} +
        +
        +
        {/each} -
      +
      -

      Default Operating System Behavior

      -
        - {#each ipv6PrivacyContent.howItWorks.defaultBehavior as behavior, index (`behavior-${index}`)} -
      • {behavior}
      • - {/each} -
      -
      - -
      -

      {ipv6PrivacyContent.lifetimes.title}

      - -
      -
      -
      Preferred Lifetime
      -
      {ipv6PrivacyContent.lifetimes.preferredLifetime.description}
      -
      Typical: {ipv6PrivacyContent.lifetimes.preferredLifetime.typical}
      -
      Behavior: {ipv6PrivacyContent.lifetimes.preferredLifetime.behavior}
      -
      +
      +

      {ipv6PrivacyContent.howItWorks.title}

      -
      -
      Valid Lifetime
      -
      {ipv6PrivacyContent.lifetimes.validLifetime.description}
      -
      Typical: {ipv6PrivacyContent.lifetimes.validLifetime.typical}
      -
      Behavior: {ipv6PrivacyContent.lifetimes.validLifetime.behavior}
      -
      +

      Address Generation Process

      +
        + {#each ipv6PrivacyContent.howItWorks.addressGeneration as step, index (`gen-step-${index}`)} +
      1. {step}
      2. + {/each} +
      -
      -
      Regeneration Interval
      -
      {ipv6PrivacyContent.lifetimes.regenerationInterval.description}
      -
      Typical: {ipv6PrivacyContent.lifetimes.regenerationInterval.typical}
      -
      Behavior: {ipv6PrivacyContent.lifetimes.regenerationInterval.behavior}
      -
      +

      Temporary Address Lifecycle

      +
        + {#each ipv6PrivacyContent.howItWorks.temporaryLifecycle as step, index (`lifecycle-step-${index}`)} +
      1. {step}
      2. + {/each} +
      -
      -
      Max Temporary Addresses
      -
      {ipv6PrivacyContent.lifetimes.maxTempAddresses.description}
      -
      Typical: {ipv6PrivacyContent.lifetimes.maxTempAddresses.typical}
      -
      Behavior: {ipv6PrivacyContent.lifetimes.maxTempAddresses.behavior}
      -
      +

      Default Operating System Behavior

      +
        + {#each ipv6PrivacyContent.howItWorks.defaultBehavior as behavior, index (`behavior-${index}`)} +
      • {behavior}
      • + {/each} +
      -
      -
      -

      {ipv6PrivacyContent.osImplementations.title}

      +
      +

      {ipv6PrivacyContent.lifetimes.title}

      - {#each Object.entries(ipv6PrivacyContent.osImplementations) as [key, os] (key)} - {#if typeof os === 'object' && os.os} -
      -
      {os.os}
      -
      -
      Default Behavior: {os.defaultBehavior}
      - -

      Configuration:

      - {#each os.configuration as config, index (`config-${index}`)} - {config} - {/each} - - {#if (os as OSImplementation).values} -

      Values:

      -
        - {#each (os as OSImplementation).values! as value, index (`value-${index}`)} -
      • {value}
      • - {/each} -
      - {/if} - -

      Useful Commands:

      - {#each (os as OSImplementation).commands as command, index (`command-${index}`)} - {command} - {/each} - - {#if (os as OSImplementation).behavior} -
      Behavior: {(os as OSImplementation).behavior}
      - {/if} -
      +
      +
      +
      Preferred Lifetime
      +
      {ipv6PrivacyContent.lifetimes.preferredLifetime.description}
      +
      Typical: {ipv6PrivacyContent.lifetimes.preferredLifetime.typical}
      +
      Behavior: {ipv6PrivacyContent.lifetimes.preferredLifetime.behavior}
      - {/if} - {/each} -
      - -
      -

      Identifying Address Types

      - - - - - - - - - - - {#each ipv6PrivacyContent.identifyingAddresses as method, index (`method-${index}`)} - - - - - - - {/each} - -
      MethodStable AddressTemporary AddressExample
      {method.method}{method.stable}{method.temporary}{method.example}
      -
      - -
      -

      Troubleshooting

      - {#each ipv6PrivacyContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} -
      -
      - - {issue.issue} + +
      +
      Valid Lifetime
      +
      {ipv6PrivacyContent.lifetimes.validLifetime.description}
      +
      Typical: {ipv6PrivacyContent.lifetimes.validLifetime.typical}
      +
      Behavior: {ipv6PrivacyContent.lifetimes.validLifetime.behavior}
      -
      -

      Symptoms: {issue.symptoms.join(', ')}

      -

      Diagnosis: {issue.diagnosis}

      -
      Solutions:
      -
        - {#each issue.solutions as solution, index (`solution-${index}`)} -
      • {solution}
      • - {/each} -
      + +
      +
      Regeneration Interval
      +
      {ipv6PrivacyContent.lifetimes.regenerationInterval.description}
      +
      Typical: {ipv6PrivacyContent.lifetimes.regenerationInterval.typical}
      +
      Behavior: {ipv6PrivacyContent.lifetimes.regenerationInterval.behavior}
      -
      - {/each} -
      - -
      -

      Security Considerations

      - {#each ipv6PrivacyContent.securityConsiderations as security, index (`${security.aspect}-${index}`)} -
      -
      {security.aspect}
      -
      -

      Benefits:

      -
        - {#each security.benefits as benefit, index (`benefit-${index}`)} -
      • {benefit}
      • - {/each} -
      - -

      {security.limitations ? 'Limitations' : 'Challenges'}:

      -
        - {#each security.limitations || security.challenges as item, index (`limitation-${index}`)} -
      • {item}
      • - {/each} -
      + +
      +
      Max Temporary Addresses
      +
      {ipv6PrivacyContent.lifetimes.maxTempAddresses.description}
      +
      Typical: {ipv6PrivacyContent.lifetimes.maxTempAddresses.typical}
      +
      Behavior: {ipv6PrivacyContent.lifetimes.maxTempAddresses.behavior}
      - {/each} -
      - -
      -

      When to Use Privacy Addresses

      - {#each ipv6PrivacyContent.whenToUse as scenario, index (`${scenario.scenario}-${index}`)} -
      -
      {scenario.scenario}
      -
      -
      Recommendation: {scenario.recommendation}
      -
      Reasoning: {scenario.reasoning}
      -
      Configuration: {scenario.configuration}
      +
      + +
      +

      {ipv6PrivacyContent.osImplementations.title}

      + + {#each Object.entries(ipv6PrivacyContent.osImplementations) as [key, os] (key)} + {#if isOSImplementation(os)} +
      +
      {os.os}
      +
      +
      Default Behavior: {os.defaultBehavior}
      + +

      Configuration:

      + {#each os.configuration || [] as config, index (`config-${index}`)} + {config} + {/each} + + {#if (os as OSImplementation).values} +

      Values:

      +
        + {#each (os as OSImplementation).values! as value, index (`value-${index}`)} +
      • {value}
      • + {/each} +
      + {/if} + +

      Useful Commands:

      + {#each (os as OSImplementation).commands as command, index (`command-${index}`)} + {command} + {/each} + + {#if (os as OSImplementation).behavior} +
      Behavior: {(os as OSImplementation).behavior}
      + {/if} +
      +
      + {/if} + {/each} +
      + +
      +

      {ipv6PrivacyContent.identifyingAddresses.title}

      + + + + + + + + + + + {#each ipv6PrivacyContent.identifyingAddresses.methods as method, index (`method-${index}`)} + + + + + + + {/each} + +
      {$t('pages.ipv6Privacy.labels.method')}{$t('pages.ipv6Privacy.labels.stable')}{$t('pages.ipv6Privacy.labels.temporary')}{$t('pages.ipv6Privacy.labels.example')}
      {method.method}{method.stable}{method.temporary}{method.example}
      +
      + +
      +

      {ipv6PrivacyContent.troubleshooting.title}

      + {#each ipv6PrivacyContent.troubleshooting.issues as issue, index (`${issue.issue}-${index}`)} +
      +
      + + {issue.issue} +
      +
      +

      {$t('pages.ipv6Privacy.labels.symptoms')}: {issue.symptoms.join(', ')}

      +

      {$t('pages.ipv6Privacy.labels.diagnosis')}: {issue.diagnosis}

      +
      {$t('pages.ipv6Privacy.labels.solutions')}:
      +
        + {#each issue.solutions as solution, index (`solution-${index}`)} +
      • {solution}
      • + {/each} +
      +
      -
      - {/each} -
      - -
      -

      Best Practices

      -
        - {#each ipv6PrivacyContent.bestPractices as practice, index (`practice-${index}`)} -
      • {practice}
      • {/each} -
      -
      - -
      -

      Common Mistakes

      -
        - {#each ipv6PrivacyContent.commonMistakes as mistake, index (`mistake-${index}`)} -
      • {mistake}
      • +
      + +
      +

      {ipv6PrivacyContent.securityConsiderations.title}

      + {#each ipv6PrivacyContent.securityConsiderations.aspects as security, index (`${security.aspect}-${index}`)} +
      +
      {security.aspect}
      +
      +

      {$t('pages.ipv6Privacy.labels.benefits')}:

      +
        + {#each security.benefits as benefit, index (`benefit-${index}`)} +
      • {benefit}
      • + {/each} +
      + +

      + {security.limitations + ? $t('pages.ipv6Privacy.labels.limitations') + : $t('pages.ipv6Privacy.labels.challenges')}: +

      +
        + {#each security.limitations || security.challenges as item, index (`limitation-${index}`)} +
      • {item}
      • + {/each} +
      +
      +
      {/each} -
-
+
-
-

Quick Reference

+
+

{ipv6PrivacyContent.whenToUse.title}

+ {#each ipv6PrivacyContent.whenToUse.scenarios as scenario, index (`${scenario.scenario}-${index}`)} +
+
{scenario.scenario}
+
+
{$t('pages.ipv6Privacy.labels.recommendation')}: {scenario.recommendation}
+
{$t('pages.ipv6Privacy.labels.reasoning')}: {scenario.reasoning}
+
{$t('pages.ipv6Privacy.labels.configuration')}: {scenario.configuration}
+
+
+ {/each} +
-
-
-
Address Types
- {#each ipv6PrivacyContent.quickReference.addressTypes as type, index (`qr-type-${index}`)} -
{type}
+
+

{ipv6PrivacyContent.bestPractices.title}

+
    + {#each ipv6PrivacyContent.bestPractices.practices as practice, index (`practice-${index}`)} +
  • {practice}
  • {/each} -
+ +
-
-
Identification
- {#each ipv6PrivacyContent.quickReference.identification as tip, index (`qr-id-${index}`)} -
{tip}
+
+

{ipv6PrivacyContent.commonMistakes.title}

+
    + {#each ipv6PrivacyContent.commonMistakes.mistakes as mistake, index (`mistake-${index}`)} +
  • {mistake}
  • {/each} -
+
-
-
-
Configuration
- {#each ipv6PrivacyContent.quickReference.configuration as config, index (`qr-config-${index}`)} -
{config}
- {/each} -
+
+

{ipv6PrivacyContent.quickReference.title}

-
-
Troubleshooting
- {#each ipv6PrivacyContent.quickReference.troubleshooting as tip, index (`qr-trouble-${index}`)} -
{tip}
- {/each} +
+
+
{ipv6PrivacyContent.quickReference.addressTypesTitle}
+ {#each ipv6PrivacyContent.quickReference.addressTypes as type, index (`qr-type-${index}`)} +
{type}
+ {/each} +
+ +
+
{ipv6PrivacyContent.quickReference.identificationTitle}
+ {#each ipv6PrivacyContent.quickReference.identification as tip, index (`qr-id-${index}`)} +
{tip}
+ {/each} +
-
-
-
- - Key Point +
+
+
{ipv6PrivacyContent.quickReference.configurationTitle}
+ {#each ipv6PrivacyContent.quickReference.configuration as config, index (`qr-config-${index}`)} +
{config}
+ {/each} +
+ +
+
{ipv6PrivacyContent.quickReference.troubleshootingTitle}
+ {#each ipv6PrivacyContent.quickReference.troubleshooting as tip, index (`qr-trouble-${index}`)} +
{tip}
+ {/each} +
-
- Privacy extensions create multiple IPv6 addresses per interface. Temporary addresses change periodically for - privacy, while stable addresses remain consistent for services. Both can coexist on the same interface. + +
+
+ + {ipv6PrivacyContent.quickReference.keyRuleTitle} +
+
+ {ipv6PrivacyContent.quickReference.keyRule} +
-
-
-

Useful Tools

-
- {#each ipv6PrivacyContent.tools as tool, index (`${tool.tool}-${index}`)} -
-
{tool.tool}
-
{tool.purpose}
-
- {/each} +
+

{ipv6PrivacyContent.tools.title}

+
+ {#each ipv6PrivacyContent.tools.tools as tool, index (`${tool.tool}-${index}`)} +
+
{tool.tool}
+
{tool.purpose}
+
+ {/each} +
-
+ {/if}
diff --git a/src/routes/reference/link-local-apipa/+page.svelte b/src/routes/reference/link-local-apipa/+page.svelte index 8c08cc30..af71e65a 100644 --- a/src/routes/reference/link-local-apipa/+page.svelte +++ b/src/routes/reference/link-local-apipa/+page.svelte @@ -2,6 +2,13 @@ import { linkLocalApipaContent } from '$lib/content/link-local-apipa.js'; import Icon from '$lib/components/global/Icon.svelte'; + import { t, loadTranslations, locale } from '$lib/stores/language'; + import { onMount } from 'svelte'; + import { get } from 'svelte/store'; + + onMount(async () => { + await loadTranslations(get(locale), 'pages/link-local-apipa'); + });
@@ -20,39 +27,51 @@

{linkLocalApipaContent.apipa.title}

-
Address Range
+
{$t('pages.linkLocalApipa.apipa.addressRange.title')}
-
Network: {linkLocalApipaContent.apipa.range}
-
Full Range: {linkLocalApipaContent.apipa.fullRange}
-
Usable Range: {linkLocalApipaContent.apipa.usableRange}
-
Reserved: {linkLocalApipaContent.apipa.reservedAddresses.join(', ')}
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.network')}: + {linkLocalApipaContent.apipa.range} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.fullRange')}: + {linkLocalApipaContent.apipa.fullRange} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.usableRange')}: + {linkLocalApipaContent.apipa.usableRange} +
+
+ {$t('pages.linkLocalApipa.apipa.addressRange.reserved')}: + {linkLocalApipaContent.apipa.reservedAddresses.join(', ')} +

{linkLocalApipaContent.apipa.description}

-

When APIPA is Used

+

{$t('pages.linkLocalApipa.apipa.whenUsedTitle')}

    {#each linkLocalApipaContent.apipa.whenUsed as reason, index (`apipa-reason-${index}`)}
  • {reason}
  • {/each}
-

How APIPA Works

+

{$t('pages.linkLocalApipa.apipa.howItWorksTitle')}

    {#each linkLocalApipaContent.apipa.howItWorks as step, index (`apipa-step-${index}`)}
  1. {step}
  2. {/each}
-

APIPA Characteristics

+

{$t('pages.linkLocalApipa.apipa.characteristicsTitle')}

    {#each linkLocalApipaContent.apipa.characteristics as characteristic, index (`apipa-char-${index}`)}
  • {characteristic}
  • {/each}
-

Troubleshooting APIPA Issues

+

{$t('pages.linkLocalApipa.apipa.troubleshootingTitle')}

{#each linkLocalApipaContent.apipa.troubleshooting as issue, index (`${issue.symptom}-${index}`)}
@@ -60,8 +79,8 @@ {issue.symptom}
-

Meaning: {issue.meaning}

-

Solution: {issue.solution}

+

{$t('pages.linkLocalApipa.apipa.troubleshootingLabels.meaning')}: {issue.meaning}

+

{$t('pages.linkLocalApipa.apipa.troubleshootingLabels.solution')}: {issue.solution}

{/each} @@ -71,59 +90,70 @@

{linkLocalApipaContent.ipv6LinkLocal.title}

-
Address Range
+
{$t('pages.linkLocalApipa.ipv6.addressRange.title')}
-
Network: {linkLocalApipaContent.ipv6LinkLocal.range}
-
Full Range: {linkLocalApipaContent.ipv6LinkLocal.fullRange}
-
Common Format: {linkLocalApipaContent.ipv6LinkLocal.commonFormat}
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.network')}: + {linkLocalApipaContent.ipv6LinkLocal.range} +
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.fullRange')}: + {linkLocalApipaContent.ipv6LinkLocal.fullRange} +
+
+ {$t('pages.linkLocalApipa.ipv6.addressRange.commonFormat')}: + {linkLocalApipaContent.ipv6LinkLocal.commonFormat} +

{linkLocalApipaContent.ipv6LinkLocal.description}

-

Address Formation

+

{$t('pages.linkLocalApipa.ipv6.addressFormationTitle')}

    {#each linkLocalApipaContent.ipv6LinkLocal.formation as step, index (`ipv6-formation-${index}`)}
  1. {step}
  2. {/each}
-

When IPv6 Link-Local is Used

+

{$t('pages.linkLocalApipa.ipv6.whenUsedTitle')}

    {#each linkLocalApipaContent.ipv6LinkLocal.whenUsed as use, index (`ipv6-use-${index}`)}
  • {use}
  • {/each}
-

IPv6 Link-Local Characteristics

+

{$t('pages.linkLocalApipa.ipv6.characteristicsTitle')}

    {#each linkLocalApipaContent.ipv6LinkLocal.characteristics as characteristic, index (`ipv6-char-${index}`)}
  • {characteristic}
  • {/each}
-

Types of IPv6 Link-Local Addresses

+

{$t('pages.linkLocalApipa.ipv6.typesTitle')}

{#each linkLocalApipaContent.ipv6LinkLocal.types as type, index (`${type.type}-${index}`)}
{type.type}
{type.description}
-
Example: {type.example}
-
Privacy: {type.privacy}
+
+ {$t('pages.linkLocalApipa.ipv6.typeLabels.example')}: {type.example} +
+
{$t('pages.linkLocalApipa.ipv6.typeLabels.privacy')}: {type.privacy}
{/each}
-

IPv4 APIPA vs IPv6 Link-Local Comparison

+

{$t('pages.linkLocalApipa.comparison.title')}

- - - + + + @@ -139,29 +169,35 @@
-

Practical Examples

+

{$t('pages.linkLocalApipa.practicalExamples.title')}

{#each linkLocalApipaContent.practicalExamples as example, index (`${example.scenario}-${index}`)}
{example.scenario}
-
IPv4 Behavior: {example.ipv4Behavior}
-
IPv6 Behavior: {example.ipv6Behavior}
-
Impact: {example.impact}
+
+ {$t('pages.linkLocalApipa.practicalExamples.labels.ipv4Behavior')}: + {example.ipv4Behavior} +
+
+ {$t('pages.linkLocalApipa.practicalExamples.labels.ipv6Behavior')}: + {example.ipv6Behavior} +
+
{$t('pages.linkLocalApipa.practicalExamples.labels.impact')}: {example.impact}
{/each}
-

Troubleshooting Commands

+

{$t('pages.linkLocalApipa.troubleshootingCommands.title')}

AspectIPv4 APIPAIPv6 Link-Local{$t('pages.linkLocalApipa.comparison.headers.aspect')}{$t('pages.linkLocalApipa.comparison.headers.ipv4Apipa')}{$t('pages.linkLocalApipa.comparison.headers.ipv6LinkLocal')}
- - - - + + + + @@ -178,20 +214,23 @@
-

When to Worry

+

{$t('pages.linkLocalApipa.whenToWorry.title')}

{#each linkLocalApipaContent.whenToWorry as situation, index (`${situation.situation}-${index}`)}
{situation.situation}
-
Concern Level: {situation.concern}
-
Action: {situation.action}
+
+ {$t('pages.linkLocalApipa.whenToWorry.labels.concernLevel')}: + {situation.concern} +
+
{$t('pages.linkLocalApipa.whenToWorry.labels.action')}: {situation.action}
{/each}
-

Best Practices

+

{$t('pages.linkLocalApipa.bestPractices.title')}

    {#each linkLocalApipaContent.bestPractices as practice, index (`practice-${index}`)}
  • {practice}
  • @@ -200,7 +239,7 @@
-

Common Mistakes

+

{$t('pages.linkLocalApipa.commonMistakes.title')}

    {#each linkLocalApipaContent.commonMistakes as mistake, index (`mistake-${index}`)}
  • {mistake}
  • @@ -209,18 +248,18 @@
-

Quick Reference

+

{$t('pages.linkLocalApipa.quickReference.title')}

-
Recognition
+
{$t('pages.linkLocalApipa.quickReference.recognitionTitle')}
{#each linkLocalApipaContent.quickReference.recognition as item, index (`recognition-${index}`)}
{item}
{/each}
-
Troubleshooting
+
{$t('pages.linkLocalApipa.quickReference.troubleshootingTitle')}
{#each linkLocalApipaContent.quickReference.troubleshooting as item, index (`qr-trouble-${index}`)}
{item}
{/each} @@ -230,11 +269,10 @@
- Key Difference + {$t('pages.linkLocalApipa.quickReference.keyDifferenceTitle')}
- IPv4 APIPA (169.254.x.x) indicates a problem - DHCP failed. IPv6 link-local (fe80::) is normal and required - - every IPv6 interface has one. + {$t('pages.linkLocalApipa.quickReference.keyDifferenceText')}
diff --git a/src/routes/reference/private-vs-public-ip/+page.svelte b/src/routes/reference/private-vs-public-ip/+page.svelte index 3fdf243f..a34bfd7f 100644 --- a/src/routes/reference/private-vs-public-ip/+page.svelte +++ b/src/routes/reference/private-vs-public-ip/+page.svelte @@ -1,31 +1,37 @@
-

{privateVsPublicContent.title}

-

{privateVsPublicContent.description}

+

{$t('title')}

+

{$t('description')}

-

{privateVsPublicContent.sections.overview.title}

-

{privateVsPublicContent.sections.overview.content}

+

{$t('sections.overview.title')}

+

{$t('sections.overview.content')}

-

Private IP Address Ranges (RFC 1918)

+

{$t('privateRanges.title')}

{#each privateVsPublicContent.privateRanges as range, index (`${range.range}-${index}`)}
{range.range} - {range.class}
-
Full Range: {range.fullRange}
-
Total Addresses: {range.addresses}
-
Common Use: {range.commonUse}
-
Examples:
+
{$t('privateRanges.labels.fullRange')} {range.fullRange}
+
{$t('privateRanges.labels.totalAddresses')} {range.addresses}
+
{$t('privateRanges.labels.commonUse')} {range.commonUse}
+
{$t('privateRanges.labels.examples')}
{#each range.examples as example, index (`example-${index}`)} {example} {/each} @@ -35,22 +41,22 @@
-

Public IP Addresses

-

{privateVsPublicContent.publicRanges.description}

+

{$t('publicRanges.title')}

+

{$t('publicRanges.description')}

-

Characteristics

+

{$t('publicRanges.characteristics.title')}

    - {#each privateVsPublicContent.publicRanges.characteristics as characteristic, index (`char-${index}`)} + {#each $t('publicRanges.characteristics.items') as characteristic, index (`char-${index}`)}
  • {characteristic}
  • {/each}
-

Examples

+

{$t('publicRanges.examples.title')}

PurposeWindowsLinuxmacOS{$t('pages.linkLocalApipa.troubleshootingCommands.headers.purpose')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.windows')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.linux')}{$t('pages.linkLocalApipa.troubleshootingCommands.headers.macOS')}
- - + + @@ -65,23 +71,23 @@
-

{privateVsPublicContent.natImplications.title}

+

{$t('natImplications.title')}

{privateVsPublicContent.natImplications.privateToPublic.title}
{privateVsPublicContent.natImplications.privateToPublic.description}
-

Process:

+

{$t('natImplications.privateToPublic.process.title')}

    - {#each privateVsPublicContent.natImplications.privateToPublic.process as step, index (`nat-step-${index}`)} + {#each $t('natImplications.privateToPublic.process.steps') as step, index (`nat-step-${index}`)}
  1. {step}
  2. {/each}
-

Benefits:

+

{$t('natImplications.privateToPublic.benefits.title')}

    - {#each privateVsPublicContent.natImplications.privateToPublic.benefits as benefit, index (`benefit-${index}`)} + {#each $t('natImplications.privateToPublic.benefits.items') as benefit, index (`benefit-${index}`)}
  • {benefit}
  • {/each}
@@ -91,16 +97,16 @@
{privateVsPublicContent.natImplications.publicToPrivate.title}
{privateVsPublicContent.natImplications.publicToPrivate.description}
-

Challenges:

+

{$t('natImplications.publicToPrivate.challenges.title')}

    - {#each privateVsPublicContent.natImplications.publicToPrivate.challenges as challenge, index (`challenge-${index}`)} + {#each $t('natImplications.publicToPrivate.challenges.items') as challenge, index (`challenge-${index}`)}
  • {challenge}
  • {/each}
-

Solutions:

+

{$t('natImplications.publicToPrivate.solutions.title')}

    - {#each privateVsPublicContent.natImplications.publicToPrivate.solutions as solution, index (`solution-${index}`)} + {#each $t('natImplications.publicToPrivate.solutions.items') as solution, index (`solution-${index}`)}
  • {solution}
  • {/each}
@@ -109,15 +115,15 @@
-

Quick Identification Methods

+

{$t('identification.title')}

Public IPOwner/Service{$t('publicRanges.examples.headers.publicIp')}{$t('publicRanges.examples.headers.ownerService')}
- + - - + + @@ -132,7 +138,7 @@
Method{$t('identification.headers.method')} DescriptionPrivate IndicatorPublic Indicator{$t('identification.headers.privateIndicator')}{$t('identification.headers.publicIndicator')}
-

Useful Tools

+

{$t('tools.title')}

{#each privateVsPublicContent.identification.tools as tool, index (`${tool.tool}-${index}`)}
@@ -144,22 +150,22 @@
-

Common Network Scenarios

+

{$t('commonScenarios.title')}

{#each privateVsPublicContent.commonScenarios as scenario, index (`${scenario.scenario}-${index}`)}
{scenario.scenario}
-
Setup: {scenario.setup}
-
Private IPs: {scenario.privateIPs}
-
Public IP: {scenario.publicIP}
-
NAT Behavior: {scenario.natBehavior}
+
{$t('commonScenarios.labels.setup')} {scenario.setup}
+
{$t('commonScenarios.labels.privateIps')} {scenario.privateIPs}
+
{$t('commonScenarios.labels.publicIp')} {scenario.publicIP}
+
{$t('commonScenarios.labels.natBehavior')} {scenario.natBehavior}
{/each}
-

Troubleshooting Common Issues

+

{$t('troubleshooting.title')}

{#each privateVsPublicContent.troubleshooting as issue, index (`${issue.issue}-${index}`)}
@@ -167,16 +173,16 @@ {issue.issue}
-

Possible Causes: {issue.possibleCauses.join(', ')}

-

Diagnosis: {issue.diagnosis}

-

Solution: {issue.solution}

+

{$t('troubleshooting.labels.possibleCauses')} {issue.possibleCauses.join(', ')}

+

{$t('troubleshooting.labels.diagnosis')} {issue.diagnosis}

+

{$t('troubleshooting.labels.solution')} {issue.solution}

{/each}
-

Security Considerations

+

{$t('security.title')}

{#each privateVsPublicContent.securityConsiderations as security, index (`${security.aspect}-${index}`)}
{security.aspect}
@@ -192,7 +198,7 @@
-

Best Practices

+

{$t('bestPractices.title')}

    {#each privateVsPublicContent.bestPractices as practice, index (`practice-${index}`)}
  • {practice}
  • @@ -201,18 +207,18 @@
-

Quick Reference

+

{$t('quickReference.title')}

-
Private IP Ranges
+
{$t('quickReference.privateRanges.title')}
{#each privateVsPublicContent.quickReference.privateRanges as range, index (`qr-range-${index}`)}
{range}
{/each}
-
Identification Tips
+
{$t('quickReference.identificationTips.title')}
{#each privateVsPublicContent.quickReference.identificationTips as tip, index (`qr-tip-${index}`)}
{tip}
{/each} diff --git a/src/routes/reference/reverse-zones/+page.svelte b/src/routes/reference/reverse-zones/+page.svelte index c509d016..3357360b 100644 --- a/src/routes/reference/reverse-zones/+page.svelte +++ b/src/routes/reference/reverse-zones/+page.svelte @@ -1,276 +1,294 @@
-
-

{reverseZonesContent.title}

-

{reverseZonesContent.description}

-
+ {#if reverseZonesContent} +
+

{reverseZonesContent.title}

+

{reverseZonesContent.description}

+
-
-

{reverseZonesContent.sections.overview.title}

-

{reverseZonesContent.sections.overview.content}

-
+
+

{reverseZonesContent.sections.overview.title}

+

{reverseZonesContent.sections.overview.content}

+
-
-

{reverseZonesContent.sections.delegation.title}

-

{reverseZonesContent.sections.delegation.content}

-
+
+

{reverseZonesContent.sections.delegation.title}

+

{reverseZonesContent.sections.delegation.content}

+
-
-

{reverseZonesContent.ipv4Zones.title}

+
+

{reverseZonesContent.ipv4Zones.title}

-

Classful Boundaries (Octet-Aligned)

- - - - - - - - - - - - {#each reverseZonesContent.ipv4Zones.classfullBoundaries as boundary, index (`${boundary.cidr}-${index}`)} +

{$t('pages.reverseZones.ipv4Zones.classfullBoundariesTitle')}

+
CIDRExampleReverse ZoneDescriptionDelegation
+ - - - - - + + + + + - {/each} - -
{boundary.cidr}{boundary.example}{boundary.reverseZone}{boundary.description}{boundary.delegation}{$t('pages.reverseZones.ipv4Zones.tableHeaders.cidr')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.example')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.reverseZone')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.description')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.delegation')}
- -

Classless Delegation (CNAME Method)

- {#each reverseZonesContent.ipv4Zones.classlessDelegation as delegation, index (`${delegation.cidr}-${index}`)} -
-
{delegation.cidr} - {delegation.example}
-
-
Addresses: {delegation.addresses}
-
Problem: {delegation.problem}
-
Solution: {delegation.solution}
-
Zone Names:
- {#each delegation.zones as zone, index (`zone-${index}`)} - {zone} + + + {#each reverseZonesContent.ipv4Zones.classfullBoundaries as boundary, index (`${boundary.cidr}-${index}`)} + + {boundary.cidr} + {boundary.example} + {boundary.reverseZone} + {boundary.description} + {boundary.delegation} + {/each} -
-
- {/each} + + -

Practical IPv4 Examples

- {#each reverseZonesContent.ipv4Zones.practicalExamples as example, index (`${example.network}-${index}`)} -
-
{example.scenario}
-
-
Network: {example.network}
-
Reverse Zone: {example.reverseZone}
- {#if example.reverseZones} -
Reverse Zones:
- {#each example.reverseZones as zone, index (`rz-${index}`)} +

{$t('pages.reverseZones.ipv4Zones.classlessDelegationTitle')}

+ {#each reverseZonesContent.ipv4Zones.classlessDelegation as delegation, index (`${delegation.cidr}-${index}`)} +
+
{delegation.cidr} - {delegation.example}
+
+
{$t('pages.reverseZones.labels.addresses')}: {delegation.addresses}
+
{$t('pages.reverseZones.labels.problem')}: {delegation.problem}
+
{$t('pages.reverseZones.labels.solution')}: {delegation.solution}
+
{$t('pages.reverseZones.labels.zoneNames')}:
+ {#each delegation.zones as zone, index (`zone-${index}`)} {zone} {/each} -
Description: {example.description}
- {:else} -
PTR Records:
- {#each example.ptrRecords as record, index (`ptr-${index}`)} - {record} - {/each} - {/if} -
Delegation: {example.delegation}
+
-
- {/each} -
+ {/each} + +

{$t('pages.reverseZones.ipv4Zones.practicalExamplesTitle')}

+ {#each reverseZonesContent.ipv4Zones.practicalExamples as example, index (`${example.network}-${index}`)} +
+
{example.scenario}
+
+
{$t('pages.reverseZones.labels.network')}: {example.network}
+
+ {$t('pages.reverseZones.labels.reverseZone')}: {example.reverseZone} +
+ {#if example.reverseZones} +
{$t('pages.reverseZones.labels.reverseZones')}:
+ {#each example.reverseZones as zone, index (`rz-${index}`)} + {zone} + {/each} +
{$t('pages.reverseZones.labels.description')}: {example.description}
+ {:else} +
{$t('pages.reverseZones.labels.ptrRecords')}:
+ {#each example.ptrRecords as record, index (`ptr-${index}`)} + {record} + {/each} + {/if} +
{$t('pages.reverseZones.labels.delegation')}: {example.delegation}
+
+
+ {/each} +
-
-

{reverseZonesContent.ipv6Zones.title}

+
+

{reverseZonesContent.ipv6Zones.title}

-

Nibble Boundaries (4-bit Aligned)

- - - - - - - - - - - - {#each reverseZonesContent.ipv6Zones.nibbleBoundaries as boundary, index (`${boundary.cidr}-${index}`)} +

{$t('pages.reverseZones.ipv6Zones.nibbleBoundariesTitle')}

+
CIDRExampleReverse ZoneDescriptionDelegation
+ - - - - - + + + + + - {/each} - -
{boundary.cidr}{boundary.example}{boundary.reverseZone}{boundary.description}{boundary.delegation}{$t('pages.reverseZones.ipv4Zones.tableHeaders.cidr')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.example')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.reverseZone')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.description')}{$t('pages.reverseZones.ipv4Zones.tableHeaders.delegation')}
- -

Practical IPv6 Examples

- {#each reverseZonesContent.ipv6Zones.practicalExamples as example, index (`${example.network}-${index}`)} -
-
{example.scenario}
-
-
Network: {example.network}
-
Master Zone: {example.reverseZone}
-
Sub-zones:
- {#each example.subZones as zone, index (`subzone-${index}`)} - {zone} + + + {#each reverseZonesContent.ipv6Zones.nibbleBoundaries as boundary, index (`${boundary.cidr}-${index}`)} + + {boundary.cidr} + {boundary.example} + {boundary.reverseZone} + {boundary.description} + {boundary.delegation} + {/each} -
Management: {example.management}
+ + + +

{$t('pages.reverseZones.ipv6Zones.practicalExamplesTitle')}

+ {#each reverseZonesContent.ipv6Zones.practicalExamples as example, index (`${example.network}-${index}`)} +
+
{example.scenario}
+
+
{$t('pages.reverseZones.labels.network')}: {example.network}
+
+ {$t('pages.reverseZones.labels.masterZone')}: {example.reverseZone} +
+
{$t('pages.reverseZones.labels.subZones')}:
+ {#each example.subZones as zone, index (`subzone-${index}`)} + {zone} + {/each} +
{$t('pages.reverseZones.labels.management')}: {example.management}
+
-
- {/each} -
+ {/each} +
-
-

{reverseZonesContent.zoneCreation.title}

+
+

{reverseZonesContent.zoneCreation.title}

-
-
-
IPv4 Example ({reverseZonesContent.zoneCreation.ipv4Example.network})
-
Zone Name: {reverseZonesContent.zoneCreation.ipv4Example.zoneName}
+
+
+
IPv4 Example ({reverseZonesContent.zoneCreation.ipv4Example.network})
+
+ {$t('pages.reverseZones.labels.zoneName')}: + {reverseZonesContent.zoneCreation.ipv4Example.zoneName} +
-

Zone File:

-
{reverseZonesContent.zoneCreation.ipv4Example.zoneFile}
+

{$t('pages.reverseZones.labels.zoneFile')}:

+
{reverseZonesContent.zoneCreation.ipv4Example.zoneFile}
-

Explanation:

-
    - {#each reverseZonesContent.zoneCreation.ipv4Example.explanation as point, index (`ipv4-point-${index}`)} -
  • {point}
  • - {/each} -
-
+

{$t('pages.reverseZones.labels.explanation')}:

+
    + {#each reverseZonesContent.zoneCreation.ipv4Example.explanation as point, index (`ipv4-point-${index}`)} +
  • {point}
  • + {/each} +
+
-
-
IPv6 Example ({reverseZonesContent.zoneCreation.ipv6Example.network})
-
Zone Name: {reverseZonesContent.zoneCreation.ipv6Example.zoneName}
+
+
IPv6 Example ({reverseZonesContent.zoneCreation.ipv6Example.network})
+
Zone Name: {reverseZonesContent.zoneCreation.ipv6Example.zoneName}
-

Zone File:

-
{reverseZonesContent.zoneCreation.ipv6Example.zoneFile}
+

{$t('pages.reverseZones.labels.zoneFile')}:

+
{reverseZonesContent.zoneCreation.ipv6Example.zoneFile}
-

Explanation:

-
    - {#each reverseZonesContent.zoneCreation.ipv6Example.explanation as point, index (`ipv6-point-${index}`)} -
  • {point}
  • - {/each} -
+

{$t('pages.reverseZones.labels.explanation')}:

+
    + {#each reverseZonesContent.zoneCreation.ipv6Example.explanation as point, index (`ipv6-point-${index}`)} +
  • {point}
  • + {/each} +
+
-
-
-

Delegation Scenarios

- {#each reverseZonesContent.delegationScenarios as scenario, index (`${scenario.scenario}-${index}`)} -
-
{scenario.scenario}
-
-
Delegation: {scenario.delegation}
+
+

{$t('pages.reverseZones.delegationScenarios.title')}

+ {#each reverseZonesContent.delegationScenarios as scenario, index (`${scenario.scenario}-${index}`)} +
+
{scenario.scenario}
+
+
{$t('pages.reverseZones.labels.delegation')}: {scenario.delegation}
- {#if scenario.customerActions} -
Customer Actions:
-
    - {#each scenario.customerActions as action, index (`customer-${index}`)} -
  • {action}
  • - {/each} -
+ {#if scenario.customerActions} +
{$t('pages.reverseZones.labels.customerActions')}:
+
    + {#each scenario.customerActions as action, index (`customer-${index}`)} +
  • {action}
  • + {/each} +
-
ISP Actions:
-
    - {#each scenario.ispActions as action, index (`isp-${index}`)} -
  • {action}
  • - {/each} -
- {:else} -
Process:
-
    - {#each scenario.process as step, index (`process-${index}`)} -
  1. {step}
  2. - {/each} -
- {/if} +
{$t('pages.reverseZones.labels.ispActions')}:
+
    + {#each scenario.ispActions as action, index (`isp-${index}`)} +
  • {action}
  • + {/each} +
+ {:else} +
{$t('pages.reverseZones.labels.process')}:
+
    + {#each scenario.process as step, index (`process-${index}`)} +
  1. {step}
  2. + {/each} +
+ {/if} +
-
- {/each} -
+ {/each} +
-
-

Troubleshooting

- {#each reverseZonesContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} -
-
- - {issue.issue} -
-
-

Possible Causes: {issue.causes.join(', ')}

-

Diagnosis: {issue.diagnosis}

-

Solution: {issue.solution}

+
+

{$t('pages.reverseZones.troubleshooting.title')}

+ {#each reverseZonesContent.troubleshooting as issue, index (`${issue.issue}-${index}`)} +
+
+ + {issue.issue} +
+
+

{$t('pages.reverseZones.labels.possibleCauses')}: {issue.causes.join(', ')}

+

{$t('pages.reverseZones.labels.diagnosis')}: {issue.diagnosis}

+

{$t('pages.reverseZones.labels.solution')}: {issue.solution}

+
-
- {/each} -
- -
-

Best Practices

-
    - {#each reverseZonesContent.bestPractices as practice, index (`practice-${index}`)} -
  • {practice}
  • {/each} -
-
- -
-

Quick Reference

- -
-
-
Zone Name Formulas
- {#each reverseZonesContent.quickReference.zoneFormulas as formula, index (`formula-${index}`)} -
{formula}
- {/each} -
+
-
-
Essential Records
- {#each reverseZonesContent.quickReference.essentialRecords as record, index (`record-${index}`)} -
{record}
+
+

{$t('pages.reverseZones.bestPractices.title')}

+
    + {#each reverseZonesContent.bestPractices as practice, index (`practice-${index}`)} +
  • {practice}
  • {/each} -
+
-
-
- - Key Rule +
+

{$t('pages.reverseZones.quickReference.title')}

+ +
+
+
{$t('pages.reverseZones.quickReference.zoneFormulasTitle')}
+ {#each reverseZonesContent.quickReference.zoneFormulas as formula, index (`formula-${index}`)} +
{formula}
+ {/each} +
+ +
+
{$t('pages.reverseZones.quickReference.essentialRecordsTitle')}
+ {#each reverseZonesContent.quickReference.essentialRecords as record, index (`record-${index}`)} +
{record}
+ {/each} +
-
- IPv4 reverse zones reverse the octets (192.0.2.0/24 β†’ 2.0.192.in-addr.arpa). IPv6 reverse zones reverse the - nibbles (2001:db8::/32 β†’ 8.b.d.0.1.0.0.2.ip6.arpa). + +
+
+ + {$t('pages.reverseZones.labels.keyRuleTitle')} +
+
+ {$t('pages.reverseZones.quickReference.keyRule')} +
-
-
-

Testing Tools

-
- {#each reverseZonesContent.tools as tool, index (`${tool.tool}-${index}`)} -
-
{tool.tool}
-
{tool.purpose}
-
- {/each} +
+

{$t('pages.reverseZones.testingTools.title')}

+
+ {#each reverseZonesContent.tools as tool, index (`${tool.tool}-${index}`)} +
+
{tool.tool}
+
{tool.purpose}
+
+ {/each} +
-
+ {/if}
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 21bf7813..188e2cc5 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -7,6 +7,7 @@ import { navbarDisplay } from '$lib/stores/navbarDisplay'; import { homepageLayout } from '$lib/stores/homepageLayout'; import { DISABLE_SETTINGS } from '$lib/config/customizable-settings'; + import { t } from '$lib/stores/language'; onMount(() => { // Initialize stores @@ -19,8 +20,8 @@
-

Settings

-

Customize your experience with themes, layouts, and accessibility options.

+

{$t('settings.title')}

+

{$t('settings.description')}

{#if DISABLE_SETTINGS} @@ -28,8 +29,8 @@
-

Settings Disabled

-

Settings for this instance have been disabled by your administrator.

+

{$t('settings.disabled.title')}

+

{$t('settings.disabled.message')}

{:else} diff --git a/src/routes/subnetting/supernet-calculator/+page.svelte b/src/routes/subnetting/supernet-calculator/+page.svelte index a3dad746..8d0862b9 100644 --- a/src/routes/subnetting/supernet-calculator/+page.svelte +++ b/src/routes/subnetting/supernet-calculator/+page.svelte @@ -1,5 +1,6 @@