@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.
Files changed (200) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/fr.ts +1 -1
  3. package/src/category/i18n/ru.ts +1 -1
  4. package/src/category/index.ts +2 -1
  5. package/src/entries.ts +4 -1
  6. package/src/index.ts +2 -0
  7. package/src/layouts/PreviewLayout.astro +1 -0
  8. package/src/pages/[locale]/[slug].astro +29 -12
  9. package/src/tests/diacritics_density.test.ts +118 -0
  10. package/src/tests/inverted_punctuation.test.ts +113 -0
  11. package/src/tests/locale_completeness.test.ts +2 -3
  12. package/src/tests/no_en_dash.test.ts +73 -0
  13. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  14. package/src/tests/script_density.test.ts +94 -0
  15. package/src/tests/shared-test-helpers.ts +56 -0
  16. package/src/tests/tool_exports.test.ts +34 -0
  17. package/src/tests/tool_validation.test.ts +2 -2
  18. package/src/tool/aspectRatio/component.astro +3 -1
  19. package/src/tool/aspectRatio/i18n/de.ts +1 -1
  20. package/src/tool/aspectRatio/i18n/fr.ts +1 -1
  21. package/src/tool/aspectRatio/i18n/pl.ts +2 -2
  22. package/src/tool/aspectRatio/i18n/ru.ts +4 -4
  23. package/src/tool/aspectRatio/i18n/zh.ts +1 -1
  24. package/src/tool/aspectRatio/seo.astro +2 -1
  25. package/src/tool/calculadoraTiempoDatos/i18n/de.ts +1 -1
  26. package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +1 -1
  27. package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +5 -5
  28. package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +5 -5
  29. package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +1 -1
  30. package/src/tool/calculadoraTiempoDatos/seo.astro +2 -1
  31. package/src/tool/colorConverter/component.astro +3 -3
  32. package/src/tool/colorConverter/i18n/de.ts +1 -1
  33. package/src/tool/colorConverter/i18n/ru.ts +2 -2
  34. package/src/tool/colorConverter/seo.astro +2 -1
  35. package/src/tool/conversorExcelCsvHtml/i18n/es.ts +1 -1
  36. package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +1 -1
  37. package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +2 -2
  38. package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +2 -2
  39. package/src/tool/conversorExcelCsvHtml/seo.astro +2 -1
  40. package/src/tool/cronGenerator/i18n/de.ts +4 -4
  41. package/src/tool/cronGenerator/i18n/en.ts +1 -1
  42. package/src/tool/cronGenerator/i18n/es.ts +2 -2
  43. package/src/tool/cronGenerator/i18n/fr.ts +5 -5
  44. package/src/tool/cronGenerator/i18n/id.ts +1 -1
  45. package/src/tool/cronGenerator/i18n/nl.ts +1 -1
  46. package/src/tool/cronGenerator/i18n/pl.ts +8 -8
  47. package/src/tool/cronGenerator/i18n/pt.ts +1 -1
  48. package/src/tool/cronGenerator/i18n/ru.ts +9 -9
  49. package/src/tool/cronGenerator/i18n/sv.ts +1 -1
  50. package/src/tool/cronGenerator/i18n/zh.ts +3 -3
  51. package/src/tool/cronGenerator/seo.astro +2 -1
  52. package/src/tool/cssSpecificityCalculator/i18n/de.ts +1 -1
  53. package/src/tool/cssSpecificityCalculator/i18n/fr.ts +3 -3
  54. package/src/tool/cssSpecificityCalculator/i18n/pl.ts +2 -2
  55. package/src/tool/cssSpecificityCalculator/i18n/ru.ts +2 -2
  56. package/src/tool/cssSpecificityCalculator/seo.astro +2 -1
  57. package/src/tool/cssToInlineConverter/i18n/de.ts +6 -6
  58. package/src/tool/cssToInlineConverter/i18n/es.ts +1 -1
  59. package/src/tool/cssToInlineConverter/i18n/fr.ts +3 -3
  60. package/src/tool/cssToInlineConverter/seo.astro +2 -1
  61. package/src/tool/duplicateCssRemover/i18n/de.ts +7 -7
  62. package/src/tool/duplicateCssRemover/i18n/fr.ts +1 -1
  63. package/src/tool/duplicateCssRemover/i18n/id.ts +1 -1
  64. package/src/tool/duplicateCssRemover/i18n/nl.ts +1 -1
  65. package/src/tool/duplicateCssRemover/i18n/pl.ts +1 -1
  66. package/src/tool/duplicateCssRemover/i18n/pt.ts +1 -1
  67. package/src/tool/duplicateCssRemover/i18n/ru.ts +3 -3
  68. package/src/tool/duplicateCssRemover/i18n/sv.ts +1 -1
  69. package/src/tool/duplicateCssRemover/seo.astro +2 -1
  70. package/src/tool/generadorSecurityTxt/i18n/de.ts +5 -5
  71. package/src/tool/generadorSecurityTxt/i18n/ru.ts +2 -2
  72. package/src/tool/generadorSecurityTxt/seo.astro +2 -1
  73. package/src/tool/hashGenerator/i18n/ru.ts +5 -5
  74. package/src/tool/hashGenerator/i18n/zh.ts +2 -2
  75. package/src/tool/hashGenerator/seo.astro +2 -1
  76. package/src/tool/inspectorCertificadosSsl/i18n/de.ts +6 -6
  77. package/src/tool/inspectorCertificadosSsl/i18n/ru.ts +5 -5
  78. package/src/tool/inspectorCertificadosSsl/seo.astro +2 -1
  79. package/src/tool/jsonFormatter/i18n/de.ts +4 -4
  80. package/src/tool/jsonFormatter/i18n/en.ts +4 -4
  81. package/src/tool/jsonFormatter/i18n/es.ts +4 -4
  82. package/src/tool/jsonFormatter/i18n/fr.ts +4 -4
  83. package/src/tool/jsonFormatter/i18n/id.ts +4 -4
  84. package/src/tool/jsonFormatter/i18n/it.ts +4 -4
  85. package/src/tool/jsonFormatter/i18n/ja.ts +4 -4
  86. package/src/tool/jsonFormatter/i18n/ko.ts +4 -4
  87. package/src/tool/jsonFormatter/i18n/nl.ts +4 -4
  88. package/src/tool/jsonFormatter/i18n/pl.ts +4 -4
  89. package/src/tool/jsonFormatter/i18n/pt.ts +4 -4
  90. package/src/tool/jsonFormatter/i18n/ru.ts +5 -5
  91. package/src/tool/jsonFormatter/i18n/sv.ts +4 -4
  92. package/src/tool/jsonFormatter/i18n/tr.ts +4 -4
  93. package/src/tool/jsonFormatter/i18n/zh.ts +5 -5
  94. package/src/tool/jsonFormatter/seo.astro +2 -1
  95. package/src/tool/keycode/i18n/de.ts +7 -7
  96. package/src/tool/keycode/i18n/en.ts +5 -5
  97. package/src/tool/keycode/i18n/es.ts +5 -5
  98. package/src/tool/keycode/i18n/fr.ts +7 -7
  99. package/src/tool/keycode/i18n/id.ts +5 -5
  100. package/src/tool/keycode/i18n/it.ts +5 -5
  101. package/src/tool/keycode/i18n/ja.ts +5 -5
  102. package/src/tool/keycode/i18n/ko.ts +5 -5
  103. package/src/tool/keycode/i18n/nl.ts +5 -5
  104. package/src/tool/keycode/i18n/pl.ts +7 -7
  105. package/src/tool/keycode/i18n/pt.ts +5 -5
  106. package/src/tool/keycode/i18n/ru.ts +6 -6
  107. package/src/tool/keycode/i18n/sv.ts +5 -5
  108. package/src/tool/keycode/i18n/tr.ts +5 -5
  109. package/src/tool/keycode/i18n/zh.ts +5 -5
  110. package/src/tool/keycode/seo.astro +2 -1
  111. package/src/tool/llmCostCalculator/component.astro +1 -2
  112. package/src/tool/llmCostCalculator/i18n/de.ts +4 -4
  113. package/src/tool/llmCostCalculator/i18n/en.ts +3 -3
  114. package/src/tool/llmCostCalculator/i18n/fr.ts +3 -3
  115. package/src/tool/llmCostCalculator/i18n/id.ts +3 -3
  116. package/src/tool/llmCostCalculator/i18n/pt.ts +3 -3
  117. package/src/tool/llmCostCalculator/i18n/ru.ts +3 -3
  118. package/src/tool/llmCostCalculator/i18n/sv.ts +3 -3
  119. package/src/tool/llmCostCalculator/i18n/tr.ts +3 -3
  120. package/src/tool/llmCostCalculator/i18n/zh.ts +3 -3
  121. package/src/tool/llmCostCalculator/seo.astro +2 -1
  122. package/src/tool/mobileMockupGenerator/component.astro +4 -5
  123. package/src/tool/mobileMockupGenerator/i18n/es.ts +1 -1
  124. package/src/tool/mobileMockupGenerator/seo.astro +2 -1
  125. package/src/tool/musicalTypography/i18n/de.ts +1 -1
  126. package/src/tool/musicalTypography/i18n/fr.ts +1 -1
  127. package/src/tool/musicalTypography/i18n/id.ts +1 -1
  128. package/src/tool/musicalTypography/i18n/it.ts +1 -1
  129. package/src/tool/musicalTypography/i18n/ko.ts +1 -1
  130. package/src/tool/musicalTypography/i18n/ru.ts +3 -3
  131. package/src/tool/musicalTypography/seo.astro +2 -1
  132. package/src/tool/placeholderGenerator/component.astro +2 -2
  133. package/src/tool/placeholderGenerator/i18n/de.ts +4 -4
  134. package/src/tool/placeholderGenerator/i18n/ru.ts +1 -1
  135. package/src/tool/placeholderGenerator/seo.astro +2 -1
  136. package/src/tool/promptLibrary/i18n/de.ts +1 -1
  137. package/src/tool/promptLibrary/seo.astro +2 -1
  138. package/src/tool/readabilityCalculator/component.astro +4 -4
  139. package/src/tool/readabilityCalculator/i18n/de.ts +2 -2
  140. package/src/tool/readabilityCalculator/seo.astro +2 -1
  141. package/src/tool/readabilityCalculator/visual-readability-calculator-wcag-apca.css +87 -3
  142. package/src/tool/serpPixelSimulator/bibliography.astro +7 -0
  143. package/src/tool/serpPixelSimulator/bibliography.ts +16 -0
  144. package/src/tool/serpPixelSimulator/component.astro +84 -0
  145. package/src/tool/serpPixelSimulator/controller.ts +323 -0
  146. package/src/tool/serpPixelSimulator/entry.ts +30 -0
  147. package/src/tool/serpPixelSimulator/i18n/de.ts +214 -0
  148. package/src/tool/serpPixelSimulator/i18n/en.ts +213 -0
  149. package/src/tool/serpPixelSimulator/i18n/es.ts +214 -0
  150. package/src/tool/serpPixelSimulator/i18n/fr.ts +214 -0
  151. package/src/tool/serpPixelSimulator/i18n/id.ts +214 -0
  152. package/src/tool/serpPixelSimulator/i18n/it.ts +214 -0
  153. package/src/tool/serpPixelSimulator/i18n/ja.ts +214 -0
  154. package/src/tool/serpPixelSimulator/i18n/ko.ts +214 -0
  155. package/src/tool/serpPixelSimulator/i18n/nl.ts +214 -0
  156. package/src/tool/serpPixelSimulator/i18n/pl.ts +214 -0
  157. package/src/tool/serpPixelSimulator/i18n/pt.ts +214 -0
  158. package/src/tool/serpPixelSimulator/i18n/ru.ts +214 -0
  159. package/src/tool/serpPixelSimulator/i18n/sv.ts +214 -0
  160. package/src/tool/serpPixelSimulator/i18n/tr.ts +214 -0
  161. package/src/tool/serpPixelSimulator/i18n/zh.ts +214 -0
  162. package/src/tool/serpPixelSimulator/index.ts +12 -0
  163. package/src/tool/serpPixelSimulator/logic.ts +18 -0
  164. package/src/tool/serpPixelSimulator/seo.astro +12 -0
  165. package/src/tool/serpPixelSimulator/serp-pixel-simulator.css +335 -0
  166. package/src/tool/serpPixelSimulator/ui.ts +33 -0
  167. package/src/tool/svgSanitizer/i18n/de.ts +2 -2
  168. package/src/tool/svgSanitizer/i18n/id.ts +1 -1
  169. package/src/tool/svgSanitizer/i18n/pl.ts +1 -1
  170. package/src/tool/svgSanitizer/i18n/ru.ts +7 -7
  171. package/src/tool/svgSanitizer/i18n/sv.ts +3 -3
  172. package/src/tool/svgSanitizer/i18n/tr.ts +3 -3
  173. package/src/tool/svgSanitizer/i18n/zh.ts +3 -3
  174. package/src/tool/svgSanitizer/seo.astro +2 -1
  175. package/src/tool/svgToCss/i18n/de.ts +2 -2
  176. package/src/tool/svgToCss/i18n/es.ts +1 -1
  177. package/src/tool/svgToCss/i18n/fr.ts +1 -1
  178. package/src/tool/svgToCss/i18n/pl.ts +1 -1
  179. package/src/tool/svgToCss/i18n/ru.ts +2 -2
  180. package/src/tool/svgToCss/i18n/zh.ts +2 -2
  181. package/src/tool/svgToCss/seo.astro +2 -1
  182. package/src/tool/urlCleaner/component.astro +23 -5
  183. package/src/tool/urlCleaner/i18n/de.ts +3 -3
  184. package/src/tool/urlCleaner/i18n/es.ts +1 -1
  185. package/src/tool/urlCleaner/i18n/id.ts +1 -1
  186. package/src/tool/urlCleaner/i18n/nl.ts +1 -1
  187. package/src/tool/urlCleaner/seo.astro +2 -1
  188. package/src/tool/urlCleaner/url-tracking-cleaner.css +92 -2
  189. package/src/tool/urlEncoderDecoder/i18n/de.ts +5 -5
  190. package/src/tool/urlEncoderDecoder/i18n/fr.ts +2 -2
  191. package/src/tool/urlEncoderDecoder/i18n/pl.ts +5 -5
  192. package/src/tool/urlEncoderDecoder/i18n/ru.ts +8 -8
  193. package/src/tool/urlEncoderDecoder/i18n/sv.ts +5 -5
  194. package/src/tool/urlEncoderDecoder/i18n/zh.ts +5 -5
  195. package/src/tool/urlEncoderDecoder/seo.astro +2 -1
  196. package/src/tool/utmGenerator/i18n/de.ts +2 -2
  197. package/src/tool/utmGenerator/i18n/pl.ts +5 -5
  198. package/src/tool/utmGenerator/i18n/ru.ts +6 -6
  199. package/src/tool/utmGenerator/seo.astro +2 -1
  200. 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 24 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(24);
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} &times; ${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 vom Smartphone bis hin zu Ultra-Wide-Monitoren fehlerhaft dargestellt wird.',
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 : 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.",
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 Rzeczywista skala”. Narzędzie automatycznie obliczy drugą wartość, aby zachować proporcje.',
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 od telefonów po monitory panoramiczne.',
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 это стандартное соотношение для YouTube и большинства современных видеоплатформ. Оно соответствует таким разрешениям, как 1280×720 (HD), 1920×1080 (Full HD) или 3840×2160 (4K UHD).',
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: 'Несоблюдение соотношения сторон частая причина сплющенных фотографий, видео с неожиданной обрезкой (letterboxing) или веб-компонентов, макет которых нарушается при просмотре на разных устройствах от мобильных телефонов до ультрашироких мониторов.',
140
+ html: 'Несоблюдение соотношения сторон - частая причина сплющенных фотографий, видео с неожиданной обрезкой (letterboxing) или веб-компонентов, макет которых нарушается при просмотре на разных устройствах - от мобильных телефонов до ультрашироких мониторов.',
141
141
  },
142
142
  {
143
143
  type: 'title',
@@ -42,7 +42,7 @@ const howToData = [
42
42
  },
43
43
  {
44
44
  name: '调整缩放',
45
- text: '在“实际缩放”部分更改宽度或高度值。工具将自动计算对应的数值以保持比例。',
45
+ text: '在"实际缩放"部分更改宽度或高度值。工具将自动计算对应的数值以保持比例。',
46
46
  },
47
47
  {
48
48
  name: '查看预览',
@@ -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 Lebenszeit zeigt, wie viel Zeit jemand mit Warten verbringt. Nutzen Sie dies als Motivation zur Optimierung.' },
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 czasu życia pokazuje, ile czyjegoś życia mija na czekaniu. Użyj tego jako motywacji do optymalizacji.' },
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: 'В сегодняшнюю цифровую эпоху скорость загрузки сайта это не роскошь, а необходимость. Каждая миллисекунда имеет значение для удержания пользователей, улучшения рейтинга в поиске и максимизации конверсии. Современные пользователи ожидают, что страницы загрузятся менее чем за 3 секунды.',
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 23 MB.',
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 (5080 % 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.',
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 livstid visar hur mycket av någons liv som spenderas på att vänta. Använd detta som motivation för att optimera.' },
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 5080 % av vikten. Minska med 4060 % med verktyg som TinyPNG.',
138
- '<strong>Minifiering:</strong> Ta bort onödig kod från CSS och JavaScript. Spara 3050 %.',
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 Kopieren neben dem HEX-, RGB- oder HSL-Format, das Sie benötigen.' },
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: 'Нажмите кнопку «Копировать» рядом с нужным форматом HEX, RGB или HSL.' },
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> управление цветом это постоянная задача. Наш <strong>онлайн-конвертер цветов</strong> позволяет мгновенно и с математической точностью преобразовывать значения между <strong>HEX, RGB и HSL</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 (.xlsx) a HTML?',
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ę Kod HTML i skopiuj wynik, aby wkleić go na swojej stronie internetowej.',
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 с дополнительными классами для границ и «зебры» (чередующихся строк). Окончательные визуальные стили зависят от CSS вашего собственного сайта, что обеспечивает идеальную интеграцию.',
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: 'Перейдите на вкладку «Код HTML» и скопируйте результат для вставки на ваш сайт.',
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 表格中,转到“文件”>“下载”>“逗号分隔值 (.csv)”,然后将该文件用于我们的工具。',
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: '切换到“HTML 代码”选项卡并复制结果以粘贴到您的网站上。',
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 alle bedeutet. Beispielsweise bedeutet ein * im Minutenfeld, dass die Aufgabe jede Minute des definierten Bereichs ausgeführt wird.',
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 Erweitert“, um die genauen Minuten, Stunden oder特定 Wochentage manuell zu definieren.',
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 die Aufgabenplanung sorgt dafür, dass die digitale Welt läuft.',
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> verwenden Sie immer absolute Pfade wie <code>/usr/local/bin/node</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> always use absolute paths like <code>/usr/local/bin/node</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
  };