@jjlmoya/utils-diy 1.26.0 → 1.27.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 (48) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/tests/locale_completeness.test.ts +2 -3
  5. package/src/tests/seo_translation_completeness.test.ts +69 -0
  6. package/src/tests/tool_validation.test.ts +2 -2
  7. package/src/tool/drywallCalculator/i18n/de.ts +1 -1
  8. package/src/tool/drywallCalculator/i18n/id.ts +2 -2
  9. package/src/tool/drywallCalculator/i18n/it.ts +2 -2
  10. package/src/tool/drywallCalculator/i18n/ja.ts +1 -1
  11. package/src/tool/drywallCalculator/i18n/ko.ts +1 -1
  12. package/src/tool/drywallCalculator/i18n/nl.ts +2 -2
  13. package/src/tool/drywallCalculator/i18n/pl.ts +3 -3
  14. package/src/tool/drywallCalculator/i18n/pt.ts +3 -3
  15. package/src/tool/drywallCalculator/i18n/ru.ts +2 -2
  16. package/src/tool/drywallCalculator/i18n/sv.ts +3 -3
  17. package/src/tool/drywallCalculator/i18n/tr.ts +3 -3
  18. package/src/tool/drywallCalculator/i18n/zh.ts +3 -3
  19. package/src/tool/excavationVolumeCalculator/bibliography.astro +6 -0
  20. package/src/tool/excavationVolumeCalculator/bibliography.ts +12 -0
  21. package/src/tool/excavationVolumeCalculator/component.astro +80 -0
  22. package/src/tool/excavationVolumeCalculator/controller.ts +69 -0
  23. package/src/tool/excavationVolumeCalculator/dom-views.ts +63 -0
  24. package/src/tool/excavationVolumeCalculator/entry.ts +26 -0
  25. package/src/tool/excavationVolumeCalculator/evaluator.ts +21 -0
  26. package/src/tool/excavationVolumeCalculator/excavation-volume-calculator.css +442 -0
  27. package/src/tool/excavationVolumeCalculator/i18n/de.ts +120 -0
  28. package/src/tool/excavationVolumeCalculator/i18n/en.ts +120 -0
  29. package/src/tool/excavationVolumeCalculator/i18n/es.ts +120 -0
  30. package/src/tool/excavationVolumeCalculator/i18n/fr.ts +120 -0
  31. package/src/tool/excavationVolumeCalculator/i18n/id.ts +120 -0
  32. package/src/tool/excavationVolumeCalculator/i18n/it.ts +120 -0
  33. package/src/tool/excavationVolumeCalculator/i18n/ja.ts +120 -0
  34. package/src/tool/excavationVolumeCalculator/i18n/ko.ts +120 -0
  35. package/src/tool/excavationVolumeCalculator/i18n/nl.ts +120 -0
  36. package/src/tool/excavationVolumeCalculator/i18n/pl.ts +120 -0
  37. package/src/tool/excavationVolumeCalculator/i18n/pt.ts +120 -0
  38. package/src/tool/excavationVolumeCalculator/i18n/ru.ts +120 -0
  39. package/src/tool/excavationVolumeCalculator/i18n/sv.ts +120 -0
  40. package/src/tool/excavationVolumeCalculator/i18n/tr.ts +120 -0
  41. package/src/tool/excavationVolumeCalculator/i18n/zh.ts +120 -0
  42. package/src/tool/excavationVolumeCalculator/index.ts +11 -0
  43. package/src/tool/excavationVolumeCalculator/logic.test.ts +29 -0
  44. package/src/tool/excavationVolumeCalculator/logic.ts +35 -0
  45. package/src/tool/excavationVolumeCalculator/seo.astro +11 -0
  46. package/src/tool/excavationVolumeCalculator/storage.ts +28 -0
  47. package/src/tool/excavationVolumeCalculator/ui.ts +66 -0
  48. package/src/tools.ts +2 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-diy",
3
- "version": "1.26.0",
3
+ "version": "1.27.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -13,10 +13,11 @@ import { drillCalculator } from '../tool/drillCalculator/entry';
13
13
  import { stairCalculator } from '../tool/stairCalculator/entry';
14
14
  import { drillSharpener } from '../tool/drillSharpener/entry';
15
15
  import { workshopFractionConverter } from '../tool/workshopFractionConverter/entry';
16
+ import { excavationVolumeCalculator } from '../tool/excavationVolumeCalculator/entry';
16
17
 
17
18
  export const diyCategory: DiyCategoryEntry = {
18
19
  icon: 'mdi:hand',
19
- tools: [clayCalculator, epoxyCalculator, balusterCalculator, mortarCalculator, passepartoutCalculator, concreteCalculator, cutOptimizer, voltageDropCalculator, furnitureFit, thermalExpansionCalculator, drillCalculator, stairCalculator, drillSharpener, workshopFractionConverter],
20
+ tools: [clayCalculator, epoxyCalculator, balusterCalculator, mortarCalculator, passepartoutCalculator, concreteCalculator, cutOptimizer, voltageDropCalculator, furnitureFit, thermalExpansionCalculator, drillCalculator, stairCalculator, drillSharpener, workshopFractionConverter, excavationVolumeCalculator],
20
21
  i18n: {
21
22
  de: () => import('./i18n/de').then((m) => m.content),
22
23
  en: () => import('./i18n/en').then((m) => m.content),
package/src/entries.ts CHANGED
@@ -19,6 +19,8 @@ export { pythagoreanRightAngleCalculator } from './tool/pythagoreanRightAngleCal
19
19
  export { twoStrokeMixtureCalculator } from './tool/twoStrokeMixtureCalculator/entry';
20
20
  export { drywallCalculatorEntry } from './tool/drywallCalculator/entry';
21
21
  export type { DrywallCalculatorLocaleContent } from './tool/drywallCalculator/entry';
22
+ export { excavationVolumeCalculator } from './tool/excavationVolumeCalculator/entry';
23
+ export type { ExcavationVolumeCalculatorLocaleContent } from './tool/excavationVolumeCalculator/entry';
22
24
  export { diyCategory } from './category';
23
25
  import { balusterCalculator } from './tool/balusterCalculator/entry';
24
26
  import { clayCalculator } from './tool/clayCalculator/entry';
@@ -27,6 +29,7 @@ import { cutOptimizer } from './tool/cutOptimizer/entry';
27
29
  import { drillCalculator } from './tool/drillCalculator/entry';
28
30
  import { drillSharpener } from './tool/drillSharpener/entry';
29
31
  import { drywallCalculatorEntry } from './tool/drywallCalculator/entry';
32
+ import { excavationVolumeCalculator } from './tool/excavationVolumeCalculator/entry';
30
33
  import { epoxyCalculator } from './tool/epoxyCalculator/entry';
31
34
  import { furnitureFit } from './tool/furnitureFit/entry';
32
35
  import { mortarCalculator } from './tool/mortarCalculator/entry';
@@ -37,4 +40,4 @@ import { voltageDropCalculator } from './tool/voltageDropCalculator/entry';
37
40
  import { workshopFractionConverter } from './tool/workshopFractionConverter/entry';
38
41
  import { pythagoreanRightAngleCalculator } from './tool/pythagoreanRightAngleCalculator/entry';
39
42
  import { twoStrokeMixtureCalculator } from './tool/twoStrokeMixtureCalculator/entry';
40
- export const ALL_ENTRIES = [balusterCalculator, clayCalculator, concreteCalculator, cutOptimizer, drillCalculator, drillSharpener, drywallCalculatorEntry, epoxyCalculator, furnitureFit, mortarCalculator, passepartoutCalculator, stairCalculator, thermalExpansionCalculator, voltageDropCalculator, workshopFractionConverter, pythagoreanRightAngleCalculator, twoStrokeMixtureCalculator];
43
+ export const ALL_ENTRIES = [balusterCalculator, clayCalculator, concreteCalculator, cutOptimizer, drillCalculator, drillSharpener, drywallCalculatorEntry, epoxyCalculator, furnitureFit, mortarCalculator, passepartoutCalculator, stairCalculator, thermalExpansionCalculator, voltageDropCalculator, workshopFractionConverter, pythagoreanRightAngleCalculator, twoStrokeMixtureCalculator, excavationVolumeCalculator];
@@ -33,8 +33,7 @@ describe('Locale Completeness Validation', () => {
33
33
  });
34
34
  });
35
35
 
36
- it('all 17 tools registered', () => {
37
- expect(ALL_TOOLS.length).toBe(17);
36
+ it('all 18 tools registered', () => {
37
+ expect(ALL_TOOLS.length).toBe(18);
38
38
  });
39
39
  });
40
-
@@ -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
+
@@ -4,8 +4,8 @@ import { diyCategory } from '../data';
4
4
 
5
5
  describe('Tool Validation Suite', () => {
6
6
  describe('Library Registration', () => {
7
- it('should have 17 tools in ALL_TOOLS', () => {
8
- expect(ALL_TOOLS.length).toBe(17);
7
+ it('should have 18 tools in ALL_TOOLS', () => {
8
+ expect(ALL_TOOLS.length).toBe(18);
9
9
  });
10
10
 
11
11
  it('diyCategory should be defined', () => {
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Planen Sie stets <strong>10% Verschnittreserve</strong> für Profile und Platten ein. Für die Fugenverspachtelung werden ca. 0,8 kg Spachtelmasse pro Quadratmeter Plattenfläche zusammen mit Fugendeckstreifen benötigt.',
114
+ html: 'Planen Sie stets <strong>10% Verschnittreserve</strong> für Profile und Platten ein, um Ausgleiche bei Türausschnitten, Fensterleibungen und Deckenanschlüssen abzudecken. Für die fachgerechte Fugenverspachtelung der Verspachtelungsstufen Q1 bis Q4 werden ca. 0,8 kg Fugenspachtelmasse pro Quadratmeter Plattenfläche zusammen mit passenden Glasfaser- oder Papier-Fugendeckstreifen zur Vermeidung von Spannungsrissen benötigt.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'Pemasangan dinding partisi ringan papan gypsum memerlukan perhitungan matang pada rangka hollow baja galvanis (stud dan runner) serta pembagian sambungan papan. Penataan rangka yang tepat mencegah lendutan dan retak pada sambungan.',
90
+ html: 'Pemasangan dinding partisi ringan papan gypsum memerlukan perhitungan teknis yang matang pada rangka hollow baja galvanis (stud vertikal CW dan runner horizontal UW) serta pembagian layout susunan lembaran papan. Sesuai dengan standar teknis konstruksi, penataan rangka yang presisi mencegah lendutan struktur dinding, kerusakan lendut, dan retak pada bagian sambungan antar papan.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Selalu tambahkan margin <strong>10% faktor sisa</strong> untuk mengantisipasi potongan sekitar jendela dan pintu. Pengolahan sambungan membutuhkan sekitar 0.8 kg kompon per meter persegi permukaan papan.',
114
+ html: 'Selalu tambahkan margin <strong>10% faktor sisa pemotongan</strong> untuk mengantisipasi potongan sekitar celah jendela, bukaan pintu, dan sudut plafon. Untuk penyelesaian sambungan permukaan (tingkat finishing Q1 hingga Q4), dibutuhkan sekitar 0.8 kg kompon perekat papan gypsum per meter persegi permukaan papan beserta pita kasa penguat sambungan.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'La costruzione di sistemi a secco in cartongesso richiede il calcolo accurato dell\'orditura metallica in acciaio zincato (montanti C e guide U) e la sfalsatura dei giunti secondo le norme EN 520 per evitare fessurazioni.',
90
+ html: 'La costruzione di sistemi a secco con lastre di cartongesso richiede il calcolo accurato dell\'orditura metallica in acciaio zincato (montanti verticali C e guide orizzontali U) e la corretta sfalsatura dei giunti secondo le normative EN 520 per garantire la stabilita ed evitare fessurazioni nel tempo.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Considera sempre un <strong>10% di margine per gli sfridi</strong> di taglio. La stuccatura dei giunti richiede circa 0.8 kg di stucco in polvere o pronto all\'uso per metro quadrato di superficie.',
114
+ html: 'Considera sempre un <strong>10% di margine per gli sfridi</strong> di taglio per compensare gli adattamenti attorno a porte, finestre e spigoli di soffitto. La stuccatura professionale dei giunti (per i livelli di finitura da Q1 a Q4) richiede circa 0.8 kg di stucco specifico per cartongesso per metro quadrato di superficie, unitamente al nastro coprigiunto in fibra di vetro o carta rinforzata.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: '開口部や端部の切りしろとして<strong>10%のロス分</strong>を見込みます。目地処理には1平米あたり約0.8kgの目地パテとジョイントテープを使用します。',
114
+ html: 'ドア開口部や窓枠周り、天井取り合い部分での切り落としロスに対応するため、常に<strong>10%のロス率</strong>を余分に見込んで材料を手配します。継ぎ目処理(Q1〜Q4の下地仕上げ処理)にはボード施工面積1 square meterあたり約0.8kgの目地パテと、ひび割れ防止用のグラスファイバー製またはペーパー製ジョイントテープが必要となります。',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: '문틀 및 창호 개구부 절단 자재 손실을 고려하여 <strong>10% 자재 할증률</strong>을 반영합니다. 줄눈 퍼티는 m2당 약 0.8kg 소요됩니다.',
114
+ html: '문틀 및 창호 개구부, 천장 접합부의 절단 자재 손실에 대비하여 항상 <strong>10%의 자재 할증률</strong>을 추가로 계산하여 자재를 발주합니다. 석고보드 면적 1m2당 약 0.8kg 이음매 전용 퍼티 마감재와 균열 방지용 유리섬유 테이프 또는 종이 망사 테이프가 소요됩니다.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'Het bouwen van lichte scheidingswanden met gipsplaten vereist het nauwkeurig berekenen van het stalen frame (C-staanders en U-liggers) en het verspringend aanbrengen van de plaatvoegen volgens de norm EN 520.',
90
+ html: 'Het bouwen van lichte scheidingswanden met gipsplaten vereist het nauwkeurig berekenen van het gegalvaniseerde stalen frame (C-staanders en U-liggers) en het verspringend aanbrengen van de plaatvoegen volgens de Europese norm EN 520 om wanddoorbuiging en scheurvorming op de naden te voorkomen.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Reken altijd <strong>10% snijverlies</strong> voor profielen en platen. Het afvoegen kost ca. 0,8 kg voegpasta per vierkante meter wandopslag.',
114
+ html: 'Reken altijd een <strong>10% marge voor snijverlies</strong> voor profielen en platen om zaagafval bij deursparingen, raamuitsparingen en plafondaansluitingen op te vangen. Voor het professioneel afvoegen van de platen (voegafwerkingsniveau Q1 tot Q4) is circa 0,8 kg voegpasta per vierkante meter plaatoppervlak nodig, samen met papieren of glasvezel voegband om scheurvorming te voorkomen.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'Budowa lekkich scian dzialowych w systemie suchej zabudowy wymaga dokladnego przeliczenia stelasza z profili stalowych (CW i UW) oraz mijankowego ukladu spoin zgodnie z norma PN-EN 520.',
90
+ html: 'Budowa lekkich scian dzialowych w systemie suchej zabudowy z plyt gipsowo-kartonowych wymaga dokladnego przeliczenia stelasza z ocynkowanych profili stalowych (pionowych CW i poziomych UW) oraz mijankowego ukladu spoin zgodnie z normą PN-EN 520 w celu zapobiegania ugieciom i peknieciom.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: '<strong>Rozstaw 60 cm</strong> to standard pasujacy do plyt o szerokosci 120 cm. <strong>Rozstaw 40 cm</strong> jest wymagany pod glazure i przy wysokosciach scian powyzej 3,0 m.',
99
+ html: 'Osiowy rozstaw profili pionowych decyduje o sztywnosci i nośnosci calej sciany. <strong>Standardowy rozstaw 60 cm</strong> idealnie pasuje do plyt gipsowych o szerokosci 120 cm. Z kolei <strong>zagęszczony rozstaw 40 cm</strong> jest bezwzglednie wymagany pod ciezki montaz glazury i plytek ceramicznych, przy wysokosciach scian przekraczajacych 3,0 metry oraz w pomieszczeniach o podwyzszonych wymaganiach akustycznych.',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Dolicz <strong>10% zapasu na straty</strong> na dociecia przy drzwiach i oknach. Szpachlowanie spoin zuzywa ok. 0.8 kg masy na metr kwadratowy oplytowania.',
114
+ html: 'Zawsze doliczaj <strong>10% zapasu na straty</strong> na dociecia przy glifach drzwiowych, otworach okiennych oraz skosach sufitych. Do profesjonalnego szpachlowania spoin w standardzie od Q1 do Q4 zuzywa sie okolo 0.8 kg masy szpachlowej do plyt gipsowo-kartonowych na metr kwadratowy powierzchni wraz z tasmą zbrojacą z wlokna szklanego lub papierową.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'A construção de vedações verticais leves em drywall exige o dimensionamento correto dos perfis metálicos em aço galvanizado (montantes C e guias U) e o amarramento das juntas segundo a norma EN 520.',
90
+ html: 'A construção de vedações verticais leves em drywall exige o dimensionamento preciso e criterioso dos perfis metálicos em aço galvanizado (montantes verticais C e guias horizontais U) e o amarramento de juntas segundo as diretrizes da norma EN 520 para evitar flexões estruturais e deformações.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: 'O <strong>espaçamento padrão de 60 cm</strong> atende placas de 120 cm de largura. O <strong>espaçamento de 40 cm</strong> é obrigatório sob revestimentos cerâmicos e alturas superiores a 3,00 metros.',
99
+ html: 'O espaçamento dos montantes metálicos determina diretamente a rigidez e a capacidade de carga da parede. O <strong>espaçamento padrão de 60 cm</strong> é o mais utilizado para fixação de placas gesso acartonado com 120 cm de largura. Por outro lado, o <strong>espaçamento de 40 cm</strong> torna-se estritamente obrigatório em superfícies que receberão revestimentos cerâmicos ou porcelanatos, em paredes com pé-direito superior a 3,00 metros, bem como em divisórias com exigências acústicas elevadas.',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Considere <strong>10% de margem de perda</strong> para cortes em esquadrias. O tratamento de juntas consome cerca de 0,8 kg de massa por metro quadrado de superfície.',
114
+ html: 'Considere sempre uma <strong>margem de perda de 10%</strong> para recortes e ajustes em esquadrias de portas, janelas e cantos de forro. O tratamento profissional de juntas (para níveis de acabamento Q1 a Q4) consome cerca de 0,8 kg de massa para drywall por metro quadrado de superfície, juntamente com fita de fita de papel reforçada ou fibra de vidro para evitar trincas.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: '<strong>Стандартный шаг 60 см</strong> соответствует ширине листов 120 см. <strong>Шаг 40 см</strong> обязателен при облицовке тяжелой плиткой и высотах стен более 3.0 метров.',
99
+ html: 'Осевой шаг стоечных профилей напрямую определяет жесткость и несущую способность конструкции. <strong>Стандартный шаг 60 см</strong> идеально подходит под ширину листов 120 см. При этом <strong>усиленный шаг 40 см</strong> является строго обязательным под облицовку тяжелой керамической плиткой, при высоте перегородок более 3.0 метров, а также в помещениях с повышенными требованиями к звукоизоляции.',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Всегда учитывайте <strong>10% запас на обрезку</strong> вокруг проемов. Заделка швов требует около 0.8 кг шпаклевочной смеси на квадратный метр.',
114
+ html: 'Всегда учитывайте <strong>10% запас на обрезку</strong> профилей и листов для компенсации подрезки вокруг дверных откосов, оконных проемов и примыканий к потолку. Профессиональная заделка стыков (для качественной подготовки поверхностей от Q1 до Q4) требует около 0.8 кг специализированной шпаклевки для гипсокартона на квадратный метр площади вместе со стеклохолстом или армирующей бумажной лентой.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'Att bygga latta innervaggar med gipsskivor kraver exakt dimensionering av galvaniserade stålprofiler (reglar och skenor) samt forskjutning av skivskarvar enligt EN 520.',
90
+ html: 'Att bygga lätta innerväggar med gipsskivor kräver exakt teknisk dimensionering av galvaniserade stålprofiler (vertikala C-reglar och horisontella U-skenor) samt förskjutning av skivskarvar enligt EN 520 för att undvika väggnedböjning och sprickor.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: '<strong>Standardavstand cc 60 cm</strong> anpassas till skivbredd 120 cm. <strong>Cc 40 cm</strong> ar obligatoriskt under kakel och vid hojder over 3,0 m.',
99
+ html: 'Regelavståndet bestämmer väggens stabilitet och bärförmåga. <strong>Standardavstånd cc 60 cm</strong> anpassas till skivbredd 120 cm. <strong>Cc 40 cm</strong> är obligatoriskt vid tung kakelsättning eller porslinsklinker, vid vägghöjder över 3,0 meter samt i utrymmen med förhöjda ljudisoleringskrav.',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Lagg alltid till <strong>10% spillmarginal</strong> for kapningar vid dorrar och fonster. Skarvspackling fordtar ca 0.8 kg spackel per kvadratmeter.',
114
+ html: 'Räkna alltid med en generös <strong>spillmarginal på 10%</strong> för kapningar vid dörröppningar, fönstersmygar och takanslutningar. För professionell skarvspackling av gipsskivorna (från ytfinishsteg Q1 till Q4) krävs cirka 0,8 kg gipsbaserat skarvspackel per kvadratmeter skivyta samt pappers- eller glasfiberremsa för att undvika alla former av sprickbildning.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: 'Hafif bölme duvarların yapımında galvanizli çelik profillerin (C dikme ve U ray) doğru boyutlandırılması ve ek yerlerinin şaşırtmalı yerleştirilmesi TS EN 520 standartlarına göre çatlamaları önler.',
90
+ html: 'Hafif bölme duvarların yapımında galvanizli çelik profillerin (dikey C dikme ve yatay U ray) doğru mühendislik hesaplarıyla boyutlandırılması ve ek yerlerinin şaşırtmalı yerleştirilmesi TS EN 520 standartlarına göre duvar sehimini ve çatlamaları önler.',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: '<strong>60 cm eksen aralığı</strong> 120 cm genişliğindeki plakalar için standarttır. Seramik kaplama altında ve 3.0 metreden yüksek duvarlarda <strong>40 cm eksen aralığı</strong> zorunludur.',
99
+ html: 'Dikme aks aralığı duvarın taşıyıcı rijitliğini belirler. <strong>60 cm eksen aralığı</strong> 120 cm genişliğindeki alçı plakalar için standarttır. Ağır seramik kaplama altında, 3.0 metreden yüksek duvarlarda ve yüksek ses yalıtımı gereken hacimlerde <strong>40 cm eksen aralığı</strong> sıkılaştırması zorunludur.',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: 'Kapı ve pencere boşluklarındaki kesimler için her zaman <strong>%10 fire payı</strong> ekleyin. Derz dolgusu için metrekare başına yaklaşık 0.8 kg alçı bazlı derz macunu gereklidir.',
114
+ html: 'Kapı ve pencere boşluklarındaki kesim kayıpları ile tavan birleşim detayları için her zaman <strong>%10 fire payı</strong> ekleyerek malzeme siparişi verin. Derzlerin profesyonel dolgusu ve yüzey hazırlığı için (Q1 ile Q4 yüzey kalitesi standartlarında) metrekare başına yaklaşık 0.8 kg alçı bazlı derz macunu ve çatlamayı önleyen derz bandı kullanılır.',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -87,7 +87,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
90
- html: '轻钢龙骨隔墙施工中,镀锌钢板C型竖向龙骨与U型沿顶沿地龙骨的合理布置及板缝错缝排列是防止挠度变形与接缝开裂的关键。',
90
+ html: '在轻钢龙骨石膏板隔墙系统施工中,根据国家建筑标准规范,镀锌钢板C型竖向龙骨与U型沿顶沿地龙骨的精准尺寸计算、受力分析以及板缝交错排列是防止墙体结构挠度变形、失稳及接缝处表面开裂的核心技术保障。',
91
91
  },
92
92
  {
93
93
  type: 'title',
@@ -96,7 +96,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
99
- html: '<strong>60cm间距</strong>是120cm宽石膏板的标准配置。贴饰面砖或墙高大于3.0米时必须采用<strong>40cm间距</strong>。',
99
+ html: '龙骨的中心间距直接决定了整体隔墙的刚度与抗冲击承载能力。<strong>60cm轴线间距</strong>是适配120cm标准宽度石膏板的最常用方案;而在粘贴重型瓷砖石材面层、墙体净高超过3.0米或对隔音性能有较高要求的空间中,则必须采取<strong>40cm间距</strong>密铺加固。',
100
100
  },
101
101
  {
102
102
  type: 'card',
@@ -111,7 +111,7 @@ export const content: ToolLocaleContent<DrywallCalculatorUI> = {
111
111
  },
112
112
  {
113
113
  type: 'paragraph',
114
- html: '门窗洞口裁切需预留<strong>10%损耗率</strong>。接缝处理每平方米约消耗0.8kg嵌缝腻子。',
114
+ html: '考虑到门窗洞口裁切、墙体边角修剪以及天花板接缝等处的材料损耗,在采购石膏板和龙骨时请务必预留<strong>10%损耗率</strong>。在进行接缝嵌缝抹灰与表面修飾处理(Q1级至Q4级接缝处理标准)时,每平方米石膏板抹灰面积约需消耗0.8kg石膏板专用嵌缝腻子,并配合玻璃纤维网格布或防裂纸带进行防裂加固。',
115
115
  },
116
116
  ],
117
117
  ui: {
@@ -0,0 +1,6 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { excavationVolumeCalculatorBibliography } from './bibliography';
4
+ ---
5
+
6
+ <SharedBibliography links={excavationVolumeCalculatorBibliography} />
@@ -0,0 +1,12 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const excavationVolumeCalculatorBibliography: BibliographyEntry[] = [
4
+ {
5
+ name: 'FHWA Geotechnical Aspects of Pavements Reference Manual Chapter 5',
6
+ url: 'https://www.fhwa.dot.gov/engineering/geotech/pubs/05037/05a.cfm',
7
+ },
8
+ {
9
+ name: 'FHWA Technical Manual for Design and Construction of Road Tunnels Civil Elements',
10
+ url: 'https://www.fhwa.dot.gov/bridge/tunnel/pubs/nhi09010/tunnel_manual.pdf',
11
+ },
12
+ ];
@@ -0,0 +1,80 @@
1
+ ---
2
+ import './excavation-volume-calculator.css';
3
+ import type { ExcavationCalculatorUI } from './ui';
4
+
5
+ interface Props {
6
+ ui?: Record<string, unknown>;
7
+ }
8
+
9
+ const { ui } = Astro.props;
10
+ const t = (ui ?? {}) as ExcavationCalculatorUI;
11
+ ---
12
+
13
+ <div class="ev-root" data-ev-root data-ev-ui={JSON.stringify(t)}>
14
+ <div class="ev-board">
15
+ <div class="ev-intro">
16
+ <div class="ev-intro-mark" aria-hidden="true">∿</div>
17
+ <p>{t.onboarding}</p>
18
+ </div>
19
+
20
+ <section class="ev-scene-panel" aria-labelledby="ev-scene-title">
21
+ <div class="ev-result-heading"><span class="ev-kicker" id="ev-scene-title">{t.sceneTitle}</span><span class="ev-status" data-ev-status>{t.soilStatusLabel}: {t.soilStatusMedium}</span></div>
22
+ <div class="ev-scene" data-ev-scene></div>
23
+ <div class="ev-metrics">
24
+ <div class="ev-metric ev-metric-main"><span class="ev-metric-label">{t.bankVolumeLabel}</span><strong class="ev-metric-value"><span data-ev-bank>9.98</span> <span class="ev-unit-suffix" data-ev-volume-unit>m³</span></strong><span class="ev-metric-help">{t.bankVolumeHelp}</span></div>
25
+ <div class="ev-metric"><span class="ev-metric-label">{t.looseVolumeLabel}</span><strong class="ev-metric-value"><span data-ev-loose-low>11.48</span> to <span data-ev-loose-high>12.47</span> <span class="ev-unit-suffix" data-ev-volume-unit>m³</span></strong><span class="ev-metric-help">{t.looseVolumeHelp}</span></div>
26
+ </div>
27
+ <div class="ev-expansion"><span>{t.expansionLabel}</span><strong data-ev-expansion>15 to 25%</strong></div>
28
+ </section>
29
+
30
+ <div class="ev-workbench">
31
+ <section class="ev-control-panel" aria-labelledby="ev-dimensions-title">
32
+ <div class="ev-control-heading">
33
+ <span class="ev-kicker" id="ev-dimensions-title">{t.dimensionLabel}</span>
34
+ <div class="ev-segmented" role="group" aria-label={t.unitSystemLabel}>
35
+ <button type="button" class="ev-active" data-ev-unit="metric">{t.unitMetric}</button>
36
+ <button type="button" data-ev-unit="imperial">{t.unitImperial}</button>
37
+ </div>
38
+ </div>
39
+
40
+ <fieldset class="ev-fieldset">
41
+ <legend>{t.dimensionLabel}</legend>
42
+ <div class="ev-dimensions">
43
+ <div class="ev-field">
44
+ <label for="ev-length">{t.lengthLabel}</label>
45
+ <div class="ev-input"><input id="ev-length" data-ev-dimension="length" type="number" min="0" step="0.01" value="4.8" inputmode="decimal" /><span class="ev-unit" data-ev-unit-label>m</span></div>
46
+ </div>
47
+ <div class="ev-field">
48
+ <label for="ev-width">{t.widthLabel}</label>
49
+ <div class="ev-input"><input id="ev-width" data-ev-dimension="width" type="number" min="0" step="0.01" value="3.2" inputmode="decimal" /><span class="ev-unit" data-ev-unit-label>m</span></div>
50
+ </div>
51
+ <div class="ev-field">
52
+ <label for="ev-depth">{t.depthLabel}</label>
53
+ <div class="ev-input"><input id="ev-depth" data-ev-dimension="depth" type="number" min="0" step="0.01" value="0.65" inputmode="decimal" /><span class="ev-unit" data-ev-unit-label>m</span></div>
54
+ </div>
55
+ </div>
56
+ </fieldset>
57
+
58
+ <fieldset class="ev-fieldset ev-soil-fieldset">
59
+ <legend>{t.soilLabel}</legend>
60
+ <div class="ev-soil-grid">
61
+ <button type="button" class="ev-soil-button" data-ev-soil="sand"><span class="ev-soil-name">{t.soilSand}</span><span class="ev-soil-band">{t.soilSandBand}</span></button>
62
+ <button type="button" class="ev-soil-button ev-active" data-ev-soil="topsoil"><span class="ev-soil-name">{t.soilTopsoil}</span><span class="ev-soil-band">{t.soilTopsoilBand}</span></button>
63
+ <button type="button" class="ev-soil-button" data-ev-soil="clay"><span class="ev-soil-name">{t.soilClay}</span><span class="ev-soil-band">{t.soilClayBand}</span></button>
64
+ <button type="button" class="ev-soil-button" data-ev-soil="gravel"><span class="ev-soil-name">{t.soilGravel}</span><span class="ev-soil-band">{t.soilGravelBand}</span></button>
65
+ </div>
66
+ </fieldset>
67
+ </section>
68
+ </div>
69
+ <div class="ev-notes"><p class="ev-interpretation"><strong>{t.interpretationTitle}</strong> {t.interpretationText}</p><p class="ev-warning">{t.warning}</p></div>
70
+ </div>
71
+ </div>
72
+
73
+ <script>
74
+ import { mountExcavationCalculator } from './controller';
75
+ import type { ExcavationCalculatorUI } from './ui';
76
+
77
+ const root = document.querySelector('[data-ev-root]');
78
+ const ui = JSON.parse(root?.getAttribute('data-ev-ui') || '{}') as ExcavationCalculatorUI;
79
+ if (root) mountExcavationCalculator(root, ui);
80
+ </script>
@@ -0,0 +1,69 @@
1
+ import { calculateExcavation, convertDimension } from './logic';
2
+ import { renderResults } from './dom-views';
3
+ import { readExcavationState, writeExcavationState } from './storage';
4
+ import type { ExcavationCalculatorUI, ExcavationInput, ExcavationUnitSystem, SoilProfile } from './ui';
5
+
6
+ const initialState: ExcavationInput = { unitSystem: 'metric', length: 4.8, width: 3.2, depth: 0.65, soil: 'topsoil' };
7
+
8
+ export function mountExcavationCalculator(root: Element, ui: ExcavationCalculatorUI): void {
9
+ const state = readExcavationState(initialState);
10
+ setControls(root, state);
11
+ bindUnitButtons(root, state, ui);
12
+ bindDimensionInputs(root, state, ui);
13
+ bindSoilButtons(root, state, ui);
14
+ render(root, state, ui);
15
+ }
16
+
17
+ function bindUnitButtons(root: Element, state: ExcavationInput, ui: ExcavationCalculatorUI): void {
18
+ root.querySelectorAll('[data-ev-unit]').forEach((button) => button.addEventListener('click', () => {
19
+ const next = (button as HTMLElement).dataset.evUnit as ExcavationUnitSystem;
20
+ if (next === state.unitSystem) return;
21
+ ['length', 'width', 'depth'].forEach((key) => {
22
+ const value = state[key as keyof ExcavationInput] as number;
23
+ state[key as 'length' | 'width' | 'depth'] = convertDimension(value, state.unitSystem, next);
24
+ });
25
+ state.unitSystem = next;
26
+ setControls(root, state);
27
+ render(root, state, ui);
28
+ }));
29
+ }
30
+
31
+ function bindDimensionInputs(root: Element, state: ExcavationInput, ui: ExcavationCalculatorUI): void {
32
+ root.querySelectorAll<HTMLInputElement>('[data-ev-dimension]').forEach((input) => input.addEventListener('input', () => {
33
+ const key = input.dataset.evDimension as 'length' | 'width' | 'depth';
34
+ state[key] = Number(input.value) || 0;
35
+ render(root, state, ui);
36
+ }));
37
+ }
38
+
39
+ function bindSoilButtons(root: Element, state: ExcavationInput, ui: ExcavationCalculatorUI): void {
40
+ root.querySelectorAll('[data-ev-soil]').forEach((button) => button.addEventListener('click', () => {
41
+ state.soil = (button as HTMLElement).dataset.evSoil as SoilProfile;
42
+ setControls(root, state);
43
+ render(root, state, ui);
44
+ }));
45
+ }
46
+
47
+
48
+ function setControls(root: Element, state: ExcavationInput): void {
49
+ root.querySelectorAll('[data-ev-unit]').forEach((button) => button.classList.toggle('ev-active', (button as HTMLElement).dataset.evUnit === state.unitSystem));
50
+ root.querySelectorAll<HTMLInputElement>('[data-ev-dimension]').forEach((input) => {
51
+ const key = input.dataset.evDimension as 'length' | 'width' | 'depth';
52
+ input.value = String(roundInput(state[key]));
53
+ });
54
+ root.querySelectorAll('[data-ev-soil]').forEach((button) => button.classList.toggle('ev-active', (button as HTMLElement).dataset.evSoil === state.soil));
55
+ root.querySelectorAll('[data-ev-unit-label]').forEach((element) => { element.textContent = state.unitSystem === 'metric' ? 'm' : 'ft'; });
56
+ root.querySelectorAll('[data-ev-volume-unit]').forEach((element) => { element.textContent = state.unitSystem === 'metric' ? 'm³' : 'ft³'; });
57
+ }
58
+
59
+ function render(root: Element, state: ExcavationInput, ui: ExcavationCalculatorUI): void {
60
+ const output = calculateExcavation(state);
61
+ const system = state.unitSystem;
62
+ renderResults(root, output, state.soil, ui);
63
+ writeExcavationState(state);
64
+ root.dataset.evSystem = system;
65
+ }
66
+
67
+ function roundInput(value: number): number {
68
+ return Math.round(value * 100) / 100;
69
+ }