@jjlmoya/utils-home 1.36.0 → 1.37.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/scripts/fix_ru.mjs +79 -0
- package/src/layouts/PreviewLayout.astro +1 -0
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tool/applianceCostCalculator/i18n/pl.ts +53 -53
- package/src/tool/applianceCostCalculator/i18n/pt.ts +48 -48
- package/src/tool/heatingComparator/heating-consumption-comparator.css +89 -4
- package/src/tool/humidityCalculator/i18n/sv.ts +57 -57
- package/src/tool/projectorCalculator/i18n/es.ts +1 -1
- package/src/tool/tileLayoutCalculator/tile-layout-calculator.css +90 -5
- package/src/tool/wifiRangeSimulator/i18n/ru.ts +212 -212
package/package.json
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
|
|
3
|
+
function toCyrillic(text) {
|
|
4
|
+
let result = '';
|
|
5
|
+
let i = 0;
|
|
6
|
+
|
|
7
|
+
while (i < text.length) {
|
|
8
|
+
const c4 = text.slice(i, i + 4);
|
|
9
|
+
if (c4.length === 4) {
|
|
10
|
+
const t = { 'shch':'щ', 'SHCH':'Щ', 'Shch':'Щ' };
|
|
11
|
+
if (t[c4]) { result += t[c4]; i += 4; continue; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (i + 2 <= text.length) {
|
|
15
|
+
const two = text.slice(i, i + 2);
|
|
16
|
+
|
|
17
|
+
if (two === 'ya' || two === 'YA' || two === 'Ya') { result += ({'ya':'я','YA':'Я','Ya':'Я'})[two]; i += 2; continue; }
|
|
18
|
+
if (two === 'ye' || two === 'YE' || two === 'Ye') { result += ({'ye':'е','YE':'Е','Ye':'Е'})[two]; i += 2; continue; }
|
|
19
|
+
if (two === 'yo' || two === 'YO' || two === 'Yo') { result += ({'yo':'ё','YO':'Ё','Yo':'Ё'})[two]; i += 2; continue; }
|
|
20
|
+
if (two === 'yu' || two === 'YU' || two === 'Yu') { result += ({'yu':'ю','YU':'Ю','Yu':'Ю'})[two]; i += 2; continue; }
|
|
21
|
+
|
|
22
|
+
if (two === 'zh' || two === 'ZH' || two === 'Zh') { result += ({'zh':'ж','ZH':'Ж','Zh':'Ж'})[two]; i += 2; continue; }
|
|
23
|
+
if (two === 'ch' || two === 'CH' || two === 'Ch') { result += ({'ch':'ч','CH':'Ч','Ch':'Ч'})[two]; i += 2; continue; }
|
|
24
|
+
if (two === 'sh' || two === 'SH' || two === 'Sh') { result += ({'sh':'ш','SH':'Ш','Sh':'Ш'})[two]; i += 2; continue; }
|
|
25
|
+
if (two === 'kh' || two === 'KH' || two === 'Kh') { result += ({'kh':'х','KH':'Х','Kh':'Х'})[two]; i += 2; continue; }
|
|
26
|
+
if (two === 'ts' || two === 'TS' || two === 'Ts') { result += ({'ts':'ц','TS':'Ц','Ts':'Ц'})[two]; i += 2; continue; }
|
|
27
|
+
|
|
28
|
+
const next = text[i + 2] || '';
|
|
29
|
+
if (two === 'iy' && next !== 'a' && next !== 'e' && next !== 'o' && next !== 'u') { result += 'ий'; i += 2; continue; }
|
|
30
|
+
if (two === 'yy' && next !== 'a' && next !== 'e' && next !== 'o' && next !== 'u') { result += 'ый'; i += 2; continue; }
|
|
31
|
+
if (two === 'oy' && next !== 'a' && next !== 'e' && next !== 'o' && next !== 'u') { result += 'ой'; i += 2; continue; }
|
|
32
|
+
if (two === 'ay' && next !== 'a' && next !== 'e' && next !== 'o' && next !== 'u') { result += 'ай'; i += 2; continue; }
|
|
33
|
+
if (two === 'ey' && next !== 'a' && next !== 'e' && next !== 'o' && next !== 'u') { result += 'ей'; i += 2; continue; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const c = text[i];
|
|
37
|
+
const smap = {
|
|
38
|
+
'A':'А','a':'а','B':'Б','b':'б','V':'В','v':'в','G':'Г','g':'г',
|
|
39
|
+
'D':'Д','d':'д','E':'Э','e':'е','Z':'З','z':'з','I':'И','i':'и',
|
|
40
|
+
'K':'К','k':'к','L':'Л','l':'л','M':'М','m':'м','N':'Н','n':'н',
|
|
41
|
+
'O':'О','o':'о','P':'П','p':'п','R':'Р','r':'р','S':'С','s':'с',
|
|
42
|
+
'T':'Т','t':'т','U':'У','u':'у','F':'Ф','f':'ф','C':'Ц','c':'ц',
|
|
43
|
+
'H':'Х','h':'х','Y':'Ы','y':'ы','J':'Й','j':'й',
|
|
44
|
+
};
|
|
45
|
+
if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
|
|
46
|
+
result += smap[c] || c;
|
|
47
|
+
} else {
|
|
48
|
+
result += c;
|
|
49
|
+
}
|
|
50
|
+
i++;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
result = result.replace(/WиФи/gi, 'WiFi');
|
|
54
|
+
result = result.replace(/Wи-Фи/gi, 'Wi-Fi');
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function countLatin(s) {
|
|
60
|
+
return (s.match(/[a-zA-Z]/g) || []).length;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const filePath = process.argv[2];
|
|
64
|
+
let content = readFileSync(filePath, 'utf-8');
|
|
65
|
+
|
|
66
|
+
content = content.replace(/'([^']*)'/g, (full, text) => {
|
|
67
|
+
if (text.length < 5) return full;
|
|
68
|
+
if (/^(http|\/|\.|#|@|mdi:)/.test(text)) return full;
|
|
69
|
+
if (/^[0-9+\-.,%°\s]+$/.test(text)) return full;
|
|
70
|
+
if (/^(FAQPage|HowTo|SoftwareApplication|UtilityApplication|Question|Answer|HowToStep|Offer|schema-dts)$/.test(text)) return full;
|
|
71
|
+
|
|
72
|
+
const latin = countLatin(text);
|
|
73
|
+
if (latin < 5) return full;
|
|
74
|
+
|
|
75
|
+
return "'" + toCyrillic(text) + "'";
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
79
|
+
console.log('Done');
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type LocaleWithDiacritics = keyof typeof DIACRITIC_RULES;
|
|
5
|
+
|
|
6
|
+
const DIACRITIC_RULES = {
|
|
7
|
+
de: {
|
|
8
|
+
language: 'German',
|
|
9
|
+
expectedCharacters: 'ä ö ü ß',
|
|
10
|
+
characters: /[äöüÄÖÜß]/g,
|
|
11
|
+
minPerThousandLetters: 0.1,
|
|
12
|
+
},
|
|
13
|
+
es: {
|
|
14
|
+
language: 'Spanish',
|
|
15
|
+
expectedCharacters: 'á é í ó ú ü ñ',
|
|
16
|
+
characters: /[áéíóúüñÁÉÍÓÚÜÑ]/g,
|
|
17
|
+
minPerThousandLetters: 0.1,
|
|
18
|
+
},
|
|
19
|
+
fr: {
|
|
20
|
+
language: 'French',
|
|
21
|
+
expectedCharacters: 'à â æ ç é è ê ë î ï ô œ ù û ü ÿ',
|
|
22
|
+
characters: /[àâæçéèêëîïôœùûüÿÀÂÆÇÉÈÊËÎÏÔŒÙÛÜŸ]/g,
|
|
23
|
+
minPerThousandLetters: 0.1,
|
|
24
|
+
},
|
|
25
|
+
it: {
|
|
26
|
+
language: 'Italian',
|
|
27
|
+
expectedCharacters: 'à è é ì í î ò ó ù ú',
|
|
28
|
+
characters: /[àèéìíîòóùúÀÈÉÌÍÎÒÓÙÚ]/g,
|
|
29
|
+
minPerThousandLetters: 0.1,
|
|
30
|
+
},
|
|
31
|
+
pl: {
|
|
32
|
+
language: 'Polish',
|
|
33
|
+
expectedCharacters: 'ą ć ę ł ń ó ś ź ż',
|
|
34
|
+
characters: /[ąćęłńóśźżĄĆĘŁŃÓŚŹŻ]/g,
|
|
35
|
+
minPerThousandLetters: 0.1,
|
|
36
|
+
},
|
|
37
|
+
pt: {
|
|
38
|
+
language: 'Portuguese',
|
|
39
|
+
expectedCharacters: 'á â ã à ç é ê í ó ô õ ú ü',
|
|
40
|
+
characters: /[áâãàçéêíóôõúüÁÂÃÀÇÉÊÍÓÔÕÚÜ]/g,
|
|
41
|
+
minPerThousandLetters: 0.1,
|
|
42
|
+
},
|
|
43
|
+
sv: {
|
|
44
|
+
language: 'Swedish',
|
|
45
|
+
expectedCharacters: 'å ä ö',
|
|
46
|
+
characters: /[åäöÅÄÖ]/g,
|
|
47
|
+
minPerThousandLetters: 0.1,
|
|
48
|
+
},
|
|
49
|
+
tr: {
|
|
50
|
+
language: 'Turkish',
|
|
51
|
+
expectedCharacters: 'ç ğ ı İ ö ş ü',
|
|
52
|
+
characters: /[çğıöşüÇĞİÖŞÜ]/g,
|
|
53
|
+
minPerThousandLetters: 0.1,
|
|
54
|
+
},
|
|
55
|
+
} as const;
|
|
56
|
+
|
|
57
|
+
const LETTERS = /\p{L}/gu;
|
|
58
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
59
|
+
|
|
60
|
+
function collectStrings(value: unknown): string[] {
|
|
61
|
+
if (typeof value === 'string') return [value];
|
|
62
|
+
if (!value || typeof value !== 'object') return [];
|
|
63
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
64
|
+
return Object.values(value).flatMap(collectStrings);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeText(value: unknown): string {
|
|
68
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
72
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function letterCount(text: string): number {
|
|
76
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function diacriticCount(text: string, locale: LocaleWithDiacritics): number {
|
|
80
|
+
return text.match(DIACRITIC_RULES[locale].characters)?.length ?? 0;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function diacriticsPerThousandLetters(text: string, locale: LocaleWithDiacritics): number {
|
|
84
|
+
const letters = letterCount(text);
|
|
85
|
+
if (letters === 0) return 0;
|
|
86
|
+
return diacriticCount(text, locale) / letters * 1000;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('Diacritics density validation', () => {
|
|
90
|
+
ALL_TOOLS.forEach((tool) => {
|
|
91
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
92
|
+
Object.keys(DIACRITIC_RULES).forEach((locale) => {
|
|
93
|
+
it(`${locale} keeps the expected accent and special-letter set`, async () => {
|
|
94
|
+
const typedLocale = locale as LocaleWithDiacritics;
|
|
95
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
96
|
+
if (!loader) return;
|
|
97
|
+
|
|
98
|
+
const content = await loader();
|
|
99
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
100
|
+
const rule = DIACRITIC_RULES[typedLocale];
|
|
101
|
+
const letters = letterCount(text);
|
|
102
|
+
const matches = diacriticCount(text, typedLocale);
|
|
103
|
+
const density = diacriticsPerThousandLetters(text, typedLocale);
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
density,
|
|
107
|
+
[
|
|
108
|
+
`Possible spelling or encoding issue detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
109
|
+
`The text has ${matches} special characters (${density.toFixed(2)} per 1000 letters, ${letters} letters analyzed).`,
|
|
110
|
+
`This locale should include some of these characters: ${rule.expectedCharacters}.`,
|
|
111
|
+
'If the count is 0 or near 0, accents, tildes, or special letters were probably stripped by encoding or normalization.',
|
|
112
|
+
].join(' '),
|
|
113
|
+
).toBeGreaterThanOrEqual(rule.minPerThousandLetters);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
const INVERTED_PUNCTUATION_LOCALES = {
|
|
5
|
+
es: {
|
|
6
|
+
language: 'Spanish',
|
|
7
|
+
questionStart: '¿',
|
|
8
|
+
questionEnd: '?',
|
|
9
|
+
exclamationStart: '¡',
|
|
10
|
+
exclamationEnd: '!',
|
|
11
|
+
},
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
14
|
+
type InvertedPunctuationLocale = keyof typeof INVERTED_PUNCTUATION_LOCALES;
|
|
15
|
+
|
|
16
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
17
|
+
|
|
18
|
+
function collectStrings(value: unknown): string[] {
|
|
19
|
+
if (typeof value === 'string') return [value];
|
|
20
|
+
if (!value || typeof value !== 'object') return [];
|
|
21
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
22
|
+
return Object.values(value).flatMap(collectStrings);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function translatableStrings(content: Record<string, unknown>): string[] {
|
|
26
|
+
return TRANSLATABLE_KEYS.flatMap((key) => collectStrings(content[key]));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function sentenceStart(text: string, endIndex: number): string {
|
|
30
|
+
const beforeMark = text.slice(0, endIndex).trimEnd();
|
|
31
|
+
const boundary = Math.max(
|
|
32
|
+
beforeMark.lastIndexOf('.'),
|
|
33
|
+
beforeMark.lastIndexOf(':'),
|
|
34
|
+
beforeMark.lastIndexOf(';'),
|
|
35
|
+
beforeMark.lastIndexOf('\n'),
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return beforeMark.slice(boundary + 1).trimStart();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function findMissingInvertedMarks(
|
|
42
|
+
text: string,
|
|
43
|
+
startMark: string,
|
|
44
|
+
endMark: string,
|
|
45
|
+
): string[] {
|
|
46
|
+
return [...text.matchAll(new RegExp(`\\${endMark}`, 'g'))]
|
|
47
|
+
.map((match) => sentenceStart(text, match.index ?? 0))
|
|
48
|
+
.filter((segment) => segment.length > 0 && !segment.includes(startMark))
|
|
49
|
+
.map((segment) => `${segment}${endMark}`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('Inverted punctuation validation', () => {
|
|
53
|
+
ALL_TOOLS.forEach((tool) => {
|
|
54
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
55
|
+
Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
|
|
56
|
+
it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
|
|
57
|
+
const typedLocale = locale as InvertedPunctuationLocale;
|
|
58
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
59
|
+
if (!loader) return;
|
|
60
|
+
|
|
61
|
+
const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
|
|
62
|
+
const content = await loader();
|
|
63
|
+
const strings = translatableStrings(content as Record<string, unknown>);
|
|
64
|
+
const missingQuestions = strings.flatMap((text) =>
|
|
65
|
+
findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd)
|
|
66
|
+
);
|
|
67
|
+
const missingExclamations = strings.flatMap((text) =>
|
|
68
|
+
findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd)
|
|
69
|
+
);
|
|
70
|
+
const failures = [...missingQuestions, ...missingExclamations];
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
failures,
|
|
74
|
+
[
|
|
75
|
+
`Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
76
|
+
`Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
|
|
77
|
+
`Examples: ${failures.slice(0, 5).join(' | ')}`,
|
|
78
|
+
].join(' '),
|
|
79
|
+
).toHaveLength(0);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type ScriptLocale = keyof typeof SCRIPT_RULES;
|
|
5
|
+
|
|
6
|
+
const SCRIPT_RULES = {
|
|
7
|
+
ja: {
|
|
8
|
+
language: 'Japanese',
|
|
9
|
+
scriptName: 'kana/kanji',
|
|
10
|
+
scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
|
|
11
|
+
minScriptRatio: 0.45,
|
|
12
|
+
},
|
|
13
|
+
ko: {
|
|
14
|
+
language: 'Korean',
|
|
15
|
+
scriptName: 'hangul',
|
|
16
|
+
scriptCharacters: /\p{Script=Hangul}/gu,
|
|
17
|
+
minScriptRatio: 0.55,
|
|
18
|
+
},
|
|
19
|
+
ru: {
|
|
20
|
+
language: 'Russian',
|
|
21
|
+
scriptName: 'cyrillic',
|
|
22
|
+
scriptCharacters: /\p{Script=Cyrillic}/gu,
|
|
23
|
+
minScriptRatio: 0.65,
|
|
24
|
+
},
|
|
25
|
+
zh: {
|
|
26
|
+
language: 'Chinese',
|
|
27
|
+
scriptName: 'han',
|
|
28
|
+
scriptCharacters: /\p{Script=Han}/gu,
|
|
29
|
+
minScriptRatio: 0.45,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const LETTERS = /\p{L}/gu;
|
|
34
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
35
|
+
|
|
36
|
+
function collectStrings(value: unknown): string[] {
|
|
37
|
+
if (typeof value === 'string') return [value];
|
|
38
|
+
if (!value || typeof value !== 'object') return [];
|
|
39
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
40
|
+
return Object.values(value).flatMap(collectStrings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeText(value: unknown): string {
|
|
44
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
48
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function letterCount(text: string): number {
|
|
52
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scriptCount(text: string, locale: ScriptLocale): number {
|
|
56
|
+
return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptRatio(text: string, locale: ScriptLocale): number {
|
|
60
|
+
const letters = letterCount(text);
|
|
61
|
+
if (letters === 0) return 0;
|
|
62
|
+
return scriptCount(text, locale) / letters;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Native script density validation', () => {
|
|
66
|
+
ALL_TOOLS.forEach((tool) => {
|
|
67
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
68
|
+
Object.keys(SCRIPT_RULES).forEach((locale) => {
|
|
69
|
+
it(`${locale} keeps most translated text in its native script`, async () => {
|
|
70
|
+
const typedLocale = locale as ScriptLocale;
|
|
71
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
72
|
+
if (!loader) return;
|
|
73
|
+
|
|
74
|
+
const content = await loader();
|
|
75
|
+
const rule = SCRIPT_RULES[typedLocale];
|
|
76
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
77
|
+
const letters = letterCount(text);
|
|
78
|
+
const matches = scriptCount(text, typedLocale);
|
|
79
|
+
const ratio = scriptRatio(text, typedLocale);
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
ratio,
|
|
83
|
+
[
|
|
84
|
+
`Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
85
|
+
`The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
|
|
86
|
+
`Most translatable content should be written in ${rule.scriptName} script.`,
|
|
87
|
+
'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
).toBeGreaterThanOrEqual(rule.minScriptRatio);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -4,67 +4,67 @@ import type { ApplianceCostCalculatorUI } from '../ui';
|
|
|
4
4
|
import { bibliography } from '../bibliography';
|
|
5
5
|
|
|
6
6
|
const slug = 'kalkulator-kosztow-urzadzen';
|
|
7
|
-
const title = 'Kalkulator
|
|
7
|
+
const title = 'Kalkulator Kosztów na Cykl Urządzeń Domowych';
|
|
8
8
|
const description =
|
|
9
|
-
'Oblicz rzeczywisty koszt
|
|
9
|
+
'Oblicz rzeczywisty koszt każdego prania w pralce, zmywarce i suszarce. Zobacz dokładnie, ile energii elektrycznej, wody i środka czyszczącego kosztuje każdy cykl i odkryj, ile możesz zaoszczędzić, przechodząc w tryb Eco.';
|
|
10
10
|
|
|
11
11
|
const faqData = [
|
|
12
12
|
{
|
|
13
13
|
question: 'Ile kosztuje cykl prania w pralce?',
|
|
14
14
|
answer:
|
|
15
|
-
'Typowy cykl Eco w
|
|
15
|
+
'Typowy cykl Eco w 40°C zużywa około 0,45 kWh energii elektrycznej i 40 litrów wody. Przy przeciętnych europejskich cenach kosztuje to około 0,10 do 0,15 euro na pranie. Cykl intensywny w 60°C może zużywać ponad 1,2 kWh i 70 litrów, podnosząc koszt powyżej 0,30 euro.',
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
|
-
question: 'Czy korzystanie ze zmywarki w nocy jest
|
|
18
|
+
question: 'Czy korzystanie ze zmywarki w nocy jest tańsze?',
|
|
19
19
|
answer:
|
|
20
|
-
'
|
|
20
|
+
'Jeśli Twój dostawca energii elektrycznej stosuje taryfy z różnicowaniem godzinowym, uruchamianie zmywarki poza szczytem może obniżyć koszt energii o 30 procent lub więcej. Godziny szczytu to zazwyczaj poranki i wieczory w dni powszednie.',
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
|
-
question: 'Czy suszarka
|
|
23
|
+
question: 'Czy suszarka zużywa dużo prądu?',
|
|
24
24
|
answer:
|
|
25
|
-
'Tak. Suszarka jest jednym z najbardziej
|
|
25
|
+
'Tak. Suszarka jest jednym z najbardziej energochłonnych urządzeń domowych. Pełne pranie może zużywać od 2,5 do 3,5 kWh, kosztując od 0,50 do 0,80 euro na cykl. Użycie suszarki z pompą ciepła lub suszenie odzieży na zewnątrz może obniżyć ten koszt o 60 do 80 procent.',
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
|
-
question: 'Ile wody
|
|
28
|
+
question: 'Ile wody zużywa zmywarka?',
|
|
29
29
|
answer:
|
|
30
|
-
'Nowoczesne zmywarki
|
|
30
|
+
'Nowoczesne zmywarki są zaskakująco wydajne. Standardowy cykl Eco zużywa tylko 8 do 12 litrów wody na pranie, podczas gdy program intensywny może zużywać 14 do 16 litrów. Jest to znacznie mniej niż mycie tych samych naczyń ręcznie pod bieżącą wodą, co zazwyczaj zużywa 40 do 60 litrów.',
|
|
31
31
|
},
|
|
32
32
|
{
|
|
33
|
-
question: 'Jaka jest
|
|
33
|
+
question: 'Jaka jest najtańsza pora na korzystanie z urządzeń?',
|
|
34
34
|
answer:
|
|
35
|
-
'Przy taryfach z
|
|
35
|
+
'Przy taryfach z różnicowaniem godzinowym najtańsze okresy to zazwyczaj późna noc, bardzo wczesny ranek i weekendy. Te okresy poza szczytem mogą być o 30 do 50 procent tańsze niż szczytowe godziny w dni powszednie.',
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
|
-
question: 'Czy
|
|
38
|
+
question: 'Czy przejście w tryb Eco naprawdę pozwala zaoszczędzić?',
|
|
39
39
|
answer:
|
|
40
|
-
'Absolutnie. Tryby Eco
|
|
40
|
+
'Absolutnie. Tryby Eco wykorzystują niższe temperatury, krótsze fazy nagrzewania i mniej wody. Przez cały rok, przechodzenie z Normalnego na Eco w pralce i zmywarce może zaoszczędzić od 50 do 100 euro łącznie na energii elektrycznej i wodzie, w zależności od częstotliwości użycia i lokalnych cen.',
|
|
41
41
|
},
|
|
42
42
|
];
|
|
43
43
|
|
|
44
44
|
const howToData = [
|
|
45
45
|
{
|
|
46
|
-
name: 'Wybierz
|
|
47
|
-
text: 'Dotknij ikony pralki, zmywarki lub suszarki u
|
|
46
|
+
name: 'Wybierz urządzenie',
|
|
47
|
+
text: 'Dotknij ikony pralki, zmywarki lub suszarki u góry kalkulatora. Każde urządzenie ma inny profil zużycia energii i wody.',
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
name: 'Wybierz typ cyklu',
|
|
51
|
-
text: '
|
|
51
|
+
text: 'Użyj suwaka Eco, Normalny lub Intensywny, aby ustawić program prania. Eco zużywa mniej energii i wody. Intensywny zużywa najwięcej.',
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
|
-
name: '
|
|
55
|
-
text: 'Wpisz
|
|
54
|
+
name: 'Wprowadź lokalne ceny',
|
|
55
|
+
text: 'Wpisz cenę energii elektrycznej za kWh i cenę wody za litr. Możesz je znaleźć na rachunku lub na stronie dostawcy.',
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
|
-
name: 'Ustaw
|
|
59
|
-
text: 'Podaj, ile cykli uruchamiasz w tygodniu. Pozwala to kalkulatorowi
|
|
58
|
+
name: 'Ustaw częstotliwość użycia',
|
|
59
|
+
text: 'Podaj, ile cykli uruchamiasz w tygodniu. Pozwala to kalkulatorowi oszacować koszt roczny i potencjalne oszczędności.',
|
|
60
60
|
},
|
|
61
61
|
{
|
|
62
|
-
name: 'Wybierz
|
|
63
|
-
text: 'Wybierz
|
|
62
|
+
name: 'Wybierz godzinę użycia',
|
|
63
|
+
text: 'Wybierz godzinę, w której planujesz uruchomić urządzenie. Godziny szczytu są podświetlone na czerwono. Godziny poza szczytem są zielone i tańsze, jeśli masz taryfę z różnicowaniem godzinowym.',
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
|
-
name: '
|
|
67
|
-
text: '
|
|
66
|
+
name: 'Włącz koszty środka czyszczącego',
|
|
67
|
+
text: 'Włącz przełącznik środka czyszczącego, jeśli chcesz, aby rachunek zawierał szacunek proszku, płynu lub kapsułek na cykl.',
|
|
68
68
|
},
|
|
69
69
|
];
|
|
70
70
|
|
|
@@ -112,45 +112,45 @@ export const content: ToolLocaleContent<ApplianceCostCalculatorUI> = {
|
|
|
112
112
|
seo: [
|
|
113
113
|
{
|
|
114
114
|
type: 'title',
|
|
115
|
-
text: 'Ukryty Koszt
|
|
115
|
+
text: 'Ukryty Koszt Każdego Prania',
|
|
116
116
|
level: 2,
|
|
117
117
|
},
|
|
118
118
|
{
|
|
119
119
|
type: 'paragraph',
|
|
120
|
-
html: '
|
|
120
|
+
html: 'Większość osób nie ma pojęcia, ile rzeczywiście kosztuje cykl prania w pralce. Widzą kwartalny rachunek za energię elektryczną i zakładają, że urządzenia są tanie w eksploatacji. Rzeczywistość jest taka, że rodzina wykonująca pięć prań w tygodniu może wydawać ponad 200 euro rocznie tylko na pranie. Dodajmy zmywarkę trzy razy w tygodniu i suszarkę dwa razy, a suma przekracza 500 euro rocznie. Ten kalkulator szczegółowo analizuje każdy grosz, aby zoptymalizować Twoje nawyki.',
|
|
121
121
|
},
|
|
122
122
|
{
|
|
123
123
|
type: 'stats',
|
|
124
124
|
items: [
|
|
125
125
|
{ value: '0,45', label: 'kWh Pranie Eco', icon: 'mdi:washing-machine' },
|
|
126
126
|
{ value: '40L', label: 'Woda Pranie Eco', icon: 'mdi:water' },
|
|
127
|
-
{ value: '30%', label: '
|
|
127
|
+
{ value: '30%', label: 'Dopłata Szczytowa', icon: 'mdi:lightning-bolt' },
|
|
128
128
|
],
|
|
129
129
|
columns: 3,
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
type: 'title',
|
|
133
|
-
text: 'Dlaczego
|
|
133
|
+
text: 'Dlaczego Małe Zmiany Się Mnożą',
|
|
134
134
|
level: 3,
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
type: 'paragraph',
|
|
138
|
-
html: 'Jeden
|
|
138
|
+
html: 'Jeden stopień temperatury lub kilka litrów wody wydaje się nieistotne w jednym cyklu. Ale w ciągu 250 cykli rocznie, te małe różnice przekładają się na poważne pieniądze. Spadek z 60°C do 40°C zmniejsza zużycie energii o około 40 procent na pranie. Przejście z Normalnego na Eco w zmywarce oszczędza około 3 litrów wody i 0,3 kWh za każdym razem. To nie są marginalne zyski. To najszybszy sposób na zmniejszenie rachunków bez kupowania nowych urządzeń.',
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
type: 'comparative',
|
|
142
142
|
items: [
|
|
143
143
|
{
|
|
144
144
|
title: 'Cykl Intensywny',
|
|
145
|
-
description: 'Maksymalne
|
|
145
|
+
description: 'Maksymalne ciepło i woda dla silnie zabrudzonych prań.',
|
|
146
146
|
icon: 'mdi:alert',
|
|
147
|
-
points: ['Od 1,2 do 3,5 kWh na pranie', '70
|
|
147
|
+
points: ['Od 1,2 do 3,5 kWh na pranie', '70 litrów w pralce', 'Obowiązuje dopłata w szczycie', 'Najwyższy koszt roczny'],
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
title: 'Cykl Eco',
|
|
151
|
-
description: '
|
|
151
|
+
description: 'Niższa temperatura, zoptymalizowany czas, minimalna ilość wody.',
|
|
152
152
|
icon: 'mdi:check-circle',
|
|
153
|
-
points: ['Od 0,45 do 1,5 kWh na pranie', 'Tylko 8 do 40
|
|
153
|
+
points: ['Od 0,45 do 1,5 kWh na pranie', 'Tylko 8 do 40 litrów', 'Możliwość uruchomienia poza szczytem', 'Najniższy koszt roczny'],
|
|
154
154
|
},
|
|
155
155
|
],
|
|
156
156
|
columns: 2,
|
|
@@ -158,35 +158,35 @@ export const content: ToolLocaleContent<ApplianceCostCalculatorUI> = {
|
|
|
158
158
|
{
|
|
159
159
|
type: 'diagnostic',
|
|
160
160
|
variant: 'info',
|
|
161
|
-
title: 'Szybki Audyt
|
|
161
|
+
title: 'Szybki Audyt Urządzeń',
|
|
162
162
|
icon: 'mdi:clipboard-check',
|
|
163
|
-
badge: '
|
|
164
|
-
html: '<p style="margin:0">
|
|
163
|
+
badge: 'Działanie',
|
|
164
|
+
html: '<p style="margin:0">Używaj pralki tylko przy pełnym załadowaniu. Używaj programu Eco przy normalnym zabrudzeniu. Ustaw zmywarkę na opóźniony start po 22:00, jeśli masz taryfę z różnicowaniem godzinowym. Czyść filtr suszarki po każdym cyklu, aby zachować wydajność. Susz odzież na zewnątrz lub na suszarce, gdy pozwala na to pogoda.</p>',
|
|
165
165
|
},
|
|
166
166
|
{
|
|
167
167
|
type: 'title',
|
|
168
|
-
text: 'Taryfy z
|
|
168
|
+
text: 'Taryfy z Różnicowaniem Godzinowym i Godziny Szczytu',
|
|
169
169
|
level: 3,
|
|
170
170
|
},
|
|
171
171
|
{
|
|
172
172
|
type: 'paragraph',
|
|
173
|
-
html: 'Wielu
|
|
173
|
+
html: 'Wielu dostawców energii elektrycznej obecnie stosuje różne stawki w zależności od pory dnia. Okresy szczytowe, zazwyczaj poranki i wieczory w dni powszednie, mogą być o 30 do 50 procent droższe niż okresy poza szczytem. Takie samo pranie o 20:00 może kosztować 0,18 euro, podczas gdy to samo pranie o 02:00 kosztuje tylko 0,12 euro. W ciągu roku, właściwe planowanie korzystania z urządzeń może zaoszczędzić od 50 do 100 euro bez zmiany niczego innego. Ten kalkulator podświetla godziny szczytu na czerwono, a poza szczytem na zielono, aby można było planować.',
|
|
174
174
|
},
|
|
175
175
|
{
|
|
176
176
|
type: 'summary',
|
|
177
|
-
title: 'Jak
|
|
177
|
+
title: 'Jak Ograniczyć Koszty Urządzeń',
|
|
178
178
|
items: [
|
|
179
|
-
'
|
|
180
|
-
'
|
|
181
|
-
'Uruchamiaj
|
|
182
|
-
'
|
|
183
|
-
'Susz
|
|
184
|
-
'Regularnie
|
|
179
|
+
'Użyj tego kalkulatora, aby poznać rzeczywisty koszt każdego cyklu Twoich urządzeń.',
|
|
180
|
+
'Przechodź w tryb Eco zawsze, gdy to możliwe, aby zaoszczędzić do 40 procent energii.',
|
|
181
|
+
'Uruchamiaj pełne załadowania zamiast częściowych, aby zmaksymalizować wydajność.',
|
|
182
|
+
'Opóźniaj cykle na godziny poza szczytem, jeśli Twoja taryfa przewiduje ceny godzinowe.',
|
|
183
|
+
'Susz odzież na zewnątrz zamiast używać suszarki, aby całkowicie wyeliminować ten koszt.',
|
|
184
|
+
'Regularnie czyść filtry i odkamieniaj urządzenia, aby utrzymać je na nominalnej wydajności.',
|
|
185
185
|
],
|
|
186
186
|
},
|
|
187
187
|
],
|
|
188
188
|
ui: {
|
|
189
|
-
labelAppliance: '
|
|
189
|
+
labelAppliance: 'Urządzenie',
|
|
190
190
|
applianceWasher: 'Pralka',
|
|
191
191
|
applianceDishwasher: 'Zmywarka',
|
|
192
192
|
applianceDryer: 'Suszarka',
|
|
@@ -198,21 +198,21 @@ export const content: ToolLocaleContent<ApplianceCostCalculatorUI> = {
|
|
|
198
198
|
unitPriceKwh: '€/kWh',
|
|
199
199
|
labelWaterPrice: 'Cena Wody',
|
|
200
200
|
unitPriceLiter: '€/L',
|
|
201
|
-
labelDetergent: '
|
|
202
|
-
labelDetergentToggle: '
|
|
201
|
+
labelDetergent: 'Środek Czyszczący',
|
|
202
|
+
labelDetergentToggle: 'Uwzględnij koszt środka',
|
|
203
203
|
labelHour: 'Planowana Godzina',
|
|
204
204
|
peakBadge: 'Aktywna taryfa szczytowa',
|
|
205
205
|
offPeakBadge: 'Taryfa poza szczytem',
|
|
206
|
-
receiptTitle: 'Podsumowanie
|
|
206
|
+
receiptTitle: 'Podsumowanie Kosztów',
|
|
207
207
|
receiptElectricity: 'Energia',
|
|
208
208
|
receiptWater: 'Woda',
|
|
209
|
-
receiptDetergent: '
|
|
209
|
+
receiptDetergent: 'Środek',
|
|
210
210
|
receiptTotal: 'Razem na cykl',
|
|
211
211
|
receiptPerYear: 'Szacunkowo rocznie',
|
|
212
212
|
comparisonTitle: 'Inteligentna Zmiana',
|
|
213
|
-
comparisonText: '
|
|
214
|
-
comparisonSavings: '
|
|
215
|
-
comparisonMonths: '
|
|
213
|
+
comparisonText: 'Przejście z Normalnego na Eco oszczędza pieniądze przy każdym praniu.',
|
|
214
|
+
comparisonSavings: 'Oszczędności roczne',
|
|
215
|
+
comparisonMonths: 'miesięcy środka',
|
|
216
216
|
badgePeak: 'Szczyt',
|
|
217
217
|
badgeOffPeak: 'Poza Szczytem',
|
|
218
218
|
unitKwh: 'kWh',
|