@jjlmoya/utils-drones 1.31.0 → 1.33.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 (67) hide show
  1. package/package.json +1 -1
  2. package/src/category/i18n/de.ts +1 -1
  3. package/src/category/i18n/en.ts +2 -2
  4. package/src/category/i18n/es.ts +1 -1
  5. package/src/category/i18n/fr.ts +2 -2
  6. package/src/category/i18n/id.ts +1 -1
  7. package/src/category/i18n/it.ts +1 -1
  8. package/src/category/i18n/nl.ts +1 -1
  9. package/src/category/i18n/pl.ts +1 -1
  10. package/src/category/i18n/pt.ts +1 -1
  11. package/src/category/i18n/sv.ts +1 -1
  12. package/src/category/i18n/tr.ts +1 -1
  13. package/src/components/PreviewNavSidebar.astro +116 -116
  14. package/src/components/PreviewToolbar.astro +143 -143
  15. package/src/data.ts +1 -1
  16. package/src/env.d.ts +1 -1
  17. package/src/layouts/PreviewLayout.astro +118 -118
  18. package/src/pages/[locale].astro +251 -251
  19. package/src/pages/index.astro +4 -4
  20. package/src/tests/bibliography_wellformed_export.test.ts +46 -0
  21. package/src/tests/category_seo_quality.test.ts +76 -0
  22. package/src/tests/faq_count.test.ts +1 -1
  23. package/src/tests/mocks/astro_mock.js +1 -1
  24. package/src/tests/no_h1_in_components.test.ts +1 -1
  25. package/src/tests/seo_translation_completeness.test.ts +69 -0
  26. package/src/tests/seo_wellformed_export.test.ts +65 -0
  27. package/src/tests/tool_validation.test.ts +15 -15
  28. package/src/tool/antenna-length-calculator/bibliography.astro +6 -6
  29. package/src/tool/antenna-length-calculator/component.astro +329 -329
  30. package/src/tool/antenna-length-calculator/seo.astro +15 -15
  31. package/src/tool/drone-battery-c-rating-calculator/i18n/de.ts +192 -192
  32. package/src/tool/drone-battery-c-rating-calculator/i18n/en.ts +192 -192
  33. package/src/tool/drone-battery-c-rating-calculator/i18n/es.ts +192 -192
  34. package/src/tool/drone-battery-c-rating-calculator/i18n/id.ts +192 -192
  35. package/src/tool/drone-battery-c-rating-calculator/i18n/ja.ts +192 -192
  36. package/src/tool/drone-battery-c-rating-calculator/i18n/ko.ts +192 -192
  37. package/src/tool/drone-battery-c-rating-calculator/i18n/nl.ts +192 -192
  38. package/src/tool/drone-battery-c-rating-calculator/i18n/pl.ts +192 -192
  39. package/src/tool/drone-battery-c-rating-calculator/i18n/ru.ts +184 -184
  40. package/src/tool/drone-battery-c-rating-calculator/i18n/sv.ts +189 -189
  41. package/src/tool/drone-battery-c-rating-calculator/i18n/tr.ts +187 -187
  42. package/src/tool/drone-battery-c-rating-calculator/i18n/zh.ts +192 -192
  43. package/src/tool/drone-flight-time/component.astro +187 -187
  44. package/src/tool/drone-flight-time/seo.astro +15 -15
  45. package/src/tool/drone-motor-propeller-calculator/i18n/de.ts +1 -1
  46. package/src/tool/drone-motor-propeller-calculator/i18n/fr.ts +1 -1
  47. package/src/tool/drone-motor-propeller-calculator/i18n/id.ts +1 -1
  48. package/src/tool/drone-motor-propeller-calculator/i18n/it.ts +1 -1
  49. package/src/tool/drone-motor-propeller-calculator/i18n/ja.ts +1 -1
  50. package/src/tool/drone-motor-propeller-calculator/i18n/ko.ts +1 -1
  51. package/src/tool/drone-motor-propeller-calculator/i18n/nl.ts +1 -1
  52. package/src/tool/drone-motor-propeller-calculator/i18n/pl.ts +1 -1
  53. package/src/tool/drone-motor-propeller-calculator/i18n/pt.ts +1 -1
  54. package/src/tool/drone-motor-propeller-calculator/i18n/ru.ts +1 -1
  55. package/src/tool/drone-motor-propeller-calculator/i18n/sv.ts +1 -1
  56. package/src/tool/drone-motor-propeller-calculator/i18n/tr.ts +1 -1
  57. package/src/tool/drone-motor-propeller-calculator/i18n/zh.ts +2 -2
  58. package/src/tool/drone-motor-propeller-calculator/seo.astro +7 -4
  59. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/id.ts +1 -1
  60. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/it.ts +1 -1
  61. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/nl.ts +1 -1
  62. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/pt.ts +1 -1
  63. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/ru.ts +1 -1
  64. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/sv.ts +2 -2
  65. package/src/tool/fpv-drone-thrust-to-weight-ratio/i18n/tr.ts +1 -1
  66. package/src/tool/gps-coordinates-converter/component.astro +275 -275
  67. package/src/tool/gps-coordinates-converter/seo.astro +15 -15
@@ -0,0 +1,69 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+
4
+ const ASIAN_LOCALES = ['ja', 'ko', 'zh'];
5
+
6
+ function getSimpleSectionLength(section: any): number {
7
+ if (section.type === 'title') return section.text ? section.text.length : 0;
8
+ if (section.type === 'paragraph') return section.html ? section.html.replace(/<[^>]*>/g, '').length : 0;
9
+ if (section.type === 'list') return section.items ? section.items.join('').length : 0;
10
+ return 0;
11
+ }
12
+
13
+ function getComplexSectionLength(section: any): number {
14
+ if (section.type === 'diagnostic' || section.type === 'tip') {
15
+ return (section.title ? section.title.length : 0) + (section.html ? section.html.replace(/<[^>]*>/g, '').length : 0);
16
+ }
17
+ if (section.type === 'table') {
18
+ return ((section.headers || []).join('').length) + ((section.rows || []).flat().join('').length);
19
+ }
20
+ return 0;
21
+ }
22
+
23
+ function getSectionLength(section: any): number {
24
+ return getSimpleSectionLength(section) || getComplexSectionLength(section);
25
+ }
26
+
27
+ function calculateSeoTextLength(seoSections: any[]): number {
28
+ return seoSections.reduce((acc, section) => acc + getSectionLength(section), 0);
29
+ }
30
+
31
+ function checkLocaleSeoLength(toolId: string, locale: string, localeLen: number, enLen: number): void {
32
+ const isAsian = ASIAN_LOCALES.includes(locale);
33
+ const minLength = Math.floor(enLen * (isAsian ? 0.25 : 0.70));
34
+ const msgType = isAsian ? 'suspiciously short' : 'truncated/lazy';
35
+
36
+ expect(
37
+ localeLen,
38
+ `[LAZY SEO TRANSLATION] Tool "${toolId}" locale "${locale}" SEO text is ${msgType} (${localeLen} chars vs EN ${enLen} chars, expected min ${minLength})`,
39
+ ).toBeGreaterThanOrEqual(minLength);
40
+ }
41
+
42
+ async function auditSingleLocale(toolId: string, locale: string, loader: any, enSeoLength: number): Promise<void> {
43
+ if (locale === 'en' || !loader) return;
44
+ const content = await loader();
45
+ if (!content.seo || !Array.isArray(content.seo)) return;
46
+
47
+ checkLocaleSeoLength(toolId, locale, calculateSeoTextLength(content.seo), enSeoLength);
48
+ }
49
+
50
+ describe('SEO Translation Completeness & Laziness Audit', () => {
51
+ ALL_TOOLS.forEach(({ entry }) => {
52
+ describe(`Tool: ${entry.id}`, () => {
53
+ it('should not have lazy or truncated SEO content compared to English reference', async () => {
54
+ const enLoader = entry.i18n['en' as keyof typeof entry.i18n];
55
+ if (!enLoader) return;
56
+
57
+ const enContent = await enLoader();
58
+ if (!enContent.seo || !Array.isArray(enContent.seo)) return;
59
+
60
+ const enSeoLength = calculateSeoTextLength(enContent.seo);
61
+
62
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
63
+ await auditSingleLocale(entry.id, locale, loader, enSeoLength);
64
+ }
65
+ });
66
+ });
67
+ });
68
+ });
69
+
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { readdirSync, readFileSync, existsSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+
5
+ function findSeoAstroFiles(dir: string): string[] {
6
+ const files: string[] = [];
7
+ if (!existsSync(dir)) return files;
8
+ const entries = readdirSync(dir, { withFileTypes: true });
9
+
10
+ for (const entry of entries) {
11
+ const fullPath = join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ files.push(...findSeoAstroFiles(fullPath));
14
+ } else if (entry.isFile() && entry.name === 'seo.astro') {
15
+ files.push(fullPath);
16
+ }
17
+ }
18
+
19
+ return files;
20
+ }
21
+
22
+ const toolDir = join(process.cwd(), 'src', 'tool');
23
+ const seoFiles = findSeoAstroFiles(toolDir);
24
+
25
+ describe('SEO Component Wellformed Export', () => {
26
+ it('found tool seo.astro components', () => {
27
+ expect(seoFiles.length).toBeGreaterThan(0);
28
+ });
29
+
30
+ seoFiles.forEach((file) => {
31
+ const relativePath = relative(process.cwd(), file);
32
+
33
+ it(`${relativePath} should dynamically load SEO sections and use SEORenderer`, () => {
34
+ const content = readFileSync(file, 'utf-8');
35
+
36
+ const usesSeoRenderer =
37
+ content.includes('@jjlmoya/utils-shared') &&
38
+ (content.includes('SEORenderer') || content.includes('SEOArticle'));
39
+
40
+ const acquiresDynamicContent =
41
+ content.includes('.i18n') ||
42
+ content.includes('loader') ||
43
+ content.includes('await loader') ||
44
+ content.includes('.seo');
45
+
46
+ const isBrokenPattern =
47
+ /sections\s*=\s*\[\s*\]/.test(content) && !acquiresDynamicContent;
48
+
49
+ expect(
50
+ usesSeoRenderer,
51
+ `File "${relativePath}" does not import or use SEORenderer from @jjlmoya/utils-shared.`,
52
+ ).toBe(true);
53
+
54
+ expect(
55
+ isBrokenPattern,
56
+ `File "${relativePath}" relies on a static sections=[] prop default without fetching content from entry.i18n, resulting in empty SEO text on consumers.`,
57
+ ).toBe(false);
58
+
59
+ expect(
60
+ acquiresDynamicContent,
61
+ `File "${relativePath}" does not fetch dynamic i18n content for SEO rendering.`,
62
+ ).toBe(true);
63
+ });
64
+ });
65
+ });
@@ -1,17 +1,17 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { ALL_TOOLS } from '../tools';
3
- import { dronesCategory } from '../data';
4
-
5
- describe('Tool Validation Suite', () => {
6
- describe('Library Registration', () => {
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ALL_TOOLS } from '../tools';
3
+ import { dronesCategory } from '../data';
4
+
5
+ describe('Tool Validation Suite', () => {
6
+ describe('Library Registration', () => {
7
7
  it('should have 8 tools in ALL_TOOLS', () => {
8
8
  expect(ALL_TOOLS.length).toBe(8);
9
- });
10
-
11
- it('dronesCategory should be defined', () => {
12
- expect(dronesCategory).toBeDefined();
13
- expect(dronesCategory.i18n).toBeDefined();
14
- });
15
- });
16
- });
17
-
9
+ });
10
+
11
+ it('dronesCategory should be defined', () => {
12
+ expect(dronesCategory).toBeDefined();
13
+ expect(dronesCategory.i18n).toBeDefined();
14
+ });
15
+ });
16
+ });
17
+
@@ -1,6 +1,6 @@
1
- ---
2
- import { Bibliography as BibliographyComponent } from '@jjlmoya/utils-shared';
3
- import { bibliography } from './bibliography';
4
- ---
5
-
6
- <BibliographyComponent links={bibliography} />
1
+ ---
2
+ import { Bibliography as BibliographyComponent } from '@jjlmoya/utils-shared';
3
+ import { bibliography } from './bibliography';
4
+ ---
5
+
6
+ <BibliographyComponent links={bibliography} />