@jjlmoya/utils-developer 1.13.0 → 1.15.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/i18n/fr.ts +1 -1
- package/src/category/i18n/ru.ts +1 -1
- package/src/layouts/PreviewLayout.astro +1 -0
- package/src/pages/[locale]/[slug].astro +29 -12
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +113 -0
- package/src/tests/no_en_dash.test.ts +73 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tool/aspectRatio/aspect-ratio-calculator.css +1 -1
- package/src/tool/aspectRatio/i18n/de.ts +1 -1
- package/src/tool/aspectRatio/i18n/fr.ts +1 -1
- package/src/tool/aspectRatio/i18n/pl.ts +2 -2
- package/src/tool/aspectRatio/i18n/ru.ts +4 -4
- package/src/tool/aspectRatio/i18n/zh.ts +1 -1
- package/src/tool/aspectRatio/seo.astro +2 -1
- package/src/tool/calculadoraTiempoDatos/data-time-calculator-web-speed-impact.css +26 -26
- package/src/tool/calculadoraTiempoDatos/i18n/de.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/pl.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/i18n/ru.ts +5 -5
- package/src/tool/calculadoraTiempoDatos/i18n/sv.ts +5 -5
- package/src/tool/calculadoraTiempoDatos/i18n/zh.ts +1 -1
- package/src/tool/calculadoraTiempoDatos/seo.astro +2 -1
- package/src/tool/colorConverter/color-converter-rgb-hex-hsl.css +9 -9
- package/src/tool/colorConverter/i18n/de.ts +1 -1
- package/src/tool/colorConverter/i18n/ru.ts +2 -2
- package/src/tool/colorConverter/seo.astro +2 -1
- package/src/tool/conversorExcelCsvHtml/excel-csv-html-table-converter.css +24 -24
- package/src/tool/conversorExcelCsvHtml/i18n/es.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/pl.ts +1 -1
- package/src/tool/conversorExcelCsvHtml/i18n/ru.ts +2 -2
- package/src/tool/conversorExcelCsvHtml/i18n/zh.ts +2 -2
- package/src/tool/conversorExcelCsvHtml/seo.astro +2 -1
- package/src/tool/cronGenerator/cron.css +5 -5
- package/src/tool/cronGenerator/i18n/de.ts +4 -4
- package/src/tool/cronGenerator/i18n/en.ts +1 -1
- package/src/tool/cronGenerator/i18n/es.ts +2 -2
- package/src/tool/cronGenerator/i18n/fr.ts +5 -5
- package/src/tool/cronGenerator/i18n/id.ts +1 -1
- package/src/tool/cronGenerator/i18n/nl.ts +1 -1
- package/src/tool/cronGenerator/i18n/pl.ts +8 -8
- package/src/tool/cronGenerator/i18n/pt.ts +1 -1
- package/src/tool/cronGenerator/i18n/ru.ts +9 -9
- package/src/tool/cronGenerator/i18n/sv.ts +1 -1
- package/src/tool/cronGenerator/i18n/zh.ts +3 -3
- package/src/tool/cronGenerator/seo.astro +2 -1
- package/src/tool/cssSpecificityCalculator/css-specificity-calculator.css +1 -1
- package/src/tool/cssSpecificityCalculator/i18n/de.ts +1 -1
- package/src/tool/cssSpecificityCalculator/i18n/fr.ts +3 -3
- package/src/tool/cssSpecificityCalculator/i18n/pl.ts +2 -2
- package/src/tool/cssSpecificityCalculator/i18n/ru.ts +2 -2
- package/src/tool/cssSpecificityCalculator/seo.astro +2 -1
- package/src/tool/cssToInlineConverter/css-to-inline-converter.css +1 -1
- package/src/tool/cssToInlineConverter/i18n/de.ts +6 -6
- package/src/tool/cssToInlineConverter/i18n/es.ts +1 -1
- package/src/tool/cssToInlineConverter/i18n/fr.ts +3 -3
- package/src/tool/cssToInlineConverter/seo.astro +2 -1
- package/src/tool/duplicateCssRemover/duplicate-css-remover.css +2 -2
- package/src/tool/duplicateCssRemover/i18n/de.ts +7 -7
- package/src/tool/duplicateCssRemover/i18n/fr.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/id.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/nl.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/pl.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/pt.ts +1 -1
- package/src/tool/duplicateCssRemover/i18n/ru.ts +3 -3
- package/src/tool/duplicateCssRemover/i18n/sv.ts +1 -1
- package/src/tool/duplicateCssRemover/seo.astro +2 -1
- package/src/tool/generadorSecurityTxt/i18n/de.ts +5 -5
- package/src/tool/generadorSecurityTxt/i18n/ru.ts +2 -2
- package/src/tool/generadorSecurityTxt/security-txt-generator-rfc-9116.css +9 -9
- package/src/tool/generadorSecurityTxt/seo.astro +2 -1
- package/src/tool/hashGenerator/hash-generator.css +10 -10
- package/src/tool/hashGenerator/i18n/ru.ts +5 -5
- package/src/tool/hashGenerator/i18n/zh.ts +2 -2
- package/src/tool/hashGenerator/seo.astro +2 -1
- package/src/tool/inspectorCertificadosSsl/i18n/de.ts +6 -6
- package/src/tool/inspectorCertificadosSsl/i18n/ru.ts +5 -5
- package/src/tool/inspectorCertificadosSsl/seo.astro +2 -1
- package/src/tool/inspectorCertificadosSsl/ssl-tls-certificate-inspector.css +9 -9
- package/src/tool/jsonFormatter/i18n/de.ts +4 -4
- package/src/tool/jsonFormatter/i18n/en.ts +4 -4
- package/src/tool/jsonFormatter/i18n/es.ts +4 -4
- package/src/tool/jsonFormatter/i18n/fr.ts +4 -4
- package/src/tool/jsonFormatter/i18n/id.ts +4 -4
- package/src/tool/jsonFormatter/i18n/it.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ja.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ko.ts +4 -4
- package/src/tool/jsonFormatter/i18n/nl.ts +4 -4
- package/src/tool/jsonFormatter/i18n/pl.ts +4 -4
- package/src/tool/jsonFormatter/i18n/pt.ts +4 -4
- package/src/tool/jsonFormatter/i18n/ru.ts +5 -5
- package/src/tool/jsonFormatter/i18n/sv.ts +4 -4
- package/src/tool/jsonFormatter/i18n/tr.ts +4 -4
- package/src/tool/jsonFormatter/i18n/zh.ts +5 -5
- package/src/tool/jsonFormatter/json-formatter.css +33 -33
- package/src/tool/jsonFormatter/seo.astro +2 -1
- package/src/tool/keycode/i18n/de.ts +7 -7
- package/src/tool/keycode/i18n/en.ts +5 -5
- package/src/tool/keycode/i18n/es.ts +5 -5
- package/src/tool/keycode/i18n/fr.ts +7 -7
- package/src/tool/keycode/i18n/id.ts +5 -5
- package/src/tool/keycode/i18n/it.ts +5 -5
- package/src/tool/keycode/i18n/ja.ts +5 -5
- package/src/tool/keycode/i18n/ko.ts +5 -5
- package/src/tool/keycode/i18n/nl.ts +5 -5
- package/src/tool/keycode/i18n/pl.ts +7 -7
- package/src/tool/keycode/i18n/pt.ts +5 -5
- package/src/tool/keycode/i18n/ru.ts +6 -6
- package/src/tool/keycode/i18n/sv.ts +5 -5
- package/src/tool/keycode/i18n/tr.ts +5 -5
- package/src/tool/keycode/i18n/zh.ts +5 -5
- package/src/tool/keycode/keycode.css +1 -1
- package/src/tool/keycode/seo.astro +2 -1
- package/src/tool/llmCostCalculator/i18n/de.ts +4 -4
- package/src/tool/llmCostCalculator/i18n/en.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/fr.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/id.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/pt.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/ru.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/sv.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/tr.ts +3 -3
- package/src/tool/llmCostCalculator/i18n/zh.ts +3 -3
- package/src/tool/llmCostCalculator/llm-cost-calculator.css +1 -1
- package/src/tool/llmCostCalculator/seo.astro +2 -1
- package/src/tool/mobileMockupGenerator/i18n/es.ts +1 -1
- package/src/tool/mobileMockupGenerator/mobile-mockup-generator.css +68 -68
- package/src/tool/mobileMockupGenerator/seo.astro +2 -1
- package/src/tool/musicalTypography/i18n/de.ts +1 -1
- package/src/tool/musicalTypography/i18n/fr.ts +1 -1
- package/src/tool/musicalTypography/i18n/id.ts +1 -1
- package/src/tool/musicalTypography/i18n/it.ts +1 -1
- package/src/tool/musicalTypography/i18n/ko.ts +1 -1
- package/src/tool/musicalTypography/i18n/ru.ts +3 -3
- package/src/tool/musicalTypography/musical-typography.css +8 -8
- package/src/tool/musicalTypography/seo.astro +2 -1
- package/src/tool/placeholderGenerator/i18n/de.ts +4 -4
- package/src/tool/placeholderGenerator/i18n/ru.ts +1 -1
- package/src/tool/placeholderGenerator/placeholder-image-generator.css +2 -2
- package/src/tool/placeholderGenerator/seo.astro +2 -1
- package/src/tool/promptLibrary/i18n/de.ts +1 -1
- package/src/tool/promptLibrary/prompt-library.css +30 -30
- package/src/tool/promptLibrary/seo.astro +2 -1
- package/src/tool/readabilityCalculator/i18n/de.ts +2 -2
- package/src/tool/readabilityCalculator/seo.astro +2 -1
- package/src/tool/readabilityCalculator/visual-readability-calculator-wcag-apca.css +98 -14
- package/src/tool/svgSanitizer/i18n/de.ts +2 -2
- package/src/tool/svgSanitizer/i18n/id.ts +1 -1
- package/src/tool/svgSanitizer/i18n/pl.ts +1 -1
- package/src/tool/svgSanitizer/i18n/ru.ts +7 -7
- package/src/tool/svgSanitizer/i18n/sv.ts +3 -3
- package/src/tool/svgSanitizer/i18n/tr.ts +3 -3
- package/src/tool/svgSanitizer/i18n/zh.ts +3 -3
- package/src/tool/svgSanitizer/seo.astro +2 -1
- package/src/tool/svgSanitizer/svg-sanitizer.css +7 -7
- package/src/tool/svgToCss/i18n/de.ts +2 -2
- package/src/tool/svgToCss/i18n/es.ts +1 -1
- package/src/tool/svgToCss/i18n/fr.ts +1 -1
- package/src/tool/svgToCss/i18n/pl.ts +1 -1
- package/src/tool/svgToCss/i18n/ru.ts +2 -2
- package/src/tool/svgToCss/i18n/zh.ts +2 -2
- package/src/tool/svgToCss/seo.astro +2 -1
- package/src/tool/svgToCss/svg-to-css-converter.css +2 -2
- package/src/tool/urlCleaner/component.astro +23 -5
- package/src/tool/urlCleaner/i18n/de.ts +3 -3
- package/src/tool/urlCleaner/i18n/es.ts +1 -1
- package/src/tool/urlCleaner/i18n/id.ts +1 -1
- package/src/tool/urlCleaner/i18n/nl.ts +1 -1
- package/src/tool/urlCleaner/seo.astro +2 -1
- package/src/tool/urlCleaner/url-tracking-cleaner.css +104 -14
- package/src/tool/urlEncoderDecoder/i18n/de.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/fr.ts +2 -2
- package/src/tool/urlEncoderDecoder/i18n/pl.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/ru.ts +8 -8
- package/src/tool/urlEncoderDecoder/i18n/sv.ts +5 -5
- package/src/tool/urlEncoderDecoder/i18n/zh.ts +5 -5
- package/src/tool/urlEncoderDecoder/seo.astro +2 -1
- package/src/tool/urlEncoderDecoder/url-encoder-decoder.css +1 -1
- package/src/tool/utmGenerator/i18n/de.ts +2 -2
- package/src/tool/utmGenerator/i18n/pl.ts +5 -5
- package/src/tool/utmGenerator/i18n/ru.ts +6 -6
- package/src/tool/utmGenerator/seo.astro +2 -1
- package/src/tool/utmGenerator/utm-generator.css +6 -6
package/package.json
CHANGED
package/src/category/i18n/fr.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const content: CategoryLocaleContent = {
|
|
|
4
4
|
slug: 'developpement-web',
|
|
5
5
|
title: 'Développement Web',
|
|
6
6
|
description:
|
|
7
|
-
"Outils gratuits de développement web
|
|
7
|
+
"Outils gratuits de développement web: formateurs, calculatrices CSS, générateurs et utilitaires pour développeurs frontend et backend.",
|
|
8
8
|
seo: [
|
|
9
9
|
{
|
|
10
10
|
type: 'summary',
|
package/src/category/i18n/ru.ts
CHANGED
|
@@ -23,7 +23,7 @@ export const content: CategoryLocaleContent = {
|
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
type: 'paragraph',
|
|
26
|
-
html: 'Коллекция инструментов, разработанных для ускорения рабочего процесса веб-разработчиков. От валидации JSON до калькуляторов специфичности CSS
|
|
26
|
+
html: 'Коллекция инструментов, разработанных для ускорения рабочего процесса веб-разработчиков. От валидации JSON до калькуляторов специфичности CSS - каждая утилита создана для решения реальных повседневных задач разработки.',
|
|
27
27
|
},
|
|
28
28
|
{
|
|
29
29
|
type: 'tip',
|
|
@@ -34,18 +34,28 @@ export async function getStaticPaths() {
|
|
|
34
34
|
]),
|
|
35
35
|
) as Partial<Record<KnownLocale, string>>;
|
|
36
36
|
|
|
37
|
+
const firstLoader = entry.i18n.en ?? Object.values(entry.i18n)[0];
|
|
38
|
+
const englishSlug = firstLoader ? (await firstLoader()).slug : entry.id;
|
|
39
|
+
|
|
37
40
|
for (const { locale, content } of localeContents) {
|
|
38
|
-
const allToolsNav =
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
const allToolsNav = (
|
|
42
|
+
await Promise.all(
|
|
43
|
+
ALL_TOOLS.map(async ({ entry: navEntry }) => {
|
|
44
|
+
const loader = navEntry.i18n[locale] ?? navEntry.i18n.en;
|
|
45
|
+
if (!loader) return null;
|
|
46
|
+
const navContent = await loader();
|
|
47
|
+
return {
|
|
48
|
+
id: navEntry.id,
|
|
49
|
+
title: navContent.title,
|
|
50
|
+
href: `/${locale}/${navContent.slug}`,
|
|
51
|
+
isActive: navEntry.id === entry.id,
|
|
52
|
+
};
|
|
53
|
+
}),
|
|
54
|
+
)
|
|
55
|
+
).filter(Boolean) as NavItem[];
|
|
46
56
|
paths.push({
|
|
47
57
|
params: { locale, slug: content.slug },
|
|
48
|
-
props: { Component, locale, content, localeUrls, allToolsNav },
|
|
58
|
+
props: { Component, locale, content, localeUrls, allToolsNav, englishSlug },
|
|
49
59
|
});
|
|
50
60
|
}
|
|
51
61
|
}
|
|
@@ -66,11 +76,17 @@ interface Props {
|
|
|
66
76
|
content: ToolLocaleContent;
|
|
67
77
|
localeUrls: Partial<Record<KnownLocale, string>>;
|
|
68
78
|
allToolsNav: NavItem[];
|
|
79
|
+
englishSlug: string;
|
|
69
80
|
}
|
|
70
81
|
|
|
71
|
-
const { Component, locale, content, localeUrls, allToolsNav } = Astro.props;
|
|
82
|
+
const { Component, locale, content, localeUrls, allToolsNav, englishSlug } = Astro.props;
|
|
83
|
+
|
|
84
|
+
const cssFiles = import.meta.glob("../../tool/*/*.css", { query: "?raw", import: "default" });
|
|
85
|
+
const cssKey = Object.keys(cssFiles).find((k) => k.endsWith(`/${englishSlug}.css`));
|
|
86
|
+
const cssLoader = cssKey ? cssFiles[cssKey] : undefined;
|
|
87
|
+
const toolCss = cssLoader ? await cssLoader() as string : "";
|
|
72
88
|
|
|
73
|
-
const seoContent: UtilitySEOContent = { locale, sections: content.seo };
|
|
89
|
+
const seoContent: UtilitySEOContent = { locale, sections: content.seo ?? [] };
|
|
74
90
|
|
|
75
91
|
const words = content.title.split(" ");
|
|
76
92
|
const titleHighlight = words[0] || "";
|
|
@@ -89,8 +105,9 @@ const titleBase = words.slice(1).join(" ") || "";
|
|
|
89
105
|
tools={allToolsNav}
|
|
90
106
|
/>
|
|
91
107
|
<Fragment slot="head">
|
|
108
|
+
{toolCss ? <Fragment set:html={`<style is:inline>${toolCss}</style>`} /> : null}
|
|
92
109
|
{
|
|
93
|
-
content.schemas.map((schema: unknown) => (
|
|
110
|
+
( content.schemas ?? []).map((schema: unknown) => (
|
|
94
111
|
<script
|
|
95
112
|
is:inline
|
|
96
113
|
type="application/ld+json"
|
|
@@ -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,113 @@
|
|
|
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
|
+
|
|
32
|
+
const boundaries = [
|
|
33
|
+
...[...beforeMark.matchAll(/\.(?=\s|$)/g)].map((m) => m.index ?? 0),
|
|
34
|
+
...[...beforeMark.matchAll(/:(?=\s|$)/g)].map((m) => m.index ?? 0),
|
|
35
|
+
...[...beforeMark.matchAll(/;(?=\s|$)/g)].map((m) => m.index ?? 0),
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
const lastBoundary = boundaries.length > 0
|
|
39
|
+
? Math.max(...boundaries)
|
|
40
|
+
: -1;
|
|
41
|
+
|
|
42
|
+
const nlBoundary = beforeMark.lastIndexOf('\n');
|
|
43
|
+
const boundary = Math.max(lastBoundary, nlBoundary);
|
|
44
|
+
|
|
45
|
+
return beforeMark.slice(boundary + 1).trimStart();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isCodeContext(text: string, index: number, mark: string): boolean {
|
|
49
|
+
if (mark === '!') {
|
|
50
|
+
const after = text.slice(index + 1, index + 10);
|
|
51
|
+
return /^important\b/.test(after);
|
|
52
|
+
}
|
|
53
|
+
if (mark === '?') {
|
|
54
|
+
const before = text.slice(Math.max(0, index - 2), index);
|
|
55
|
+
return /[,#\/&+=]/.test(before) || / y $/.test(text.slice(0, index));
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function findMissingInvertedMarks(
|
|
61
|
+
text: string,
|
|
62
|
+
startMark: string,
|
|
63
|
+
endMark: string,
|
|
64
|
+
_toolId: string,
|
|
65
|
+
): string[] {
|
|
66
|
+
const cleaned = text.replace(/<code>.*?<\/code>/g, '');
|
|
67
|
+
|
|
68
|
+
return [...cleaned.matchAll(new RegExp(`\\${endMark}`, 'g'))]
|
|
69
|
+
.filter((match) => !isCodeContext(cleaned, match.index ?? 0, endMark))
|
|
70
|
+
.map((match) => sentenceStart(cleaned, match.index ?? 0))
|
|
71
|
+
.filter((segment) => {
|
|
72
|
+
if (segment.length === 0) return false;
|
|
73
|
+
if (segment.includes(startMark)) return false;
|
|
74
|
+
if (endMark === '?' && /:\/\//.test(segment)) return false;
|
|
75
|
+
return true;
|
|
76
|
+
})
|
|
77
|
+
.map((segment) => `${segment}${endMark}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('Inverted punctuation validation', () => {
|
|
81
|
+
ALL_TOOLS.forEach((tool) => {
|
|
82
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
83
|
+
Object.keys(INVERTED_PUNCTUATION_LOCALES).forEach((locale) => {
|
|
84
|
+
it(`${locale} uses opening punctuation marks for questions and exclamations`, async () => {
|
|
85
|
+
const typedLocale = locale as InvertedPunctuationLocale;
|
|
86
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
87
|
+
if (!loader) return;
|
|
88
|
+
|
|
89
|
+
const rule = INVERTED_PUNCTUATION_LOCALES[typedLocale];
|
|
90
|
+
const content = await loader();
|
|
91
|
+
const strings = translatableStrings(content as Record<string, unknown>);
|
|
92
|
+
const toolId = tool.entry.id;
|
|
93
|
+
const missingQuestions = strings.flatMap((text) =>
|
|
94
|
+
findMissingInvertedMarks(text, rule.questionStart, rule.questionEnd, toolId)
|
|
95
|
+
);
|
|
96
|
+
const missingExclamations = strings.flatMap((text) =>
|
|
97
|
+
findMissingInvertedMarks(text, rule.exclamationStart, rule.exclamationEnd, toolId)
|
|
98
|
+
);
|
|
99
|
+
const failures = [...missingQuestions, ...missingExclamations];
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
failures,
|
|
103
|
+
[
|
|
104
|
+
`Missing opening punctuation marks in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
105
|
+
`Questions must use ${rule.questionStart}...${rule.questionEnd} and exclamations must use ${rule.exclamationStart}...${rule.exclamationEnd}.`,
|
|
106
|
+
`Examples: ${failures.slice(0, 5).join(' | ')}`,
|
|
107
|
+
].join(' '),
|
|
108
|
+
).toHaveLength(0);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
function getFiles(dir: string): string[] {
|
|
6
|
+
const results: string[] = [];
|
|
7
|
+
if (!fs.existsSync(dir)) {
|
|
8
|
+
return results;
|
|
9
|
+
}
|
|
10
|
+
const list = fs.readdirSync(dir);
|
|
11
|
+
for (const file of list) {
|
|
12
|
+
const fullPath = path.join(dir, file);
|
|
13
|
+
const stat = fs.statSync(fullPath);
|
|
14
|
+
if (stat && stat.isDirectory()) {
|
|
15
|
+
if (file !== 'tests' && file !== 'node_modules' && file !== '.astro') {
|
|
16
|
+
results.push(...getFiles(fullPath));
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
results.push(fullPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isContentFile(filePath: string): boolean {
|
|
26
|
+
return /\\i18n\\/.test(filePath) || filePath.endsWith('bibliography.ts');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const srcDir = path.join(process.cwd(), 'src');
|
|
30
|
+
const scriptsDir = path.join(process.cwd(), 'scripts');
|
|
31
|
+
const filesToTest = [
|
|
32
|
+
...getFiles(srcDir).filter(isContentFile),
|
|
33
|
+
...getFiles(scriptsDir).filter(isContentFile),
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const aiTypographyGarbage = [
|
|
37
|
+
'\u2013',
|
|
38
|
+
'\u2014',
|
|
39
|
+
'\u2026',
|
|
40
|
+
'\u201C',
|
|
41
|
+
'\u201D',
|
|
42
|
+
'\u2018',
|
|
43
|
+
'\u2019',
|
|
44
|
+
'\u00AB',
|
|
45
|
+
'\u00BB',
|
|
46
|
+
'\u200B',
|
|
47
|
+
'\u201E',
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
describe('Typography Garbage Character Validation', () => {
|
|
51
|
+
filesToTest.forEach((filePath) => {
|
|
52
|
+
const relativePath = path.relative(process.cwd(), filePath);
|
|
53
|
+
it(`should not contain typography garbage characters in ${relativePath}`, () => {
|
|
54
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
55
|
+
const hasAiPatterns = aiTypographyGarbage.some(char => content.includes(char));
|
|
56
|
+
expect(hasAiPatterns).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it(`should not contain space before colon in ${relativePath}`, () => {
|
|
60
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
61
|
+
const spaceBeforeColon = / : /.test(content);
|
|
62
|
+
expect(spaceBeforeColon).toBe(false);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it(`should not contain double hyphen in ${relativePath}`, () => {
|
|
66
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
67
|
+
const sanitized = content
|
|
68
|
+
.replace(/-----(BEGIN|END) [A-Z ]+-----/g, '')
|
|
69
|
+
.replace(/--step-\w+/g, '');
|
|
70
|
+
expect(sanitized).not.toContain('--');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ToolExportValidationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function validateComponentType(
|
|
9
|
+
toolId: string,
|
|
10
|
+
componentName: string,
|
|
11
|
+
component: unknown,
|
|
12
|
+
failures: string[],
|
|
13
|
+
): void {
|
|
14
|
+
if (typeof component !== 'function') {
|
|
15
|
+
failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function validateComponentExecution(
|
|
20
|
+
toolId: string,
|
|
21
|
+
componentName: string,
|
|
22
|
+
fn: () => Promise<unknown>,
|
|
23
|
+
failures: string[],
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const result = await fn();
|
|
27
|
+
if (!result || typeof result !== 'object') {
|
|
28
|
+
failures.push(`${toolId}: ${componentName} import returned invalid result`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
|
|
36
|
+
const failures: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
|
|
40
|
+
validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
|
|
41
|
+
validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
|
|
42
|
+
|
|
43
|
+
const componentFn = tool.Component as () => Promise<unknown>;
|
|
44
|
+
const seoFn = tool.SEOComponent as () => Promise<unknown>;
|
|
45
|
+
const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
|
|
46
|
+
|
|
47
|
+
await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
|
|
48
|
+
await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
|
|
49
|
+
await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
passed: failures.length === 0,
|
|
54
|
+
failures,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { validateToolExports } from './shared-test-helpers';
|
|
4
|
+
|
|
5
|
+
describe('Tool Exports Pattern Validation', () => {
|
|
6
|
+
describe('Component Exports Format', () => {
|
|
7
|
+
ALL_TOOLS.forEach((tool) => {
|
|
8
|
+
it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
|
|
9
|
+
expect(typeof tool.Component).toBe('function');
|
|
10
|
+
expect(tool.Component).toBeInstanceOf(Function);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
|
|
14
|
+
expect(typeof tool.SEOComponent).toBe('function');
|
|
15
|
+
expect(tool.SEOComponent).toBeInstanceOf(Function);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
|
|
19
|
+
expect(typeof tool.BibliographyComponent).toBe('function');
|
|
20
|
+
expect(tool.BibliographyComponent).toBeInstanceOf(Function);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Dynamic Import Validation', () => {
|
|
26
|
+
it('all tools must have functional dynamic imports', async () => {
|
|
27
|
+
const result = await validateToolExports(ALL_TOOLS);
|
|
28
|
+
if (!result.passed) {
|
|
29
|
+
throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
|
|
30
|
+
}
|
|
31
|
+
expect(result.passed).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -137,7 +137,7 @@ export const content: ToolLocaleContent<AspectRatioUI> = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'paragraph',
|
|
140
|
-
html: 'Falsch gehandhabte Seitenverhältnisse sind eine häufige Ursache für gestauchte Fotos, Videos mit unerwartetem Beschnitt (Letterboxing) oder Webkomponenten, deren Layout beim Betrachten auf unterschiedlichen Geräten
|
|
140
|
+
html: 'Falsch gehandhabte Seitenverhältnisse sind eine häufige Ursache für gestauchte Fotos, Videos mit unerwartetem Beschnitt (Letterboxing) oder Webkomponenten, deren Layout beim Betrachten auf unterschiedlichen Geräten - vom Smartphone bis hin zu Ultra-Wide-Monitoren - fehlerhaft dargestellt wird.',
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
type: 'title',
|
|
@@ -21,7 +21,7 @@ const faqData = [
|
|
|
21
21
|
{
|
|
22
22
|
question: "Comment calculer la hauteur à partir de la largeur avec un ratio donné ?",
|
|
23
23
|
answer:
|
|
24
|
-
"La formule est
|
|
24
|
+
"La formule est: Hauteur = Largeur × (Ratio Hauteur / Ratio Largeur). Par exemple, pour une largeur de 1280px avec un ratio 16:9, la hauteur serait: 1280 × (9/16) = 720px.",
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
question: "Quel est le Ratio de Format standard pour les vidéos YouTube ?",
|
|
@@ -42,7 +42,7 @@ const howToData = [
|
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
name: 'Dostosuj skalę',
|
|
45
|
-
text: 'Zmień wartość szerokości lub wysokości w sekcji
|
|
45
|
+
text: 'Zmień wartość szerokości lub wysokości w sekcji "Rzeczywista skala". Narzędzie automatycznie obliczy drugą wartość, aby zachować proporcje.',
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
name: 'Sprawdź podgląd',
|
|
@@ -137,7 +137,7 @@ export const content: ToolLocaleContent<AspectRatioUI> = {
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
type: 'paragraph',
|
|
140
|
-
html: 'Błędne zarządzanie proporcjami jest częstą przyczyną spłaszczonych zdjęć, filmów z nieoczekiwanym przycięciem (letterboxing) lub komponentów stron internetowych, które tracą swój układ przy przeglądaniu na różnych urządzeniach
|
|
140
|
+
html: 'Błędne zarządzanie proporcjami jest częstą przyczyną spłaszczonych zdjęć, filmów z nieoczekiwanym przycięciem (letterboxing) lub komponentów stron internetowych, które tracą swój układ przy przeglądaniu na różnych urządzeniach - od telefonów po monitory panoramiczne.',
|
|
141
141
|
},
|
|
142
142
|
{
|
|
143
143
|
type: 'title',
|