@jjlmoya/utils-developer 1.14.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/category/i18n/fr.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/index.ts +2 -0
- package/src/layouts/PreviewLayout.astro +1 -0
- package/src/pages/[locale]/[slug].astro +29 -12
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +113 -0
- package/src/tests/locale_completeness.test.ts +2 -3
- package/src/tests/no_en_dash.test.ts +73 -0
- package/src/tests/pagespeed_best_practices.test.ts +198 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/aspectRatio/component.astro +3 -1
- package/src/tool/aspectRatio/i18n/de.ts +1 -1
- package/src/tool/aspectRatio/i18n/fr.ts +1 -1
- package/src/tool/aspectRatio/i18n/pl.ts +2 -2
- package/src/tool/aspectRatio/i18n/ru.ts +4 -4
- package/src/tool/aspectRatio/i18n/zh.ts +1 -1
- package/src/tool/aspectRatio/seo.astro +2 -1
- package/src/tool/calculadoraTiempoDatos/i18n/de.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +5 -5
- package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +5 -5
- package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/seo.astro +2 -1
- package/src/tool/colorConverter/component.astro +3 -3
- package/src/tool/colorConverter/i18n/de.ts +1 -1
- package/src/tool/colorConverter/i18n/ru.ts +2 -2
- package/src/tool/colorConverter/seo.astro +2 -1
- package/src/tool/conversorExcelCsvHtml/i18n/es.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +2 -2
- package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +2 -2
- package/src/tool/conversorExcelCsvHtml/seo.astro +2 -1
- package/src/tool/cronGenerator/i18n/de.ts +4 -4
- package/src/tool/cronGenerator/i18n/en.ts +1 -1
- package/src/tool/cronGenerator/i18n/es.ts +2 -2
- package/src/tool/cronGenerator/i18n/fr.ts +5 -5
- package/src/tool/cronGenerator/i18n/id.ts +1 -1
- package/src/tool/cronGenerator/i18n/nl.ts +1 -1
- package/src/tool/cronGenerator/i18n/pl.ts +8 -8
- package/src/tool/cronGenerator/i18n/pt.ts +1 -1
- package/src/tool/cronGenerator/i18n/ru.ts +9 -9
- package/src/tool/cronGenerator/i18n/sv.ts +1 -1
- package/src/tool/cronGenerator/i18n/zh.ts +3 -3
- package/src/tool/cronGenerator/seo.astro +2 -1
- package/src/tool/cssSpecificityCalculator/i18n/de.ts +1 -1
- package/src/tool/cssSpecificityCalculator/i18n/fr.ts +3 -3
- package/src/tool/cssSpecificityCalculator/i18n/pl.ts +2 -2
- package/src/tool/cssSpecificityCalculator/i18n/ru.ts +2 -2
- package/src/tool/cssSpecificityCalculator/seo.astro +2 -1
- package/src/tool/cssToInlineConverter/i18n/de.ts +6 -6
- package/src/tool/cssToInlineConverter/i18n/es.ts +1 -1
- package/src/tool/cssToInlineConverter/i18n/fr.ts +3 -3
- package/src/tool/cssToInlineConverter/seo.astro +2 -1
- package/src/tool/duplicateCssRemover/i18n/de.ts +7 -7
- package/src/tool/duplicateCssRemover/i18n/fr.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/id.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/nl.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/pl.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/pt.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/ru.ts +3 -3
- package/src/tool/duplicateCssRemover/i18n/sv.ts +1 -1
- package/src/tool/duplicateCssRemover/seo.astro +2 -1
- package/src/tool/generadorSecurityTxt/i18n/de.ts +5 -5
- package/src/tool/generadorSecurityTxt/i18n/ru.ts +2 -2
- package/src/tool/generadorSecurityTxt/seo.astro +2 -1
- package/src/tool/hashGenerator/i18n/ru.ts +5 -5
- package/src/tool/hashGenerator/i18n/zh.ts +2 -2
- package/src/tool/hashGenerator/seo.astro +2 -1
- package/src/tool/inspectorCertificadosSsl/i18n/de.ts +6 -6
- package/src/tool/inspectorCertificadosSsl/i18n/ru.ts +5 -5
- package/src/tool/inspectorCertificadosSsl/seo.astro +2 -1
- package/src/tool/jsonFormatter/i18n/de.ts +4 -4
- package/src/tool/jsonFormatter/i18n/en.ts +4 -4
- package/src/tool/jsonFormatter/i18n/es.ts +4 -4
- package/src/tool/jsonFormatter/i18n/fr.ts +4 -4
- package/src/tool/jsonFormatter/i18n/id.ts +4 -4
- package/src/tool/jsonFormatter/i18n/it.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ja.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ko.ts +4 -4
- package/src/tool/jsonFormatter/i18n/nl.ts +4 -4
- package/src/tool/jsonFormatter/i18n/pl.ts +4 -4
- package/src/tool/jsonFormatter/i18n/pt.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ru.ts +5 -5
- package/src/tool/jsonFormatter/i18n/sv.ts +4 -4
- package/src/tool/jsonFormatter/i18n/tr.ts +4 -4
- package/src/tool/jsonFormatter/i18n/zh.ts +5 -5
- package/src/tool/jsonFormatter/seo.astro +2 -1
- package/src/tool/keycode/i18n/de.ts +7 -7
- package/src/tool/keycode/i18n/en.ts +5 -5
- package/src/tool/keycode/i18n/es.ts +5 -5
- package/src/tool/keycode/i18n/fr.ts +7 -7
- package/src/tool/keycode/i18n/id.ts +5 -5
- package/src/tool/keycode/i18n/it.ts +5 -5
- package/src/tool/keycode/i18n/ja.ts +5 -5
- package/src/tool/keycode/i18n/ko.ts +5 -5
- package/src/tool/keycode/i18n/nl.ts +5 -5
- package/src/tool/keycode/i18n/pl.ts +7 -7
- package/src/tool/keycode/i18n/pt.ts +5 -5
- package/src/tool/keycode/i18n/ru.ts +6 -6
- package/src/tool/keycode/i18n/sv.ts +5 -5
- package/src/tool/keycode/i18n/tr.ts +5 -5
- package/src/tool/keycode/i18n/zh.ts +5 -5
- package/src/tool/keycode/seo.astro +2 -1
- package/src/tool/llmCostCalculator/component.astro +1 -2
- package/src/tool/llmCostCalculator/i18n/de.ts +4 -4
- package/src/tool/llmCostCalculator/i18n/en.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/fr.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/id.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/pt.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/ru.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/sv.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/tr.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/zh.ts +3 -3
- package/src/tool/llmCostCalculator/seo.astro +2 -1
- package/src/tool/mobileMockupGenerator/component.astro +4 -5
- package/src/tool/mobileMockupGenerator/i18n/es.ts +1 -1
- package/src/tool/mobileMockupGenerator/seo.astro +2 -1
- package/src/tool/musicalTypography/i18n/de.ts +1 -1
- package/src/tool/musicalTypography/i18n/fr.ts +1 -1
- package/src/tool/musicalTypography/i18n/id.ts +1 -1
- package/src/tool/musicalTypography/i18n/it.ts +1 -1
- package/src/tool/musicalTypography/i18n/ko.ts +1 -1
- package/src/tool/musicalTypography/i18n/ru.ts +3 -3
- package/src/tool/musicalTypography/seo.astro +2 -1
- package/src/tool/placeholderGenerator/component.astro +2 -2
- package/src/tool/placeholderGenerator/i18n/de.ts +4 -4
- package/src/tool/placeholderGenerator/i18n/ru.ts +1 -1
- package/src/tool/placeholderGenerator/seo.astro +2 -1
- package/src/tool/promptLibrary/i18n/de.ts +1 -1
- package/src/tool/promptLibrary/seo.astro +2 -1
- package/src/tool/readabilityCalculator/component.astro +4 -4
- package/src/tool/readabilityCalculator/i18n/de.ts +2 -2
- package/src/tool/readabilityCalculator/seo.astro +2 -1
- package/src/tool/readabilityCalculator/visual-readability-calculator-wcag-apca.css +87 -3
- package/src/tool/serpPixelSimulator/bibliography.astro +7 -0
- package/src/tool/serpPixelSimulator/bibliography.ts +16 -0
- package/src/tool/serpPixelSimulator/component.astro +84 -0
- package/src/tool/serpPixelSimulator/controller.ts +323 -0
- package/src/tool/serpPixelSimulator/entry.ts +30 -0
- package/src/tool/serpPixelSimulator/i18n/de.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/en.ts +213 -0
- package/src/tool/serpPixelSimulator/i18n/es.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/fr.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/id.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/it.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/ja.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/ko.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/nl.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/pl.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/pt.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/ru.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/sv.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/tr.ts +214 -0
- package/src/tool/serpPixelSimulator/i18n/zh.ts +214 -0
- package/src/tool/serpPixelSimulator/index.ts +12 -0
- package/src/tool/serpPixelSimulator/logic.ts +18 -0
- package/src/tool/serpPixelSimulator/seo.astro +12 -0
- package/src/tool/serpPixelSimulator/serp-pixel-simulator.css +335 -0
- package/src/tool/serpPixelSimulator/ui.ts +33 -0
- package/src/tool/svgSanitizer/i18n/de.ts +2 -2
- package/src/tool/svgSanitizer/i18n/id.ts +1 -1
- package/src/tool/svgSanitizer/i18n/pl.ts +1 -1
- package/src/tool/svgSanitizer/i18n/ru.ts +7 -7
- package/src/tool/svgSanitizer/i18n/sv.ts +3 -3
- package/src/tool/svgSanitizer/i18n/tr.ts +3 -3
- package/src/tool/svgSanitizer/i18n/zh.ts +3 -3
- package/src/tool/svgSanitizer/seo.astro +2 -1
- package/src/tool/svgToCss/i18n/de.ts +2 -2
- package/src/tool/svgToCss/i18n/es.ts +1 -1
- package/src/tool/svgToCss/i18n/fr.ts +1 -1
- package/src/tool/svgToCss/i18n/pl.ts +1 -1
- package/src/tool/svgToCss/i18n/ru.ts +2 -2
- package/src/tool/svgToCss/i18n/zh.ts +2 -2
- package/src/tool/svgToCss/seo.astro +2 -1
- package/src/tool/urlCleaner/component.astro +23 -5
- package/src/tool/urlCleaner/i18n/de.ts +3 -3
- package/src/tool/urlCleaner/i18n/es.ts +1 -1
- package/src/tool/urlCleaner/i18n/id.ts +1 -1
- package/src/tool/urlCleaner/i18n/nl.ts +1 -1
- package/src/tool/urlCleaner/seo.astro +2 -1
- package/src/tool/urlCleaner/url-tracking-cleaner.css +92 -2
- package/src/tool/urlEncoderDecoder/i18n/de.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/fr.ts +2 -2
- package/src/tool/urlEncoderDecoder/i18n/pl.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/ru.ts +8 -8
- package/src/tool/urlEncoderDecoder/i18n/sv.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/zh.ts +5 -5
- package/src/tool/urlEncoderDecoder/seo.astro +2 -1
- package/src/tool/utmGenerator/i18n/de.ts +2 -2
- package/src/tool/utmGenerator/i18n/pl.ts +5 -5
- package/src/tool/utmGenerator/i18n/ru.ts +6 -6
- package/src/tool/utmGenerator/seo.astro +2 -1
- package/src/tools.ts +2 -2
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type ScriptLocale = keyof typeof SCRIPT_RULES;
|
|
5
|
+
|
|
6
|
+
const SCRIPT_RULES = {
|
|
7
|
+
ja: {
|
|
8
|
+
language: 'Japanese',
|
|
9
|
+
scriptName: 'kana/kanji',
|
|
10
|
+
scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
|
|
11
|
+
minScriptRatio: 0.45,
|
|
12
|
+
},
|
|
13
|
+
ko: {
|
|
14
|
+
language: 'Korean',
|
|
15
|
+
scriptName: 'hangul',
|
|
16
|
+
scriptCharacters: /\p{Script=Hangul}/gu,
|
|
17
|
+
minScriptRatio: 0.55,
|
|
18
|
+
},
|
|
19
|
+
ru: {
|
|
20
|
+
language: 'Russian',
|
|
21
|
+
scriptName: 'cyrillic',
|
|
22
|
+
scriptCharacters: /\p{Script=Cyrillic}/gu,
|
|
23
|
+
minScriptRatio: 0.65,
|
|
24
|
+
},
|
|
25
|
+
zh: {
|
|
26
|
+
language: 'Chinese',
|
|
27
|
+
scriptName: 'han',
|
|
28
|
+
scriptCharacters: /\p{Script=Han}/gu,
|
|
29
|
+
minScriptRatio: 0.45,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const LETTERS = /\p{L}/gu;
|
|
34
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
35
|
+
|
|
36
|
+
function collectStrings(value: unknown): string[] {
|
|
37
|
+
if (typeof value === 'string') return [value];
|
|
38
|
+
if (!value || typeof value !== 'object') return [];
|
|
39
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
40
|
+
return Object.values(value).flatMap(collectStrings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeText(value: unknown): string {
|
|
44
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
48
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function letterCount(text: string): number {
|
|
52
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scriptCount(text: string, locale: ScriptLocale): number {
|
|
56
|
+
return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptRatio(text: string, locale: ScriptLocale): number {
|
|
60
|
+
const letters = letterCount(text);
|
|
61
|
+
if (letters === 0) return 0;
|
|
62
|
+
return scriptCount(text, locale) / letters;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Native script density validation', () => {
|
|
66
|
+
ALL_TOOLS.forEach((tool) => {
|
|
67
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
68
|
+
Object.keys(SCRIPT_RULES).forEach((locale) => {
|
|
69
|
+
it(`${locale} keeps most translated text in its native script`, async () => {
|
|
70
|
+
const typedLocale = locale as ScriptLocale;
|
|
71
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
72
|
+
if (!loader) return;
|
|
73
|
+
|
|
74
|
+
const content = await loader();
|
|
75
|
+
const rule = SCRIPT_RULES[typedLocale];
|
|
76
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
77
|
+
const letters = letterCount(text);
|
|
78
|
+
const matches = scriptCount(text, typedLocale);
|
|
79
|
+
const ratio = scriptRatio(text, typedLocale);
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
ratio,
|
|
83
|
+
[
|
|
84
|
+
`Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
85
|
+
`The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
|
|
86
|
+
`Most translatable content should be written in ${rule.scriptName} script.`,
|
|
87
|
+
'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
).toBeGreaterThanOrEqual(rule.minScriptRatio);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ToolExportValidationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function validateComponentType(
|
|
9
|
+
toolId: string,
|
|
10
|
+
componentName: string,
|
|
11
|
+
component: unknown,
|
|
12
|
+
failures: string[],
|
|
13
|
+
): void {
|
|
14
|
+
if (typeof component !== 'function') {
|
|
15
|
+
failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function validateComponentExecution(
|
|
20
|
+
toolId: string,
|
|
21
|
+
componentName: string,
|
|
22
|
+
fn: () => Promise<unknown>,
|
|
23
|
+
failures: string[],
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const result = await fn();
|
|
27
|
+
if (!result || typeof result !== 'object') {
|
|
28
|
+
failures.push(`${toolId}: ${componentName} import returned invalid result`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
|
|
36
|
+
const failures: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
|
|
40
|
+
validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
|
|
41
|
+
validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
|
|
42
|
+
|
|
43
|
+
const componentFn = tool.Component as () => Promise<unknown>;
|
|
44
|
+
const seoFn = tool.SEOComponent as () => Promise<unknown>;
|
|
45
|
+
const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
|
|
46
|
+
|
|
47
|
+
await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
|
|
48
|
+
await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
|
|
49
|
+
await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
passed: failures.length === 0,
|
|
54
|
+
failures,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { validateToolExports } from './shared-test-helpers';
|
|
4
|
+
|
|
5
|
+
describe('Tool Exports Pattern Validation', () => {
|
|
6
|
+
describe('Component Exports Format', () => {
|
|
7
|
+
ALL_TOOLS.forEach((tool) => {
|
|
8
|
+
it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
|
|
9
|
+
expect(typeof tool.Component).toBe('function');
|
|
10
|
+
expect(tool.Component).toBeInstanceOf(Function);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
|
|
14
|
+
expect(typeof tool.SEOComponent).toBe('function');
|
|
15
|
+
expect(tool.SEOComponent).toBeInstanceOf(Function);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
|
|
19
|
+
expect(typeof tool.BibliographyComponent).toBe('function');
|
|
20
|
+
expect(tool.BibliographyComponent).toBeInstanceOf(Function);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Dynamic Import Validation', () => {
|
|
26
|
+
it('all tools must have functional dynamic imports', async () => {
|
|
27
|
+
const result = await validateToolExports(ALL_TOOLS);
|
|
28
|
+
if (!result.passed) {
|
|
29
|
+
throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
|
|
30
|
+
}
|
|
31
|
+
expect(result.passed).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -4,8 +4,8 @@ import { developerCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have 25 tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(25);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('developerCategory should be defined', () => {
|
|
@@ -137,12 +137,14 @@ const t = (ui ?? {}) as AspectRatioUI;
|
|
|
137
137
|
const { rw, rh, cw, ch } = getInputValues();
|
|
138
138
|
const divisor = calculateGcd(Math.round(rw * 100), Math.round(rh * 100)) / 100;
|
|
139
139
|
const ratioText = `${Math.round(rw / divisor)}:${Math.round(rh / divisor)}`;
|
|
140
|
+
const viewWidth = viewport.clientWidth - 80;
|
|
141
|
+
const viewHeight = viewport.clientHeight - 80;
|
|
142
|
+
const { w, h } = computeBoxDimensions(rw, rh, viewWidth, viewHeight);
|
|
140
143
|
|
|
141
144
|
arcBoxRatio.textContent = ratioText;
|
|
142
145
|
arcBoxRes.innerHTML = `${cw} × ${ch}px`;
|
|
143
146
|
arcBadge.textContent = ratioText;
|
|
144
147
|
|
|
145
|
-
const { w, h } = computeBoxDimensions(rw, rh, viewport.clientWidth - 80, viewport.clientHeight - 80);
|
|
146
148
|
arcBox.style.width = w + 'px';
|
|
147
149
|
arcBox.style.height = h + 'px';
|
|
148
150
|
};
|
|
@@ -137,7 +137,7 @@ export const content: ToolLocaleContent<AspectRatioUI> = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'paragraph',
|
|
140
|
-
html: 'Falsch gehandhabte Seitenverhältnisse sind eine häufige Ursache für gestauchte Fotos, Videos mit unerwartetem Beschnitt (Letterboxing) oder Webkomponenten, deren Layout beim Betrachten auf unterschiedlichen Geräten
|
|
140
|
+
html: 'Falsch gehandhabte Seitenverhältnisse sind eine häufige Ursache für gestauchte Fotos, Videos mit unerwartetem Beschnitt (Letterboxing) oder Webkomponenten, deren Layout beim Betrachten auf unterschiedlichen Geräten - vom Smartphone bis hin zu Ultra-Wide-Monitoren - fehlerhaft dargestellt wird.',
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
type: 'title',
|
|
@@ -21,7 +21,7 @@ const faqData = [
|
|
|
21
21
|
{
|
|
22
22
|
question: "Comment calculer la hauteur à partir de la largeur avec un ratio donné ?",
|
|
23
23
|
answer:
|
|
24
|
-
"La formule est
|
|
24
|
+
"La formule est: Hauteur = Largeur × (Ratio Hauteur / Ratio Largeur). Par exemple, pour une largeur de 1280px avec un ratio 16:9, la hauteur serait: 1280 × (9/16) = 720px.",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
question: "Quel est le Ratio de Format standard pour les vidéos YouTube ?",
|
|
@@ -42,7 +42,7 @@ const howToData = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'Dostosuj skalę',
|
|
45
|
-
text: 'Zmień wartość szerokości lub wysokości w sekcji
|
|
45
|
+
text: 'Zmień wartość szerokości lub wysokości w sekcji "Rzeczywista skala". Narzędzie automatycznie obliczy drugą wartość, aby zachować proporcje.',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: 'Sprawdź podgląd',
|
|
@@ -137,7 +137,7 @@ export const content: ToolLocaleContent<AspectRatioUI> = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'paragraph',
|
|
140
|
-
html: 'Błędne zarządzanie proporcjami jest częstą przyczyną spłaszczonych zdjęć, filmów z nieoczekiwanym przycięciem (letterboxing) lub komponentów stron internetowych, które tracą swój układ przy przeglądaniu na różnych urządzeniach
|
|
140
|
+
html: 'Błędne zarządzanie proporcjami jest częstą przyczyną spłaszczonych zdjęć, filmów z nieoczekiwanym przycięciem (letterboxing) lub komponentów stron internetowych, które tracą swój układ przy przeglądaniu na różnych urządzeniach - od telefonów po monitory panoramiczne.',
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
type: 'title',
|
|
@@ -16,7 +16,7 @@ const faqData = [
|
|
|
16
16
|
{
|
|
17
17
|
question: 'Почему важно соблюдать правильные пропорции?',
|
|
18
18
|
answer:
|
|
19
|
-
'Игнорирование соотношения сторон приводит к сплющенным или растянутым изображениям, появлению нежелательных черных полос (letterboxing) в видео и нарушению макета веб-компонентов на экранах разного размера. Соблюдение правильных пропорций
|
|
19
|
+
'Игнорирование соотношения сторон приводит к сплющенным или растянутым изображениям, появлению нежелательных черных полос (letterboxing) в видео и нарушению макета веб-компонентов на экранах разного размера. Соблюдение правильных пропорций - залог профессионального вида.',
|
|
20
20
|
},
|
|
21
21
|
{
|
|
22
22
|
question: 'Как рассчитать высоту на основе ширины при заданном соотношении?',
|
|
@@ -26,7 +26,7 @@ const faqData = [
|
|
|
26
26
|
{
|
|
27
27
|
question: 'Какое стандартное соотношение сторон используется для видео на YouTube?',
|
|
28
28
|
answer:
|
|
29
|
-
'16:9
|
|
29
|
+
'16:9 - это стандартное соотношение для YouTube и большинства современных видеоплатформ. Оно соответствует таким разрешениям, как 1280×720 (HD), 1920×1080 (Full HD) или 3840×2160 (4K UHD).',
|
|
30
30
|
},
|
|
31
31
|
{
|
|
32
32
|
question: 'Какое соотношение сторон используется для вертикальных видео в соцсетях?',
|
|
@@ -42,7 +42,7 @@ const howToData = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'Настройте масштаб',
|
|
45
|
-
text: 'Измените значение ширины или высоты в разделе
|
|
45
|
+
text: 'Измените значение ширины или высоты в разделе "Реальный масштаб". Инструмент автоматически рассчитает противоположное значение для сохранения пропорций.',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: 'Проверьте результат',
|
|
@@ -137,7 +137,7 @@ export const content: ToolLocaleContent<AspectRatioUI> = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'paragraph',
|
|
140
|
-
html: 'Несоблюдение соотношения сторон
|
|
140
|
+
html: 'Несоблюдение соотношения сторон - частая причина сплющенных фотографий, видео с неожиданной обрезкой (letterboxing) или веб-компонентов, макет которых нарушается при просмотре на разных устройствах - от мобильных телефонов до ультрашироких мониторов.',
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
type: 'title',
|
|
@@ -9,6 +9,7 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
const { locale = 'es' } = Astro.props;
|
|
11
11
|
const content = await aspectRatio.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -36,7 +36,7 @@ const faqData = [
|
|
|
36
36
|
const howToData = [
|
|
37
37
|
{ name: 'Website-Gewicht eingeben', text: 'Verwenden Sie Browser-Entwicklertools oder WebPageTest, um das Gewicht Ihrer Seite zu ermitteln. Geben Sie diesen Wert in MB ein.' },
|
|
38
38
|
{ name: 'Ladezeiten beobachten', text: 'Der Rechner zeigt an, wie viele Sekunden Ihre Website zum Laden bei 3G, 4G und 5G benötigt. Die realen Zeiten sind meist höher.' },
|
|
39
|
-
{ name: 'Lebenszeitauswirkung verstehen', text: 'Der Prozentsatz der
|
|
39
|
+
{ name: 'Lebenszeitauswirkung verstehen', text: 'Der Prozentsatz der "Lebenszeit" zeigt, wie viel Zeit jemand mit Warten verbringt. Nutzen Sie dies als Motivation zur Optimierung.' },
|
|
40
40
|
{ name: 'Optimieren und neu berechnen', text: 'Messen Sie nach der Optimierung erneut und berechnen Sie neu. Sehen Sie, wie kleine Verbesserungen große Auswirkungen haben.' },
|
|
41
41
|
];
|
|
42
42
|
|
|
@@ -36,7 +36,7 @@ const faqData = [
|
|
|
36
36
|
const howToData = [
|
|
37
37
|
{ name: 'Wpisz wagę swojej strony', text: 'Użyj narzędzi deweloperskich przeglądarki lub WebPageTest, aby sprawdzić wagę strony. Wpisz tę wartość w MB.' },
|
|
38
38
|
{ name: 'Obserwuj czasy ładowania', text: 'Kalkulator pokazuje, ile sekund zajmuje ładowanie strony w sieciach 3G, 4G i 5G. Czasy w rzeczywistości są zazwyczaj wyższe.' },
|
|
39
|
-
{ name: 'Zrozum wpływ na czas życia', text: 'Procent
|
|
39
|
+
{ name: 'Zrozum wpływ na czas życia', text: 'Procent "czasu życia" pokazuje, ile czyjegoś życia mija na czekaniu. Użyj tego jako motywacji do optymalizacji.' },
|
|
40
40
|
{ name: 'Optymalizuj i oblicz ponownie', text: 'Po optymalizacji zmierz ponownie i wykonaj obliczenia. Zobacz, jak małe ulepszenia mają duży wpływ.' },
|
|
41
41
|
];
|
|
42
42
|
|
|
@@ -36,7 +36,7 @@ const faqData = [
|
|
|
36
36
|
const howToData = [
|
|
37
37
|
{ name: 'Введите вес вашего сайта', text: 'Используйте инструменты разработчика браузера или WebPageTest, чтобы узнать вес вашей страницы. Введите это значение в МБ.' },
|
|
38
38
|
{ name: 'Наблюдайте за временем загрузки', text: 'Калькулятор показывает, сколько секунд требуется вашему сайту для загрузки на 3G, 4G и 5G. Реальное время обычно выше.' },
|
|
39
|
-
{ name: 'Поймите влияние на продолжительность жизни', text: 'Процент
|
|
39
|
+
{ name: 'Поймите влияние на продолжительность жизни', text: 'Процент "времени жизни" показывает, сколько времени чьей-то жизни тратится на ожидание. Используйте это как мотивацию для оптимизации.' },
|
|
40
40
|
{ name: 'Оптимизируйте и пересчитайте', text: 'После оптимизации измерьте показатели снова и сделайте перерасчет. Посмотрите, как небольшие улучшения приносят значительный результат.' },
|
|
41
41
|
];
|
|
42
42
|
|
|
@@ -114,7 +114,7 @@ export const content: ToolLocaleContent<CalculadoraTiempoDatosUI> = {
|
|
|
114
114
|
{ type: 'title', text: 'Почему скорость загрузки сайта критически важна', level: 2 },
|
|
115
115
|
{
|
|
116
116
|
type: 'paragraph',
|
|
117
|
-
html: 'В сегодняшнюю цифровую эпоху скорость загрузки сайта
|
|
117
|
+
html: 'В сегодняшнюю цифровую эпоху скорость загрузки сайта - это не роскошь, а необходимость. Каждая миллисекунда имеет значение для удержания пользователей, улучшения рейтинга в поиске и максимизации конверсии. Современные пользователи ожидают, что страницы загрузятся менее чем за 3 секунды.',
|
|
118
118
|
},
|
|
119
119
|
{ type: 'title', text: 'Влияние на пользовательский опыт', level: 3 },
|
|
120
120
|
{
|
|
@@ -125,9 +125,9 @@ export const content: ToolLocaleContent<CalculadoraTiempoDatosUI> = {
|
|
|
125
125
|
{
|
|
126
126
|
type: 'list',
|
|
127
127
|
items: [
|
|
128
|
-
'<strong>3G:</strong> 0,4 Мбит/с
|
|
129
|
-
'<strong>4G/LTE:</strong> 10 Мбит/с
|
|
130
|
-
'<strong>5G:</strong> 100+ Мбит/с
|
|
128
|
+
'<strong>3G:</strong> 0,4 Мбит/с - часто встречается в сельской местности и развивающихся странах',
|
|
129
|
+
'<strong>4G/LTE:</strong> 10 Мбит/с - стандарт в развитых странах',
|
|
130
|
+
'<strong>5G:</strong> 100+ Мбит/с - постепенно расширяется, но все еще ограничен',
|
|
131
131
|
],
|
|
132
132
|
},
|
|
133
133
|
{ type: 'title', text: 'Стратегии по уменьшению веса сайта', level: 3 },
|
|
@@ -21,11 +21,11 @@ const faqData = [
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
question: 'Hur mycket bör min webbplats väga?',
|
|
24
|
-
answer: 'Google rekommenderar att startsidan laddas på mindre än 3 sekunder via en typisk 4G-anslutning. För detta bör en webbplats helst väga mellan 1 och 2 MB. Det globala genomsnittet ligger dock nära 2
|
|
24
|
+
answer: 'Google rekommenderar att startsidan laddas på mindre än 3 sekunder via en typisk 4G-anslutning. För detta bör en webbplats helst väga mellan 1 och 2 MB. Det globala genomsnittet ligger dock nära 2-3 MB.',
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
question: 'Hur kan jag minska min webbplats vikt?',
|
|
28
|
-
answer: 'Huvudstrategier: optimera bilder (50
|
|
28
|
+
answer: 'Huvudstrategier: optimera bilder (50-80 % av vikten), minifiera CSS och JavaScript, använda lazy loading, implementera webbläsarcache och använda en CDN. Bildoptimering är vanligtvis den mest effektiva faktorn.',
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
31
|
question: 'Påverkar laddningshastigheten Google-rankningen?',
|
|
@@ -36,7 +36,7 @@ const faqData = [
|
|
|
36
36
|
const howToData = [
|
|
37
37
|
{ name: 'Ange din webbplats vikt', text: 'Använd webbläsarens utvecklarverktyg eller WebPageTest för att hitta din sidas vikt. Ange det värdet i MB.' },
|
|
38
38
|
{ name: 'Observera laddningstider', text: 'Kalkylatorn visar hur många sekunder det tar för din webbplats att ladda via 3G, 4G och 5G. Tiderna i verkligheten är vanligtvis högre.' },
|
|
39
|
-
{ name: 'Förstå livstidspåverkan', text: 'Procentandelen
|
|
39
|
+
{ name: 'Förstå livstidspåverkan', text: 'Procentandelen "livstid" visar hur mycket av någons liv som spenderas på att vänta. Använd detta som motivation för att optimera.' },
|
|
40
40
|
{ name: 'Optimera och beräkna om', text: 'Mät igen och beräkna om efter optimeringen. Se hur små förbättringar har stor betydelse.' },
|
|
41
41
|
];
|
|
42
42
|
|
|
@@ -134,8 +134,8 @@ export const content: ToolLocaleContent<CalculadoraTiempoDatosUI> = {
|
|
|
134
134
|
{
|
|
135
135
|
type: 'list',
|
|
136
136
|
items: [
|
|
137
|
-
'<strong>Bildoptimering:</strong> Representerar 50
|
|
138
|
-
'<strong>Minifiering:</strong> Ta bort onödig kod från CSS och JavaScript. Spara 30
|
|
137
|
+
'<strong>Bildoptimering:</strong> Representerar 50-80 % av vikten. Minska med 40-60 % med verktyg som TinyPNG.',
|
|
138
|
+
'<strong>Minifiering:</strong> Ta bort onödig kod från CSS och JavaScript. Spara 30-50 %.',
|
|
139
139
|
'<strong>Lazy Loading:</strong> Ladda bilder först när användaren scrollar till dem.',
|
|
140
140
|
'<strong>Webbläsarcache:</strong> Cachelagra statiska filer i användarnas webbläsare.',
|
|
141
141
|
'<strong>CDN:</strong> Leverera innehåll från servrar som ligger geografiskt nära.',
|
|
@@ -36,7 +36,7 @@ const faqData = [
|
|
|
36
36
|
const howToData = [
|
|
37
37
|
{ name: '输入您的网站权重', text: '使用浏览器开发者工具或 WebPageTest 获取页面权重。以 MB 为单位输入该值。' },
|
|
38
38
|
{ name: '观察加载时间', text: '计算器显示网站在 3G、4G 和 5G 下加载所需的秒数。现实中的时间通常更高。' },
|
|
39
|
-
{ name: '了解对寿命的影响', text: '
|
|
39
|
+
{ name: '了解对寿命的影响', text: '"寿命"百分比显示了一个人有多少生命花在了等待上。以此作为优化的动力。' },
|
|
40
40
|
{ name: '优化并重新计算', text: '优化后,再次测量并重新计算。观察微小的改进如何产生重大影响。' },
|
|
41
41
|
];
|
|
42
42
|
|
|
@@ -9,6 +9,7 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
const { locale = 'es' } = Astro.props;
|
|
11
11
|
const content = await calculadoraTiempoDatos.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -53,21 +53,21 @@ const t = (ui ?? {}) as ColorConverterUI;
|
|
|
53
53
|
<span>{t.labelR}</span>
|
|
54
54
|
<span id="cc-r-val">255</span>
|
|
55
55
|
</div>
|
|
56
|
-
<input type="range" id="cc-r-slider" class="cc-range" min="0" max="255" value="255" />
|
|
56
|
+
<input type="range" id="cc-r-slider" class="cc-range" min="0" max="255" value="255" aria-label={t.labelR} />
|
|
57
57
|
</div>
|
|
58
58
|
<div class="cc-slider-group">
|
|
59
59
|
<div class="cc-slider-header">
|
|
60
60
|
<span>{t.labelG}</span>
|
|
61
61
|
<span id="cc-g-val">255</span>
|
|
62
62
|
</div>
|
|
63
|
-
<input type="range" id="cc-g-slider" class="cc-range" min="0" max="255" value="255" />
|
|
63
|
+
<input type="range" id="cc-g-slider" class="cc-range" min="0" max="255" value="255" aria-label={t.labelG} />
|
|
64
64
|
</div>
|
|
65
65
|
<div class="cc-slider-group">
|
|
66
66
|
<div class="cc-slider-header">
|
|
67
67
|
<span>{t.labelB}</span>
|
|
68
68
|
<span id="cc-b-val">255</span>
|
|
69
69
|
</div>
|
|
70
|
-
<input type="range" id="cc-b-slider" class="cc-range" min="0" max="255" value="255" />
|
|
70
|
+
<input type="range" id="cc-b-slider" class="cc-range" min="0" max="255" value="255" aria-label={t.labelB} />
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
73
73
|
</div>
|
|
@@ -28,7 +28,7 @@ const faqData = [
|
|
|
28
28
|
const howToData = [
|
|
29
29
|
{ name: 'Farbe auswählen', text: 'Verwenden Sie die Schieberegler für Rot, Grün und Blau oder geben Sie einen HEX-Code direkt in das Textfeld ein.' },
|
|
30
30
|
{ name: 'RGB-Kanäle anpassen', text: 'Bewegen Sie die Schieberegler, um die Intensität jedes Kanals zu ändern und die Aktualisierung in Echtzeit zu sehen.' },
|
|
31
|
-
{ name: 'Format kopieren', text: 'Klicken Sie auf die Schaltfläche
|
|
31
|
+
{ name: 'Format kopieren', text: 'Klicken Sie auf die Schaltfläche "Kopieren" neben dem HEX-, RGB- oder HSL-Format, das Sie benötigen.' },
|
|
32
32
|
{ name: 'Harmonien erkunden', text: 'Klicken Sie auf die Harmoniefarben (komplementär, analog, triadisch), um sie in die Zwischenablage zu kopieren.' },
|
|
33
33
|
];
|
|
34
34
|
|
|
@@ -28,7 +28,7 @@ const faqData = [
|
|
|
28
28
|
const howToData = [
|
|
29
29
|
{ name: 'Выберите цвет', text: 'Используйте ползунки красного, зеленого и синего или введите HEX-код прямо в текстовое поле.' },
|
|
30
30
|
{ name: 'Настройте каналы RGB', text: 'Передвигайте ползунки, чтобы изменить интенсивность каждого канала и увидеть обновление в режиме реального времени.' },
|
|
31
|
-
{ name: 'Скопируйте формат', text: 'Нажмите кнопку
|
|
31
|
+
{ name: 'Скопируйте формат', text: 'Нажмите кнопку "Копировать" рядом с нужным форматом HEX, RGB или HSL.' },
|
|
32
32
|
{ name: 'Исследуйте гармонии', text: 'Нажмите на гармоничные цвета (комплементарные, аналогичные, триадические), чтобы скопировать их в буфер обмена.' },
|
|
33
33
|
];
|
|
34
34
|
|
|
@@ -97,7 +97,7 @@ export const content: ToolLocaleContent<ColorConverterUI> = {
|
|
|
97
97
|
{ type: 'title', text: 'Конвертер цветов RGB в HEX и HSL для разработчиков', level: 2 },
|
|
98
98
|
{
|
|
99
99
|
type: 'paragraph',
|
|
100
|
-
html: 'В мире <strong>frontend-разработки</strong> и <strong>UI/UX дизайна</strong> управление цветом
|
|
100
|
+
html: 'В мире <strong>frontend-разработки</strong> и <strong>UI/UX дизайна</strong> управление цветом - это постоянная задача. Наш <strong>онлайн-конвертер цветов</strong> позволяет мгновенно и с математической точностью преобразовывать значения между <strong>HEX, RGB и HSL</strong>.',
|
|
101
101
|
},
|
|
102
102
|
{ type: 'title', text: 'Форматы цветов: HEX, RGB и HSL', level: 3 },
|
|
103
103
|
{
|
|
@@ -9,6 +9,7 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
const { locale = 'es' } = Astro.props;
|
|
11
11
|
const content = await colorConverter.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -8,7 +8,7 @@ const description = 'Convierte tus datos de Excel o CSV a tablas HTML semántica
|
|
|
8
8
|
|
|
9
9
|
const faqData = [
|
|
10
10
|
{
|
|
11
|
-
question: '¿Cómo convierto un archivo Excel (
|
|
11
|
+
question: '¿Cómo convierto un archivo Excel (XLSX) a HTML?',
|
|
12
12
|
answer: 'Primero, abre tu archivo en Excel y guárdalo como CSV (delimitado por comas). Luego, sube ese archivo CSV a nuestra herramienta o pega su contenido para obtener el código HTML de la tabla.',
|
|
13
13
|
},
|
|
14
14
|
{
|
|
@@ -40,7 +40,7 @@ const howToData = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: 'Skopiuj kod',
|
|
43
|
-
text: 'Przełącz się na kartę
|
|
43
|
+
text: 'Przełącz się na kartę "Kod HTML" i skopiuj wynik, aby wkleić go na swojej stronie internetowej.',
|
|
44
44
|
},
|
|
45
45
|
];
|
|
46
46
|
|
|
@@ -13,7 +13,7 @@ const faqData = [
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
question: 'Включает ли сгенерированный код стили CSS?',
|
|
16
|
-
answer: 'Генератор создает чистый HTML с дополнительными классами для границ и
|
|
16
|
+
answer: 'Генератор создает чистый HTML с дополнительными классами для границ и "зебры" (чередующихся строк). Окончательные визуальные стили зависят от CSS вашего собственного сайта, что обеспечивает идеальную интеграцию.',
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
question: 'Могу ли я загружать очень большие CSV-файлы?',
|
|
@@ -40,7 +40,7 @@ const howToData = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: 'Скопируйте код',
|
|
43
|
-
text: 'Перейдите на вкладку
|
|
43
|
+
text: 'Перейдите на вкладку "Код HTML" и скопируйте результат для вставки на ваш сайт.',
|
|
44
44
|
},
|
|
45
45
|
];
|
|
46
46
|
|
|
@@ -21,7 +21,7 @@ const faqData = [
|
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
question: '它与 Google 表格兼容吗?',
|
|
24
|
-
answer: '绝对兼容。在 Google
|
|
24
|
+
answer: '绝对兼容。在 Google 表格中,转到"文件">"下载">"逗号分隔值 (.csv)",然后将该文件用于我们的工具。',
|
|
25
25
|
},
|
|
26
26
|
];
|
|
27
27
|
|
|
@@ -40,7 +40,7 @@ const howToData = [
|
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
name: '复制代码',
|
|
43
|
-
text: '
|
|
43
|
+
text: '切换到"HTML 代码"选项卡并复制结果以粘贴到您的网站上。',
|
|
44
44
|
},
|
|
45
45
|
];
|
|
46
46
|
|
|
@@ -9,6 +9,7 @@ interface Props {
|
|
|
9
9
|
|
|
10
10
|
const { locale = 'es' } = Astro.props;
|
|
11
11
|
const content = await conversorExcelCsvHtml.i18n[locale]?.();
|
|
12
|
+
if (!content) return null;
|
|
12
13
|
---
|
|
13
14
|
|
|
14
|
-
{content && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
15
|
+
{content.seo?.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|
|
@@ -21,7 +21,7 @@ const faqData = [
|
|
|
21
21
|
{
|
|
22
22
|
question: 'Was bedeutet das Sternchen (*) in Cron?',
|
|
23
23
|
answer:
|
|
24
|
-
'Das Sternchen ist ein Platzhalter, der
|
|
24
|
+
'Das Sternchen ist ein Platzhalter, der "alle" bedeutet. Beispielsweise bedeutet ein * im Minutenfeld, dass die Aufgabe jede Minute des definierten Bereichs ausgeführt wird.',
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
question: 'Wie kann ich die anstehenden Ausführungen überprüfen?',
|
|
@@ -37,7 +37,7 @@ const howToData = [
|
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
39
|
name: 'Erweiterte Details konfigurieren',
|
|
40
|
-
text: 'Wechseln Sie zur Registerkarte
|
|
40
|
+
text: 'Wechseln Sie zur Registerkarte "Erweitert", um die genauen Minuten, Stunden oder特定 Wochentage manuell zu definieren.',
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: 'Übersetzung und Daten überprüfen',
|
|
@@ -157,7 +157,7 @@ export const content: ToolLocaleContent<CronGeneratorUI> = {
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
type: 'paragraph',
|
|
160
|
-
html: 'Obwohl wir im Zeitalter von <em>Serverless</em>-Servern und Cloud-Workflows leben, bleibt das <strong>Crontab</strong>-System das Rückgrat der globalen Technologieinfrastruktur. Von kleinen Blogs bis hin zu massiven Bankinfrastrukturen
|
|
160
|
+
html: 'Obwohl wir im Zeitalter von <em>Serverless</em>-Servern und Cloud-Workflows leben, bleibt das <strong>Crontab</strong>-System das Rückgrat der globalen Technologieinfrastruktur. Von kleinen Blogs bis hin zu massiven Bankinfrastrukturen - die Aufgabenplanung sorgt dafür, dass die digitale Welt läuft.',
|
|
161
161
|
},
|
|
162
162
|
{
|
|
163
163
|
type: 'title',
|
|
@@ -185,7 +185,7 @@ export const content: ToolLocaleContent<CronGeneratorUI> = {
|
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
type: 'paragraph',
|
|
188
|
-
html: '90 % der Server laufen auf <strong>UTC</strong>. Wenn Sie eine Aufgabe für 02:00 Uhr morgens in Ihrer lokalen Zeitzone planen, wird sie möglicherweise zu einer unerwarteten Zeit ausgeführt. Außerdem hat Cron keinen Zugriff auf Ihren üblichen <code>$PATH</code>
|
|
188
|
+
html: '90 % der Server laufen auf <strong>UTC</strong>. Wenn Sie eine Aufgabe für 02:00 Uhr morgens in Ihrer lokalen Zeitzone planen, wird sie möglicherweise zu einer unerwarteten Zeit ausgeführt. Außerdem hat Cron keinen Zugriff auf Ihren üblichen <code>$PATH</code> - verwenden Sie immer absolute Pfade wie <code>/usr/local/bin/node</code>.',
|
|
189
189
|
},
|
|
190
190
|
],
|
|
191
191
|
};
|
|
@@ -185,7 +185,7 @@ export const content: ToolLocaleContent<CronGeneratorUI> = {
|
|
|
185
185
|
},
|
|
186
186
|
{
|
|
187
187
|
type: 'paragraph',
|
|
188
|
-
html: '90% of servers run on <strong>UTC</strong>. If you schedule a task at 02:00 AM thinking in your local timezone, it may run at an unexpected time. Also, Cron does not have access to your usual <code>$PATH</code>
|
|
188
|
+
html: '90% of servers run on <strong>UTC</strong>. If you schedule a task at 02:00 AM thinking in your local timezone, it may run at an unexpected time. Also, Cron does not have access to your usual <code>$PATH</code> - always use absolute paths like <code>/usr/local/bin/node</code>.',
|
|
189
189
|
},
|
|
190
190
|
],
|
|
191
191
|
};
|