@jjlmoya/utils-developer 1.29.1 → 1.31.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 (140) hide show
  1. package/package.json +70 -70
  2. package/src/category/seo.astro +1 -1
  3. package/src/components/PreviewNavSidebar.astro +1 -1
  4. package/src/components/PreviewToolbar.astro +1 -1
  5. package/src/data.ts +1 -1
  6. package/src/env.d.ts +1 -1
  7. package/src/layouts/PreviewLayout.astro +1 -1
  8. package/src/pages/[locale]/[slug].astro +162 -162
  9. package/src/pages/index.astro +1 -1
  10. package/src/tests/entry_contract.test.ts +8 -2
  11. package/src/tests/faq_count.test.ts +1 -1
  12. package/src/tests/i18n_coverage.test.ts +2 -6
  13. package/src/tests/inverted_punctuation.test.ts +113 -113
  14. package/src/tests/mocks/astro_mock.js +1 -1
  15. package/src/tests/no_h1_in_components.test.ts +1 -1
  16. package/src/tests/promote_this_website_i18n.test.ts +33 -0
  17. package/src/tests/slug_language_code_format.test.ts +25 -25
  18. package/src/tests/slug_uniqueness.test.ts +86 -86
  19. package/src/tests/tool_validation.test.ts +15 -15
  20. package/src/tool/aspectRatio/i18n/de.ts +164 -164
  21. package/src/tool/aspectRatio/i18n/ja.ts +1 -1
  22. package/src/tool/aspectRatio/i18n/ko.ts +1 -1
  23. package/src/tool/aspectRatio/i18n/pl.ts +164 -164
  24. package/src/tool/aspectRatio/i18n/ru.ts +164 -164
  25. package/src/tool/aspectRatio/i18n/zh.ts +165 -165
  26. package/src/tool/aspectRatio/seo.astro +15 -15
  27. package/src/tool/calculadoraTiempoDatos/i18n/de.ts +143 -143
  28. package/src/tool/calculadoraTiempoDatos/i18n/ja.ts +1 -1
  29. package/src/tool/calculadoraTiempoDatos/i18n/ko.ts +1 -1
  30. package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +143 -143
  31. package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +144 -144
  32. package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +143 -143
  33. package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +144 -144
  34. package/src/tool/calculadoraTiempoDatos/seo.astro +15 -15
  35. package/src/tool/colorConverter/i18n/de.ts +120 -120
  36. package/src/tool/colorConverter/i18n/ja.ts +1 -1
  37. package/src/tool/colorConverter/i18n/ko.ts +1 -1
  38. package/src/tool/colorConverter/i18n/ru.ts +120 -120
  39. package/src/tool/colorConverter/i18n/zh.ts +1 -1
  40. package/src/tool/colorConverter/seo.astro +15 -15
  41. package/src/tool/conversorExcelCsvHtml/i18n/ja.ts +1 -1
  42. package/src/tool/conversorExcelCsvHtml/i18n/ko.ts +1 -1
  43. package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +124 -124
  44. package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +124 -124
  45. package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +125 -125
  46. package/src/tool/conversorExcelCsvHtml/seo.astro +15 -15
  47. package/src/tool/cronGenerator/i18n/ja.ts +1 -1
  48. package/src/tool/cronGenerator/i18n/ko.ts +1 -1
  49. package/src/tool/cronGenerator/i18n/zh.ts +183 -183
  50. package/src/tool/cronGenerator/seo.astro +15 -15
  51. package/src/tool/cssSpecificityCalculator/i18n/ja.ts +1 -1
  52. package/src/tool/cssSpecificityCalculator/i18n/ko.ts +1 -1
  53. package/src/tool/cssSpecificityCalculator/i18n/zh.ts +1 -1
  54. package/src/tool/cssSpecificityCalculator/seo.astro +15 -15
  55. package/src/tool/cssToInlineConverter/i18n/ja.ts +1 -1
  56. package/src/tool/cssToInlineConverter/i18n/ko.ts +1 -1
  57. package/src/tool/cssToInlineConverter/i18n/zh.ts +1 -1
  58. package/src/tool/cssToInlineConverter/seo.astro +15 -15
  59. package/src/tool/duplicateCssRemover/i18n/ja.ts +1 -1
  60. package/src/tool/duplicateCssRemover/i18n/ko.ts +1 -1
  61. package/src/tool/duplicateCssRemover/i18n/zh.ts +1 -1
  62. package/src/tool/duplicateCssRemover/seo.astro +15 -15
  63. package/src/tool/generadorSecurityTxt/i18n/ja.ts +1 -1
  64. package/src/tool/generadorSecurityTxt/i18n/ko.ts +1 -1
  65. package/src/tool/generadorSecurityTxt/i18n/zh.ts +1 -1
  66. package/src/tool/generadorSecurityTxt/seo.astro +15 -15
  67. package/src/tool/hashGenerator/i18n/ja.ts +1 -1
  68. package/src/tool/hashGenerator/i18n/ko.ts +1 -1
  69. package/src/tool/hashGenerator/seo.astro +15 -15
  70. package/src/tool/inspectorCertificadosSsl/i18n/ja.ts +1 -1
  71. package/src/tool/inspectorCertificadosSsl/i18n/ko.ts +1 -1
  72. package/src/tool/inspectorCertificadosSsl/i18n/zh.ts +1 -1
  73. package/src/tool/inspectorCertificadosSsl/seo.astro +15 -15
  74. package/src/tool/jsonFormatter/seo.astro +15 -15
  75. package/src/tool/jwtDecoder/i18n/es.ts +248 -248
  76. package/src/tool/jwtDecoder/i18n/fr.ts +248 -248
  77. package/src/tool/jwtDecoder/i18n/tr.ts +248 -248
  78. package/src/tool/keycode/seo.astro +15 -15
  79. package/src/tool/llmCostCalculator/i18n/fr.ts +149 -149
  80. package/src/tool/llmCostCalculator/i18n/ja.ts +1 -1
  81. package/src/tool/llmCostCalculator/i18n/ko.ts +1 -1
  82. package/src/tool/llmCostCalculator/seo.astro +15 -15
  83. package/src/tool/mobileMockupGenerator/i18n/ja.ts +1 -1
  84. package/src/tool/mobileMockupGenerator/i18n/ko.ts +1 -1
  85. package/src/tool/mobileMockupGenerator/i18n/zh.ts +1 -1
  86. package/src/tool/mobileMockupGenerator/seo.astro +15 -15
  87. package/src/tool/musicalTypography/i18n/ja.ts +1 -1
  88. package/src/tool/musicalTypography/i18n/ko.ts +1 -1
  89. package/src/tool/musicalTypography/i18n/zh.ts +1 -1
  90. package/src/tool/musicalTypography/seo.astro +15 -15
  91. package/src/tool/placeholderGenerator/i18n/ja.ts +1 -1
  92. package/src/tool/placeholderGenerator/i18n/ko.ts +1 -1
  93. package/src/tool/placeholderGenerator/i18n/zh.ts +1 -1
  94. package/src/tool/placeholderGenerator/seo.astro +15 -15
  95. package/src/tool/promoteThisWebsite/entry.ts +13 -0
  96. package/src/tool/promoteThisWebsite/i18n/de.ts +95 -0
  97. package/src/tool/promoteThisWebsite/i18n/fr.ts +95 -0
  98. package/src/tool/promoteThisWebsite/i18n/id.ts +95 -0
  99. package/src/tool/promoteThisWebsite/i18n/it.ts +95 -0
  100. package/src/tool/promoteThisWebsite/i18n/ja.ts +95 -0
  101. package/src/tool/promoteThisWebsite/i18n/ko.ts +95 -0
  102. package/src/tool/promoteThisWebsite/i18n/nl.ts +95 -0
  103. package/src/tool/promoteThisWebsite/i18n/pl.ts +95 -0
  104. package/src/tool/promoteThisWebsite/i18n/pt.ts +95 -0
  105. package/src/tool/promoteThisWebsite/i18n/ru.ts +95 -0
  106. package/src/tool/promoteThisWebsite/i18n/sv.ts +95 -0
  107. package/src/tool/promoteThisWebsite/i18n/tr.ts +95 -0
  108. package/src/tool/promoteThisWebsite/i18n/zh.ts +95 -0
  109. package/src/tool/promptLibrary/i18n/ja.ts +1 -1
  110. package/src/tool/promptLibrary/i18n/ko.ts +1 -1
  111. package/src/tool/promptLibrary/i18n/zh.ts +1 -1
  112. package/src/tool/promptLibrary/seo.astro +15 -15
  113. package/src/tool/readabilityCalculator/i18n/ja.ts +1 -1
  114. package/src/tool/readabilityCalculator/i18n/ko.ts +1 -1
  115. package/src/tool/readabilityCalculator/i18n/zh.ts +1 -1
  116. package/src/tool/readabilityCalculator/seo.astro +15 -15
  117. package/src/tool/serpPixelSimulator/entry.ts +30 -30
  118. package/src/tool/serpPixelSimulator/i18n/en.ts +211 -211
  119. package/src/tool/serpPixelSimulator/i18n/fr.ts +214 -214
  120. package/src/tool/serpPixelSimulator/i18n/ru.ts +214 -214
  121. package/src/tool/serpPixelSimulator/logic.ts +18 -18
  122. package/src/tool/serpPixelSimulator/serp-pixel-simulator.css +195 -195
  123. package/src/tool/svgSanitizer/i18n/ja.ts +1 -1
  124. package/src/tool/svgSanitizer/i18n/ko.ts +1 -1
  125. package/src/tool/svgSanitizer/seo.astro +15 -15
  126. package/src/tool/svgToCss/i18n/ja.ts +1 -1
  127. package/src/tool/svgToCss/i18n/ko.ts +1 -1
  128. package/src/tool/svgToCss/seo.astro +15 -15
  129. package/src/tool/urlCleaner/i18n/ja.ts +1 -1
  130. package/src/tool/urlCleaner/i18n/ko.ts +1 -1
  131. package/src/tool/urlCleaner/i18n/zh.ts +1 -1
  132. package/src/tool/urlCleaner/seo.astro +15 -15
  133. package/src/tool/urlEncoderDecoder/i18n/es.ts +146 -146
  134. package/src/tool/urlEncoderDecoder/i18n/ko.ts +1 -1
  135. package/src/tool/urlEncoderDecoder/seo.astro +15 -15
  136. package/src/tool/utmGenerator/i18n/ja.ts +1 -1
  137. package/src/tool/utmGenerator/i18n/ko.ts +1 -1
  138. package/src/tool/utmGenerator/i18n/zh.ts +1 -1
  139. package/src/tool/utmGenerator/seo.astro +15 -15
  140. package/src/types.ts +1 -1
package/package.json CHANGED
@@ -1,70 +1,70 @@
1
- {
2
- "name": "@jjlmoya/utils-developer",
3
- "version": "1.29.1",
4
- "type": "module",
5
- "main": "./src/index.ts",
6
- "types": "./src/index.ts",
7
- "exports": {
8
- ".": "./src/index.ts",
9
- "./data": "./src/data.ts",
10
- "./entries": "./src/entries.ts",
11
- "./runtime/*": "./src/tool/*/index.ts",
12
- "./category-seo": "./src/category/seo.astro"
13
- },
14
- "files": [
15
- "src",
16
- "scripts"
17
- ],
18
- "publishConfig": {
19
- "access": "public"
20
- },
21
- "scripts": {
22
- "dev": "astro dev",
23
- "start": "astro dev",
24
- "build": "astro build",
25
- "preview": "astro preview",
26
- "astro": "astro",
27
- "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
28
- "check": "astro check",
29
- "type-check": "astro check",
30
- "test": "vitest run",
31
- "preversion": "npm run lint && npm run test && npm run build",
32
- "postversion": "git push && git push --tags",
33
- "patch": "npm version patch",
34
- "minor": "npm version minor",
35
- "major": "npm version major",
36
- "postinstall": "node scripts/postinstall.mjs"
37
- },
38
- "lint-staged": {
39
- "*.{ts,tsx,astro}": [
40
- "eslint --fix"
41
- ]
42
- },
43
- "dependencies": {
44
- "@iconify-json/mdi": "^1.2.3",
45
- "@jjlmoya/prompagate": "^1.1.0",
46
- "@jjlmoya/utils-shared": "1.2.0",
47
- "@types/file-saver": "^2.0.7",
48
- "astro": "^6.1.2",
49
- "astro-icon": "^1.1.0",
50
- "file-saver": "^2.0.5",
51
- "jszip": "^3.10.1"
52
- },
53
- "devDependencies": {
54
- "@astrojs/check": "^0.9.8",
55
- "@types/jszip": "^3.4.0",
56
- "eslint": "^9.39.4",
57
- "eslint-plugin-astro": "^1.6.0",
58
- "eslint-plugin-no-comments": "^1.1.10",
59
- "husky": "^9.1.7",
60
- "lint-staged": "^16.4.0",
61
- "postcss-html": "^1.8.1",
62
- "schema-dts": "^1.1.2",
63
- "stylelint": "^17.6.0",
64
- "stylelint-config-standard": "^40.0.0",
65
- "stylelint-declaration-strict-value": "^1.11.1",
66
- "typescript": "^5.4.0",
67
- "typescript-eslint": "^8.58.0",
68
- "vitest": "^4.1.2"
69
- }
70
- }
1
+ {
2
+ "name": "@jjlmoya/utils-developer",
3
+ "version": "1.31.0",
4
+ "type": "module",
5
+ "main": "./src/index.ts",
6
+ "types": "./src/index.ts",
7
+ "exports": {
8
+ ".": "./src/index.ts",
9
+ "./data": "./src/data.ts",
10
+ "./entries": "./src/entries.ts",
11
+ "./runtime/*": "./src/tool/*/index.ts",
12
+ "./category-seo": "./src/category/seo.astro"
13
+ },
14
+ "files": [
15
+ "src",
16
+ "scripts"
17
+ ],
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "dev": "astro dev",
23
+ "start": "astro dev",
24
+ "build": "astro build",
25
+ "preview": "astro preview",
26
+ "astro": "astro",
27
+ "lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
28
+ "check": "astro check",
29
+ "type-check": "astro check",
30
+ "test": "vitest run",
31
+ "preversion": "npm run lint && npm run test && npm run build",
32
+ "postversion": "git push && git push --tags",
33
+ "patch": "npm version patch",
34
+ "minor": "npm version minor",
35
+ "major": "npm version major",
36
+ "postinstall": "node scripts/postinstall.mjs"
37
+ },
38
+ "lint-staged": {
39
+ "*.{ts,tsx,astro}": [
40
+ "eslint --fix"
41
+ ]
42
+ },
43
+ "dependencies": {
44
+ "@iconify-json/mdi": "^1.2.3",
45
+ "@jjlmoya/prompagate": "^1.1.0",
46
+ "@jjlmoya/utils-shared": "1.2.0",
47
+ "@types/file-saver": "^2.0.7",
48
+ "astro": "^6.1.2",
49
+ "astro-icon": "^1.1.0",
50
+ "file-saver": "^2.0.5",
51
+ "jszip": "^3.10.1"
52
+ },
53
+ "devDependencies": {
54
+ "@astrojs/check": "^0.9.8",
55
+ "@types/jszip": "^3.4.0",
56
+ "eslint": "^9.39.4",
57
+ "eslint-plugin-astro": "^1.6.0",
58
+ "eslint-plugin-no-comments": "^1.1.10",
59
+ "husky": "^9.1.7",
60
+ "lint-staged": "^16.4.0",
61
+ "postcss-html": "^1.8.1",
62
+ "schema-dts": "^1.1.2",
63
+ "stylelint": "^17.6.0",
64
+ "stylelint-config-standard": "^40.0.0",
65
+ "stylelint-declaration-strict-value": "^1.11.1",
66
+ "typescript": "^5.4.0",
67
+ "typescript-eslint": "^8.58.0",
68
+ "vitest": "^4.1.2"
69
+ }
70
+ }
@@ -12,4 +12,4 @@ const content = await developerCategory.i18n[locale]?.();
12
12
  ---
13
13
 
14
14
  {content && <SEORenderer content={{ locale, sections: content.seo }} />}
15
-
15
+
@@ -113,4 +113,4 @@ const { categoryTitle, tools = [] } = Astro.props;
113
113
  white-space: nowrap;
114
114
  }
115
115
  </style>
116
-
116
+
@@ -140,4 +140,4 @@ const otherLocales = Object.entries(localeUrls).filter(([l]) => l !== currentLoc
140
140
  box-shadow: inset -4px -2px 0 #1e293b;
141
141
  }
142
142
  </style>
143
-
143
+
package/src/data.ts CHANGED
@@ -8,4 +8,4 @@ export type {
8
8
  DeveloperToolEntry,
9
9
  DeveloperCategoryEntry,
10
10
  } from './types';
11
-
11
+
package/src/env.d.ts CHANGED
@@ -2,4 +2,4 @@ declare module '*.astro' {
2
2
  const Component: (_props: Record<string, unknown>) => unknown;
3
3
  export default Component;
4
4
  }
5
-
5
+
@@ -115,4 +115,4 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
115
115
  }
116
116
  }
117
117
  </style>
118
-
118
+
@@ -1,162 +1,162 @@
1
- ---
2
- import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
- import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
- import { ALL_TOOLS } from "../../index";
5
- import {
6
- UtilityHeader,
7
- FAQSection,
8
- Bibliography,
9
- SEORenderer,
10
- } from "@jjlmoya/utils-shared";
11
- import type { KnownLocale, ToolLocaleContent } from "../../types";
12
- import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
-
14
- export async function getStaticPaths() {
15
- const paths = [];
16
-
17
- for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
- const { default: Component } = await lazyComp();
19
- const localeEntries = Object.entries(entry.i18n) as [
20
- KnownLocale,
21
- () => Promise<ToolLocaleContent>,
22
- ][];
23
- const localeContents = await Promise.all(
24
- localeEntries.map(async ([locale, loader]) => ({
25
- locale,
26
- content: await loader(),
27
- })),
28
- );
29
-
30
- const localeUrls = Object.fromEntries(
31
- localeContents.map(({ locale, content }) => [
32
- locale,
33
- `/${locale}/${content.slug}`,
34
- ]),
35
- ) as Partial<Record<KnownLocale, string>>;
36
-
37
- const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
- const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
-
40
- for (const { locale, content } of localeContents) {
41
- const allToolsNav = (
42
- await Promise.all(
43
- ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
- const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
- if (!loader) return null;
46
- const navContent = await loader();
47
- return {
48
- id: navEntry.id,
49
- title: navContent.title,
50
- href: `/${locale}/${navContent.slug}`,
51
- isActive: navEntry.id === entry.id,
52
- };
53
- }),
54
- )
55
- ).filter(Boolean) as NavItem[];
56
- paths.push({
57
- params: { locale, slug: content.slug },
58
- props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
- });
60
- }
61
- }
62
-
63
- return paths;
64
- }
65
-
66
- interface NavItem {
67
- id: string;
68
- title: string;
69
- href: string;
70
- isActive?: boolean;
71
- }
72
-
73
- interface Props {
74
- Component: unknown;
75
- locale: KnownLocale;
76
- content: ToolLocaleContent;
77
- localeUrls: Partial<Record<KnownLocale, string>>;
78
- allToolsNav: NavItem[];
79
- englishSlug: string;
80
- }
81
-
82
- const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
-
84
- const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
- const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
- const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
87
- const toolCss = cssLoader ? await cssLoader() as string : "";
88
-
89
- const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
-
91
- const words = content.title.split(" ");
92
- const titleHighlight = words[0] || "";
93
- const titleBase = words.slice(1).join(" ") || "";
94
- ---
95
-
96
- <PreviewLayout
97
- title={content.title}
98
- currentLocale={locale}
99
- localeUrls={localeUrls}
100
- hasSidebar={true}
101
- >
102
- <PreviewNavSidebar
103
- slot="sidebar"
104
- categoryTitle="Tools"
105
- tools={allToolsNav}
106
- />
107
- <Fragment slot="head">
108
- {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
109
- {
110
- ( content.schemas ?? []).map((schema: unknown) => (
111
- <script
112
- is:inline
113
- type="application/ld+json"
114
- set:html={JSON.stringify(schema)}
115
- />
116
- ))
117
- }
118
- </Fragment>
119
-
120
- <div class="tool-page">
121
- <UtilityHeader
122
- titleHighlight={titleHighlight}
123
- titleBase={titleBase}
124
- description={content.description}
125
- />
126
-
127
- <section class="section-tool">
128
- <Component ui={content.ui} />
129
- </section>
130
-
131
- <section class="section-seo">
132
- <SEORenderer content={seoContent} />
133
- </section>
134
-
135
- <section class="section-faq">
136
- <FAQSection items={content.faq} title={content.ui.faqTitle} inLanguage={locale} />
137
- </section>
138
-
139
- <section class="section-bibliography">
140
- <Bibliography links={content.bibliography} title={content.ui.bibliographyTitle} />
141
- </section>
142
- </div>
143
- </PreviewLayout>
144
-
145
- <style>
146
- .tool-page {
147
- display: flex;
148
- flex-direction: column;
149
- gap: 2rem;
150
- }
151
- .section-tool {
152
- max-width: 1200px;
153
- margin: 0 auto;
154
- width: 100%;
155
- }
156
- .section-seo,
157
- .section-faq,
158
- .section-bibliography {
159
- padding-top: 2rem;
160
- border-top: 1px solid var(--border-color);
161
- }
162
- </style>
1
+ ---
2
+ import PreviewLayout from "../../layouts/PreviewLayout.astro";
3
+ import PreviewNavSidebar from "../../components/PreviewNavSidebar.astro";
4
+ import { ALL_TOOLS } from "../../index";
5
+ import {
6
+ UtilityHeader,
7
+ FAQSection,
8
+ Bibliography,
9
+ SEORenderer,
10
+ } from "@jjlmoya/utils-shared";
11
+ import type { KnownLocale, ToolLocaleContent } from "../../types";
12
+ import type { UtilitySEOContent } from "@jjlmoya/utils-shared";
13
+
14
+ export async function getStaticPaths() {
15
+ const paths = [];
16
+
17
+ for (const { entry, Component: lazyComp } of ALL_TOOLS) {
18
+ const { default: Component } = await lazyComp();
19
+ const localeEntries = Object.entries(entry.i18n) as [
20
+ KnownLocale,
21
+ () => Promise<ToolLocaleContent>,
22
+ ][];
23
+ const localeContents = await Promise.all(
24
+ localeEntries.map(async ([locale, loader]) => ({
25
+ locale,
26
+ content: await loader(),
27
+ })),
28
+ );
29
+
30
+ const localeUrls = Object.fromEntries(
31
+ localeContents.map(({ locale, content }) => [
32
+ locale,
33
+ `/${locale}/${content.slug}`,
34
+ ]),
35
+ ) as Partial<Record<KnownLocale, string>>;
36
+
37
+ const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
38
+ const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
39
+
40
+ for (const { locale, content } of localeContents) {
41
+ const allToolsNav = (
42
+ await Promise.all(
43
+ ALL_TOOLS.map(async ({ entry: navEntry }) => {
44
+ const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
45
+ if (!loader) return null;
46
+ const navContent = await loader();
47
+ return {
48
+ id: navEntry.id,
49
+ title: navContent.title,
50
+ href: `/${locale}/${navContent.slug}`,
51
+ isActive: navEntry.id === entry.id,
52
+ };
53
+ }),
54
+ )
55
+ ).filter(Boolean) as NavItem[];
56
+ paths.push({
57
+ params: { locale, slug: content.slug },
58
+ props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
59
+ });
60
+ }
61
+ }
62
+
63
+ return paths;
64
+ }
65
+
66
+ interface NavItem {
67
+ id: string;
68
+ title: string;
69
+ href: string;
70
+ isActive?: boolean;
71
+ }
72
+
73
+ interface Props {
74
+ Component: unknown;
75
+ locale: KnownLocale;
76
+ content: ToolLocaleContent;
77
+ localeUrls: Partial<Record<KnownLocale, string>>;
78
+ allToolsNav: NavItem[];
79
+ englishSlug: string;
80
+ }
81
+
82
+ const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
83
+
84
+ const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
85
+ const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
86
+ const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
87
+ const toolCss = cssLoader ? await cssLoader() as string : "";
88
+
89
+ const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
90
+
91
+ const words = content.title.split(" ");
92
+ const titleHighlight = words[0] || "";
93
+ const titleBase = words.slice(1).join(" ") || "";
94
+ ---
95
+
96
+ <PreviewLayout
97
+ title={content.title}
98
+ currentLocale={locale}
99
+ localeUrls={localeUrls}
100
+ hasSidebar={true}
101
+ >
102
+ <PreviewNavSidebar
103
+ slot="sidebar"
104
+ categoryTitle="Tools"
105
+ tools={allToolsNav}
106
+ />
107
+ <Fragment slot="head">
108
+ {toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
109
+ {
110
+ ( content.schemas ?? []).map((schema: unknown) => (
111
+ <script
112
+ is:inline
113
+ type="application/ld+json"
114
+ set:html={JSON.stringify(schema)}
115
+ />
116
+ ))
117
+ }
118
+ </Fragment>
119
+
120
+ <div class="tool-page">
121
+ <UtilityHeader
122
+ titleHighlight={titleHighlight}
123
+ titleBase={titleBase}
124
+ description={content.description}
125
+ />
126
+
127
+ <section class="section-tool">
128
+ <Component ui={content.ui} />
129
+ </section>
130
+
131
+ <section class="section-seo">
132
+ <SEORenderer content={seoContent} />
133
+ </section>
134
+
135
+ <section class="section-faq">
136
+ <FAQSection items={content.faq} title={content.ui.faqTitle} inLanguage={locale} />
137
+ </section>
138
+
139
+ <section class="section-bibliography">
140
+ <Bibliography links={content.bibliography} title={content.ui.bibliographyTitle} />
141
+ </section>
142
+ </div>
143
+ </PreviewLayout>
144
+
145
+ <style>
146
+ .tool-page {
147
+ display: flex;
148
+ flex-direction: column;
149
+ gap: 2rem;
150
+ }
151
+ .section-tool {
152
+ max-width: 1200px;
153
+ margin: 0 auto;
154
+ width: 100%;
155
+ }
156
+ .section-seo,
157
+ .section-faq,
158
+ .section-bibliography {
159
+ padding-top: 2rem;
160
+ border-top: 1px solid var(--border-color);
161
+ }
162
+ </style>
@@ -1,4 +1,4 @@
1
1
  ---
2
2
  ---
3
3
  <meta http-equiv="refresh" content="0;url=/es" />
4
-
4
+
@@ -2,9 +2,15 @@ import { describe, expect, it } from 'vitest';
2
2
  import { ALL_ENTRIES } from '../entries';
3
3
 
4
4
  describe('Tool entry consumer contract', () => {
5
- it('every published tool exposes a Spanish locale loader', () => {
5
+ const requiredLocales = [
6
+ 'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh',
7
+ ];
8
+
9
+ it('every published tool exposes all required locale loaders', () => {
6
10
  for (const entry of ALL_ENTRIES) {
7
- expect(typeof entry.i18n.es, `${entry.id} must expose i18n.es`).toBe('function');
11
+ for (const locale of requiredLocales) {
12
+ expect(typeof entry.i18n[locale as keyof typeof entry.i18n], `${entry.id} must expose i18n.${locale}`).toBe('function');
13
+ }
8
14
  }
9
15
  });
10
16
  });
@@ -16,4 +16,4 @@ describe('FAQ Content Validation', () => {
16
16
  expect(TOOLS.length).toBe(0);
17
17
  });
18
18
  });
19
-
19
+
@@ -4,8 +4,6 @@ import { ALL_TOOLS } from '../tools';
4
4
  const EXPECTED_LOCALES = [
5
5
  'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh'
6
6
  ];
7
- const ENGLISH_FIRST_TOOLS = new Set(['promote-this-website']);
8
-
9
7
  describe('I18n Coverage Validation', () => {
10
8
  it('all tools should be registered', () => {
11
9
  expect(ALL_TOOLS.length).toBeGreaterThan(0);
@@ -15,8 +13,7 @@ describe('I18n Coverage Validation', () => {
15
13
  describe(`Tool: ${entry.id}`, () => {
16
14
  it('should have all 15 required locales', () => {
17
15
  const registeredLocales = Object.keys(entry.i18n);
18
- const requiredLocales = ENGLISH_FIRST_TOOLS.has(entry.id) ? ['en'] : EXPECTED_LOCALES;
19
- requiredLocales.forEach((locale) => {
16
+ EXPECTED_LOCALES.forEach((locale) => {
20
17
  expect(
21
18
  registeredLocales,
22
19
  `Tool "${entry.id}" is missing locale "${locale}"`,
@@ -25,8 +22,7 @@ describe('I18n Coverage Validation', () => {
25
22
  });
26
23
 
27
24
  it('all locale loaders should be functions', () => {
28
- const requiredLocales = ENGLISH_FIRST_TOOLS.has(entry.id) ? ['en'] : EXPECTED_LOCALES;
29
- requiredLocales.forEach((locale) => {
25
+ EXPECTED_LOCALES.forEach((locale) => {
30
26
  const loader = entry.i18n[locale as keyof typeof entry.i18n];
31
27
  expect(
32
28
  typeof loader,