@jjlmoya/utils-diy 1.25.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.
- package/package.json +1 -1
- package/src/category/index.ts +2 -1
- package/src/entries.ts +4 -1
- package/src/tests/locale_completeness.test.ts +2 -3
- package/src/tests/seo_translation_completeness.test.ts +69 -0
- package/src/tests/spanish_leakage.test.ts +5 -1
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/drywallCalculator/i18n/de.ts +1 -1
- package/src/tool/drywallCalculator/i18n/id.ts +2 -2
- package/src/tool/drywallCalculator/i18n/it.ts +2 -2
- package/src/tool/drywallCalculator/i18n/ja.ts +1 -1
- package/src/tool/drywallCalculator/i18n/ko.ts +1 -1
- package/src/tool/drywallCalculator/i18n/nl.ts +2 -2
- package/src/tool/drywallCalculator/i18n/pl.ts +3 -3
- package/src/tool/drywallCalculator/i18n/pt.ts +3 -3
- package/src/tool/drywallCalculator/i18n/ru.ts +2 -2
- package/src/tool/drywallCalculator/i18n/sv.ts +3 -3
- package/src/tool/drywallCalculator/i18n/tr.ts +3 -3
- package/src/tool/drywallCalculator/i18n/zh.ts +3 -3
- package/src/tool/excavationVolumeCalculator/bibliography.astro +6 -0
- package/src/tool/excavationVolumeCalculator/bibliography.ts +12 -0
- package/src/tool/excavationVolumeCalculator/component.astro +80 -0
- package/src/tool/excavationVolumeCalculator/controller.ts +69 -0
- package/src/tool/excavationVolumeCalculator/dom-views.ts +63 -0
- package/src/tool/excavationVolumeCalculator/entry.ts +26 -0
- package/src/tool/excavationVolumeCalculator/evaluator.ts +21 -0
- package/src/tool/excavationVolumeCalculator/excavation-volume-calculator.css +442 -0
- package/src/tool/excavationVolumeCalculator/i18n/de.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/en.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/es.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/fr.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/id.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/it.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/ja.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/ko.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/nl.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/pl.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/pt.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/ru.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/sv.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/tr.ts +120 -0
- package/src/tool/excavationVolumeCalculator/i18n/zh.ts +120 -0
- package/src/tool/excavationVolumeCalculator/index.ts +11 -0
- package/src/tool/excavationVolumeCalculator/logic.test.ts +29 -0
- package/src/tool/excavationVolumeCalculator/logic.ts +35 -0
- package/src/tool/excavationVolumeCalculator/seo.astro +11 -0
- package/src/tool/excavationVolumeCalculator/storage.ts +28 -0
- package/src/tool/excavationVolumeCalculator/ui.ts +66 -0
- package/src/tools.ts +2 -1
package/package.json
CHANGED
package/src/category/index.ts
CHANGED
|
@@ -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];
|
|
@@ -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
|
+
|
|
@@ -67,12 +67,16 @@ function normalize(value: string): string {
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
function isTechnicalInvariant(text: string): boolean {
|
|
70
|
+
const ledUnitMatches = text.match(/\b\d+(?:-\d+)?\s*(?:w|lm)\b/g) ?? [];
|
|
70
71
|
return [
|
|
71
72
|
'data:image/svg+xml;base64',
|
|
72
73
|
'background-image: url',
|
|
73
74
|
'.layout-playground {',
|
|
74
75
|
'const samplerate =',
|
|
75
|
-
].some((pattern) => text.includes(pattern)) ||
|
|
76
|
+
].some((pattern) => text.includes(pattern)) ||
|
|
77
|
+
(text.includes('presets') && text.includes('hz')) ||
|
|
78
|
+
(text.includes('t_rectal') && text.includes('exp(-k * t)')) ||
|
|
79
|
+
(text.includes('led') && ledUnitMatches.length >= 3);
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
function collectString(value: string, output: string[]): void {
|
|
@@ -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
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: '
|
|
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: '문틀 및 창호
|
|
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.
|
|
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
|
|
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>
|
|
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: '
|
|
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
|
|
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>
|
|
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>
|
|
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>
|
|
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> вокруг
|
|
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
|
|
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>
|
|
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: '
|
|
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.
|
|
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
|
|
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: '
|
|
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: '
|
|
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: '
|
|
114
|
+
html: '考虑到门窗洞口裁切、墙体边角修剪以及天花板接缝等处的材料损耗,在采购石膏板和龙骨时请务必预留<strong>10%损耗率</strong>。在进行接缝嵌缝抹灰与表面修飾处理(Q1级至Q4级接缝处理标准)时,每平方米石膏板抹灰面积约需消耗0.8kg石膏板专用嵌缝腻子,并配合玻璃纤维网格布或防裂纸带进行防裂加固。',
|
|
115
115
|
},
|
|
116
116
|
],
|
|
117
117
|
ui: {
|
|
@@ -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
|
+
}
|