@jjlmoya/utils-converters 1.19.0 → 1.21.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-converters",
3
- "version": "1.19.0",
3
+ "version": "1.21.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,153 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const STRUCTURAL_KEYS = new Set([
5
+ 'id',
6
+ 'slug',
7
+ 'url',
8
+ 'icon',
9
+ 'image',
10
+ 'imageUrl',
11
+ 'keywords',
12
+ 'category',
13
+ 'tags',
14
+ 'toolId',
15
+ 'type',
16
+ 'locale',
17
+ 'language',
18
+ 'direction',
19
+ ]);
20
+
21
+ const TRANSLATABLE_KEYS = [
22
+ 'title',
23
+ 'description',
24
+ 'faqTitle',
25
+ 'faq',
26
+ 'howTo',
27
+ 'seo',
28
+ 'schemas',
29
+ ] as const;
30
+
31
+ const SPANISH_MARKERS = [
32
+ ['sangre', /\bsangre\b/gi],
33
+ ['molino', /\bmolino\b/gi],
34
+ ['grano', /\bgrano\b/gi],
35
+ ['paladar', /\bpaladar\b/gi],
36
+ ['cuchillas', /\bcuchillas\b/gi],
37
+ ['trozos', /\btrozos\b/gi],
38
+ ['frescura', /\bfrescura\b/gi],
39
+ ['ingresa', /\bingresa\b/gi],
40
+ ['selecciona', /\bselecciona\b/gi],
41
+ ['herramienta', /\bherramienta\b/gi],
42
+ ['según', /\bsegún\b/gi],
43
+ ['después', /\bdespués\b/gi],
44
+ ['puedes', /\bpuedes\b/gi],
45
+ ['debes', /\bdebes\b/gi],
46
+ ['tus', /\btus\b/gi],
47
+ ['a la vez', /\ba la vez\b/gi],
48
+ ['los datos', /\blos datos\b/gi],
49
+ ['las opciones', /\blas opciones\b/gi],
50
+ ['el resultado', /\bel resultado\b/gi],
51
+ ['método de extracción', /\bmétodo de extracción\b/gi],
52
+ ['uniformidad del molino', /\buniformidad del molino\b/gi],
53
+ ['café recién tostado', /\bcafé recién tostado\b/gi],
54
+ ] as const;
55
+
56
+ type UnknownRecord = Record<string, unknown>;
57
+
58
+ function normalize(value: string): string {
59
+ return value
60
+ .replace(/<[^>]*>/g, ' ')
61
+ .replace(/&nbsp;/gi, ' ')
62
+ .replace(/\s+/g, ' ')
63
+ .trim()
64
+ .toLocaleLowerCase('es');
65
+ }
66
+
67
+ function isTechnicalInvariant(text: string): boolean {
68
+ return [
69
+ 'data:image/svg+xml;base64',
70
+ 'background-image: url',
71
+ '.layout-playground {',
72
+ 'const samplerate =',
73
+ ].some((pattern) => text.includes(pattern)) || (text.includes('presets') && text.includes('hz'));
74
+ }
75
+
76
+ function collectString(value: string, output: string[]): void {
77
+ const text = normalize(value);
78
+ if (!isTechnicalInvariant(text) && text.length >= 20) output.push(text);
79
+ }
80
+
81
+ function collectObject(value: UnknownRecord, output: string[]): void {
82
+ Object.entries(value).forEach(([childKey, childValue]) => {
83
+ collectText(childValue, output, childKey);
84
+ });
85
+ }
86
+
87
+ function collectText(value: unknown, output: string[], key?: string): void {
88
+ if (key && STRUCTURAL_KEYS.has(key)) return;
89
+ if (typeof value === 'string') return collectString(value, output);
90
+ if (Array.isArray(value)) return value.forEach((item) => collectText(item, output));
91
+ if (value && typeof value === 'object') collectObject(value as UnknownRecord, output);
92
+ }
93
+
94
+ async function loadText(loader: unknown): Promise<string[]> {
95
+ if (typeof loader !== 'function') return [];
96
+ const module = await (loader as () => Promise<unknown>)();
97
+ const output: string[] = [];
98
+ if (module && typeof module === 'object') {
99
+ const record = module as UnknownRecord;
100
+ for (const key of TRANSLATABLE_KEYS) {
101
+ collectText(record[key], output, key);
102
+ }
103
+ }
104
+ return output;
105
+ }
106
+
107
+ function findSpanishMarkers(text: string[]): string[] {
108
+ const corpus = text.join(' ');
109
+ return SPANISH_MARKERS.flatMap(([label, pattern]) =>
110
+ pattern.test(corpus) ? [label] : [],
111
+ );
112
+ }
113
+
114
+ function findCopiedFragments(spanish: string[], translated: string[]): string[] {
115
+ const corpus = translated.join(' ');
116
+ return spanish
117
+ .filter((fragment) => fragment.length >= 80 && corpus.includes(fragment))
118
+ .sort((a, b) => b.length - a.length)
119
+ .slice(0, 3);
120
+ }
121
+
122
+ describe('Locales must not contain copied Spanish content', () => {
123
+ for (const entry of ALL_ENTRIES) {
124
+ it(`${entry.id} has no untranslated Spanish blocks`, async () => {
125
+ const spanish = await loadText(entry.i18n.es);
126
+ const failures: string[] = [];
127
+
128
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
129
+ if (locale === 'es') continue;
130
+
131
+ const translated = await loadText(loader);
132
+ const copiedFragments = findCopiedFragments(spanish, translated);
133
+ const markerHits = findSpanishMarkers(translated);
134
+
135
+ if (copiedFragments.length > 0 || markerHits.length >= 2) {
136
+ const details = [
137
+ copiedFragments.length > 0
138
+ ? `copied fragments: ${copiedFragments
139
+ .map((fragment) => JSON.stringify(fragment.slice(0, 120)))
140
+ .join(', ')}`
141
+ : '',
142
+ markerHits.length >= 2 ? `Spanish markers: ${markerHits.join(', ')}` : '',
143
+ ]
144
+ .filter(Boolean)
145
+ .join('; ');
146
+ failures.push(`${locale}: ${details}`);
147
+ }
148
+ }
149
+
150
+ expect(failures, failures.join('\n')).toEqual([]);
151
+ });
152
+ }
153
+ });
@@ -0,0 +1,123 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { ALL_ENTRIES } from '../entries';
3
+
4
+ const COPY_THRESHOLD = 0.9;
5
+
6
+ const STRUCTURAL_KEYS = new Set([
7
+ '@context',
8
+ '@type',
9
+ 'applicationCategory',
10
+ 'columns',
11
+ 'highlight',
12
+ 'icon',
13
+ 'level',
14
+ 'operatingSystem',
15
+ 'position',
16
+ 'positive',
17
+ 'price',
18
+ 'priceCurrency',
19
+ 'slug',
20
+ 'trend',
21
+ 'type',
22
+ 'url',
23
+ 'value',
24
+ 'variant',
25
+ ]);
26
+
27
+ function normalizeText(value: string): string {
28
+ return value
29
+ .replace(/<[^>]*>/g, ' ')
30
+ .replace(/&(?:amp|lt|gt|quot|apos|nbsp);/gi, ' ')
31
+ .replace(/[\u2018\u2019]/g, "'")
32
+ .replace(/[\u201c\u201d]/g, '"')
33
+ .replace(/\s+/g, ' ')
34
+ .trim()
35
+ .toLocaleLowerCase();
36
+ }
37
+
38
+ function collectText(value: unknown, path: string, parts: string[]): void {
39
+ if (typeof value === 'string') {
40
+ const normalized = normalizeText(value);
41
+ if (normalized.length >= 2) parts.push(normalized);
42
+ return;
43
+ }
44
+
45
+ if (Array.isArray(value)) {
46
+ value.forEach((item, index) => collectText(item, `${path}[${index}]`, parts));
47
+ return;
48
+ }
49
+
50
+ if (!value || typeof value !== 'object') return;
51
+
52
+ Object.entries(value).forEach(([key, child]) => {
53
+ if (STRUCTURAL_KEYS.has(key)) return;
54
+ collectText(child, `${path}.${key}`, parts);
55
+ });
56
+ }
57
+
58
+ function localeCorpus(content: unknown): string {
59
+ if (!content || typeof content !== 'object') return '';
60
+
61
+ const record = content as Record<string, unknown>;
62
+ const parts: string[] = [];
63
+ collectText(record.title, 'title', parts);
64
+ collectText(record.description, 'description', parts);
65
+ collectText(record.faqTitle, 'faqTitle', parts);
66
+ collectText(record.faq, 'faq', parts);
67
+ collectText(record.seo, 'seo', parts);
68
+ collectText(record.schemas, 'schemas', parts);
69
+ return parts.join(' ');
70
+ }
71
+
72
+ function tokenCounts(text: string): Map<string, number> {
73
+ const counts = new Map<string, number>();
74
+ for (const token of text.match(/[\p{L}\p{N}]+/gu) ?? []) {
75
+ counts.set(token, (counts.get(token) ?? 0) + 1);
76
+ }
77
+ return counts;
78
+ }
79
+
80
+ function copySimilarity(left: string, right: string): number {
81
+ const leftCounts = tokenCounts(left);
82
+ const rightCounts = tokenCounts(right);
83
+ const leftTotal = [...leftCounts.values()].reduce((sum, count) => sum + count, 0);
84
+ const rightTotal = [...rightCounts.values()].reduce((sum, count) => sum + count, 0);
85
+ if (leftTotal === 0 || rightTotal === 0) return 0;
86
+
87
+ let shared = 0;
88
+ for (const [token, count] of leftCounts) {
89
+ shared += Math.min(count, rightCounts.get(token) ?? 0);
90
+ }
91
+ return (2 * shared) / (leftTotal + rightTotal);
92
+ }
93
+
94
+ describe('Locales must not copy another locale wholesale', () => {
95
+ ALL_ENTRIES.forEach((entry) => {
96
+ it(`${entry.id} is not at least ${COPY_THRESHOLD * 100}% identical to another locale`, async () => {
97
+ const corpora = new Map<string, string>();
98
+
99
+ for (const [locale, loader] of Object.entries(entry.i18n)) {
100
+ if (!loader) continue;
101
+ corpora.set(locale, localeCorpus(await loader()));
102
+ }
103
+
104
+ const locales = [...corpora.keys()];
105
+ const violations: string[] = [];
106
+
107
+ for (let leftIndex = 0; leftIndex < locales.length; leftIndex += 1) {
108
+ for (let rightIndex = leftIndex + 1; rightIndex < locales.length; rightIndex += 1) {
109
+ const left = locales[leftIndex];
110
+ const right = locales[rightIndex];
111
+ const similarity = copySimilarity(corpora.get(left) ?? '', corpora.get(right) ?? '');
112
+
113
+ if (similarity >= COPY_THRESHOLD) {
114
+ violations.push(`${left} ↔ ${right}: ${(similarity * 100).toFixed(1)}%`);
115
+ }
116
+ }
117
+ }
118
+
119
+ expect(violations, `Locale copy threshold exceeded in ${entry.id}`).toEqual([]);
120
+ });
121
+ });
122
+ });
123
+
@@ -1,4 +1,5 @@
1
1
  import { bibliography } from '../bibliography';
2
+ import { buildPngJpgSeo } from '../seo';
2
3
  import type { ImageConverterUI } from '../../../shared/ImageConverter.astro';
3
4
  import { generateSchemas } from '../../../shared/logic/schemas';
4
5
  import type { PngAJpgLocaleContent } from '../index';
@@ -21,24 +22,24 @@ const ui: ImageConverterUI = {
21
22
 
22
23
  const faq: PngAJpgLocaleContent['faq'] = [
23
24
  {
24
- question: '¿Por qué elegir nuestro convertidor local de PNG a JPG?',
25
+ question: 'Warum unseren lokalen PNG-zu-JPG-Konverter wählen?',
25
26
  answer:
26
- 'A diferencia de las herramientas convencionales, nuestra utilidad procesa los archivos íntegramente en tu navegador. Tus imágenes nunca tocan un disco duro ajeno, garantizando la total soberanía y privacidad de tus datos.',
27
+ 'Im Gegensatz zu herkömmlichen Tools verarbeitet diese Anwendung Dateien vollständig in Ihrem Browser. Ihre Bilder verlassen Ihr Gerät nie und bleiben unter Ihrer Kontrolle.',
27
28
  },
28
29
  {
29
- question: '¿JPG o PNG? ¿Cuál es mejor para mi caso?',
30
+ question: 'JPG oder PNG: Welches Format ist besser?',
30
31
  answer:
31
- 'El PNG es ideal para logotipos y elementos con transparencia. Sin embargo, el JPG es el estándar de oro para fotografías y banners web, ya que logra pesos mucho más reducidos, mejorando drásticamente la velocidad de carga de un sitio.',
32
+ 'PNG eignet sich für Logos und Transparenz. JPG ist dagegen ideal für Fotos und Webbanner, weil die Dateien deutlich kleiner werden und Webseiten schneller laden.',
32
33
  },
33
34
  {
34
- question: '¿Cómo funciona la conversión técnica sin subir nada?',
35
+ question: 'Wie funktioniert die Konvertierung ohne Upload?',
35
36
  answer:
36
- 'Utilizamos la potencia del Canvas de HTML5. El navegador recrea la imagen en un lienzo virtual invisible, rellena las transparencias con blanco y genera un flujo de bytes que descargas directamente al instante.',
37
+ 'Die Anwendung nutzt HTML5 Canvas. Der Browser zeichnet das Bild in einem unsichtbaren Arbeitsbereich, ersetzt Transparenz durch Weiß und erzeugt die JPG-Datei direkt auf Ihrem Gerät.',
37
38
  },
38
39
  {
39
- question: '¿Es seguro para documentos confidenciales?',
40
+ question: 'Ist der Konverter für vertrauliche Dokumente sicher?',
40
41
  answer:
41
- 'Sí, es la opción más segura para profesionales de la banca, salud o derecho. Al ser una "caja negra" que solo usa tu RAM, lo que pasa dentro muere al cerrar la pestaña, evitando filtraciones en nubes de terceros.',
42
+ 'Ja. Die Verarbeitung findet ausschließlich im Arbeitsspeicher Ihres Geräts statt. Beim Schließen des Tabs bleiben keine Dateien auf einem fremden Server zurück.',
42
43
  },
43
44
  ];
44
45
 
@@ -57,116 +58,7 @@ const howTo: PngAJpgLocaleContent['howTo'] = [
57
58
  },
58
59
  ];
59
60
 
60
- const seo: PngAJpgLocaleContent['seo'] = [
61
- {
62
- type: 'title',
63
- text: 'Convertidor de PNG a JPG: La Guía Definitiva para la Optimización de Imágenes',
64
- level: 2,
65
- },
66
- {
67
- type: 'paragraph',
68
- html: 'En el mundo del diseño digital y el desarrollo web, la eficiencia lo es todo. El formato PNG (Portable Network Graphics) es amado por su capacidad de mantener transparencias y su compresión sin pérdida, pero tiene un gran enemigo: el peso del archivo. Cuando necesitas que tu sitio web vuele o que tus emails carguen instantáneamente, el paso de PNG a JPG es la decisión técnica más inteligente.',
69
- },
70
- {
71
- type: 'title',
72
- text: '¿JPG o PNG?',
73
- level: 3,
74
- },
75
- {
76
- type: 'paragraph',
77
- html: 'No existe un formato mejor que otro, sino una herramienta para cada necesidad. El PNG es un formato sin pérdida, ideal para maquetas de interfaces, logotipos con texto pequeño y elementos visuales que requieren un fondo transparente. Sin embargo, esta fidelidad tiene un coste: archivos que pueden ser 5 o 10 veces más pesados que su equivalente comprimido.',
78
- },
79
- {
80
- type: 'paragraph',
81
- html: 'El JPG (Joint Photographic Experts Group), por otro lado, utiliza algoritmos de discretización para eliminar información que el ojo humano apenas percibe, logrando pesos pluma. Es el estándar de oro para fotografías, banners publicitarios y redes sociales. Al convertir tus PNG a JPG, estás traduciendo fidelidad geométrica por velocidad de red.',
82
- },
83
- {
84
- type: 'title',
85
- text: 'Comparativa de Arquitectura: Local vs Nube',
86
- level: 3,
87
- },
88
- {
89
- type: 'comparative',
90
- items: [
91
- {
92
- title: 'Convertidores Cloud',
93
- description: 'Herramientas tradicionales que suben tus fotos a un servidor remoto.',
94
- icon: 'mdi:cloud-upload',
95
- pointIcon: 'mdi:close-circle-outline',
96
- points: [
97
- 'Latencia de red (Upload/Download)',
98
- 'Riesgo de filtración de datos privados',
99
- 'Límites de tamaño por archivo',
100
- 'Publicidad y rastreadores',
101
- ],
102
- },
103
- {
104
- title: 'Nuestra Arquitectura Local',
105
- description: 'Procesamiento directo en tu hardware mediante tecnología Vanilla JS.',
106
- icon: 'mdi:laptop-mac',
107
- highlight: true,
108
- points: [
109
- 'Velocidad instantánea sin red',
110
- 'Privacidad garantizada (0 bytes enviados)',
111
- 'Sin límites de MB por archivo',
112
- 'Interfaz profesional y limpia',
113
- ],
114
- },
115
- ],
116
- },
117
- {
118
- type: 'title',
119
- text: 'Cómo funciona la conversión técnica',
120
- level: 3,
121
- },
122
- {
123
- type: 'paragraph',
124
- html: 'Probablemente te preguntes cómo es posible convertir una imagen sin enviarla a un servidor. La magia reside en la potencia de los navegadores modernos. Cuando seleccionas un archivo, generamos un Blob que solo existe en tu RAM. Ese Blob se dibuja en un elemento HTML5 Canvas invisible.',
125
- },
126
- {
127
- type: 'paragraph',
128
- html: 'Dado que el JPG no soporta transparencias, nuestro algoritmo rellena el fondo con un color blanco sólido antes de "pintar" el PNG encima. Una vez compuesta la imagen, ejecutamos el método de exportación nativo, generando un flujo de bytes que tu ordenador descarga directamente.',
129
- },
130
- {
131
- type: 'tip',
132
- title: 'Consejo SEO: El Peso Ideal',
133
- html: 'Google penaliza activamente los sitios web lentos. Si tu Largest Contentful Paint (LCP) es alto por culpa de un PNG de cabecera de 2MB, convertirlo a un JPG de 200KB puede mejorar tus métricas de PageSpeed instantáneamente sin diferencias visuales.',
134
- },
135
- {
136
- type: 'title',
137
- text: 'Seguridad para Empresas y Profesionales',
138
- level: 3,
139
- },
140
- {
141
- type: 'paragraph',
142
- html: 'Si trabajas en sectores sensibles como la banca, la salud o el derecho, subir archivos a conversores online es una violación de seguridad. Nuestra herramienta funciona como una "caja negra": lo que pasa dentro se queda en tu RAM. Es la única forma segura de trabajar con documentos confidenciales.',
143
- },
144
- {
145
- type: 'title',
146
- text: 'Compatibilidad del Resultado',
147
- level: 3,
148
- },
149
- {
150
- type: 'list',
151
- icon: 'mdi:check-circle',
152
- items: [
153
- 'Visores de Windows, macOS y dispositivos móviles.',
154
- 'Redes sociales (Instagram, LinkedIn, etc).',
155
- 'Herramientas de ofimática (Word, PowerPoint).',
156
- 'Gestores de contenido (WordPress, Shopify).',
157
- ],
158
- },
159
- {
160
- type: 'title',
161
- text: 'Conclusión: Optimiza como un Pro',
162
- level: 3,
163
- },
164
- {
165
- type: 'paragraph',
166
- html: 'Este convertidor no es solo una página más; es una pieza de ingeniería diseñada para facilitarte la vida. Ya seas un desarrollador o un usuario doméstico, aquí tienes la solución definitiva para ahorrar megabytes y mantener tus datos a salvo.',
167
- },
168
- ];
169
-
61
+ const seo = buildPngJpgSeo({ title, description, faq, howTo });
170
62
 
171
63
  export const content: PngAJpgLocaleContent = {
172
64
  slug,
@@ -21,24 +21,24 @@ const ui: ImageConverterUI = {
21
21
 
22
22
  const faq: PngAJpgLocaleContent['faq'] = [
23
23
  {
24
- question: '¿Por qué elegir nuestro convertidor local de PNG a JPG?',
24
+ question: 'Pourquoi choisir notre convertisseur local PNG vers JPG ?',
25
25
  answer:
26
- 'A diferencia de las herramientas convencionales, nuestra utilidad procesa los archivos íntegramente en tu navegador. Tus imágenes nunca tocan un disco duro ajeno, garantizando la total soberanía y privacidad de tus datos.',
26
+ 'Contrairement aux services classiques, cet outil traite les fichiers entièrement dans le navigateur. Vos images ne touchent jamais un disque distant: vous gardez le contrôle et la confidentialité de vos données.',
27
27
  },
28
28
  {
29
- question: '¿JPG o PNG? ¿Cuál es mejor para mi caso?',
29
+ question: 'JPG ou PNG: quel format choisir ?',
30
30
  answer:
31
- 'El PNG es ideal para logotipos y elementos con transparencia. Sin embargo, el JPG es el estándar de oro para fotografías y banners web, ya que logra pesos mucho más reducidos, mejorando drásticamente la velocidad de carga de un sitio.',
31
+ 'Le PNG convient aux logos et aux éléments transparents. Le JPG est préférable pour les photos et les bannières web, car ses fichiers sont bien plus légers et accélèrent le chargement des pages.',
32
32
  },
33
33
  {
34
- question: '¿Cómo funciona la conversión técnica sin subir nada?',
34
+ question: 'Comment convertir une image sans rien envoyer ?',
35
35
  answer:
36
- 'Utilizamos la potencia del Canvas de HTML5. El navegador recrea la imagen en un lienzo virtual invisible, rellena las transparencias con blanco y genera un flujo de bytes que descargas directamente al instante.',
36
+ "Nous utilisons le Canvas HTML5. Le navigateur recrée l'image dans une zone virtuelle invisible, remplit la transparence en blanc et produit directement les octets JPEG à télécharger.",
37
37
  },
38
38
  {
39
- question: '¿Es seguro para documentos confidenciales?',
39
+ question: 'Est-ce sûr pour des documents confidentiels ?',
40
40
  answer:
41
- 'Sí, es la opción más segura para profesionales de la banca, salud o derecho. Al ser una "caja negra" que solo usa tu RAM, lo que pasa dentro muere al cerrar la pestaña, evitando filtraciones en nubes de terceros.',
41
+ "Oui. Pour la banque, la santé ou le droit, il est préférable de ne pas envoyer les images à un service externe: le traitement reste dans la RAM et s'arrête à la fermeture de l'onglet.",
42
42
  },
43
43
  ];
44
44
 
@@ -60,110 +60,110 @@ const howTo: PngAJpgLocaleContent['howTo'] = [
60
60
  const seo: PngAJpgLocaleContent['seo'] = [
61
61
  {
62
62
  type: 'title',
63
- text: 'Convertidor de PNG a JPG: La Guía Definitiva para la Optimización de Imágenes',
63
+ text: 'Convertisseur PNG vers JPG: guide pratique d\'optimisation des images',
64
64
  level: 2,
65
65
  },
66
66
  {
67
67
  type: 'paragraph',
68
- html: 'En el mundo del diseño digital y el desarrollo web, la eficiencia lo es todo. El formato PNG (Portable Network Graphics) es amado por su capacidad de mantener transparencias y su compresión sin pérdida, pero tiene un gran enemigo: el peso del archivo. Cuando necesitas que tu sitio web vuele o que tus emails carguen instantáneamente, el paso de PNG a JPG es la decisión técnica más inteligente.',
68
+ html: 'Dans le design numérique et le développement web, la rapidité compte. Le PNG conserve la transparence et la qualité sans perte, mais produit parfois des fichiers lourds. Pour des pages rapides et des pièces jointes légères, le JPG est souvent plus adapté.',
69
69
  },
70
70
  {
71
71
  type: 'title',
72
- text: '¿JPG o PNG?',
72
+ text: 'JPG ou PNG: lequel choisir ?',
73
73
  level: 3,
74
74
  },
75
75
  {
76
76
  type: 'paragraph',
77
- html: 'No existe un formato mejor que otro, sino una herramienta para cada necesidad. El PNG es un formato sin pérdida, ideal para maquetas de interfaces, logotipos con texto pequeño y elementos visuales que requieren un fondo transparente. Sin embargo, esta fidelidad tiene un coste: archivos que pueden ser 5 o 10 veces más pesados que su equivalente comprimido.',
77
+ html: "Aucun format n'est meilleur dans tous les cas. Le PNG convient aux interfaces, logos et fonds transparents, mais sa compression sans perte peut donner des fichiers beaucoup plus volumineux.",
78
78
  },
79
79
  {
80
80
  type: 'paragraph',
81
- html: 'El JPG (Joint Photographic Experts Group), por otro lado, utiliza algoritmos de discretización para eliminar información que el ojo humano apenas percibe, logrando pesos pluma. Es el estándar de oro para fotografías, banners publicitarios y redes sociales. Al convertir tus PNG a JPG, estás traduciendo fidelidad geométrica por velocidad de red.',
81
+ html: "Le JPG supprime les détails peu visibles et produit des images légères. Il convient aux photos, bannières et réseaux sociaux: une légère perte en échange d'un chargement plus rapide.",
82
82
  },
83
83
  {
84
84
  type: 'title',
85
- text: 'Comparativa de Arquitectura: Local vs Nube',
85
+ text: 'Comparaison technique: traitement local et cloud',
86
86
  level: 3,
87
87
  },
88
88
  {
89
89
  type: 'comparative',
90
90
  items: [
91
91
  {
92
- title: 'Convertidores Cloud',
93
- description: 'Herramientas tradicionales que suben tus fotos a un servidor remoto.',
92
+ title: 'Convertisseurs cloud',
93
+ description: 'Services classiques qui envoient vos photos vers un serveur distant.',
94
94
  icon: 'mdi:cloud-upload',
95
95
  pointIcon: 'mdi:close-circle-outline',
96
96
  points: [
97
- 'Latencia de red (Upload/Download)',
98
- 'Riesgo de filtración de datos privados',
99
- 'Límites de tamaño por archivo',
100
- 'Publicidad y rastreadores',
97
+ 'Latence réseau (envoi/téléchargement)',
98
+ 'Risque de fuite de données privées',
99
+ 'Limite de taille par fichier',
100
+ 'Publicités et traceurs',
101
101
  ],
102
102
  },
103
103
  {
104
- title: 'Nuestra Arquitectura Local',
105
- description: 'Procesamiento directo en tu hardware mediante tecnología Vanilla JS.',
104
+ title: 'Notre architecture locale',
105
+ description: 'Traitement direct sur votre appareil avec du JavaScript natif.',
106
106
  icon: 'mdi:laptop-mac',
107
107
  highlight: true,
108
108
  points: [
109
- 'Velocidad instantánea sin red',
110
- 'Privacidad garantizada (0 bytes enviados)',
111
- 'Sin límites de MB por archivo',
112
- 'Interfaz profesional y limpia',
109
+ 'Vitesse immédiate sans réseau',
110
+ 'Confidentialité garantie (zéro octet envoyé)',
111
+ 'Aucune limite de Mo par fichier',
112
+ 'Interface professionnelle et claire',
113
113
  ],
114
114
  },
115
115
  ],
116
116
  },
117
117
  {
118
118
  type: 'title',
119
- text: 'Cómo funciona la conversión técnica',
119
+ text: 'Fonctionnement de la conversion technique',
120
120
  level: 3,
121
121
  },
122
122
  {
123
123
  type: 'paragraph',
124
- html: 'Probablemente te preguntes cómo es posible convertir una imagen sin enviarla a un servidor. La magia reside en la potencia de los navegadores modernos. Cuando seleccionas un archivo, generamos un Blob que solo existe en tu RAM. Ese Blob se dibuja en un elemento HTML5 Canvas invisible.',
124
+ html: "La conversion sans serveur s'appuie sur les API modernes du navigateur. Le fichier devient un Blob temporaire en RAM, puis il est dessiné dans un Canvas HTML5 invisible.",
125
125
  },
126
126
  {
127
127
  type: 'paragraph',
128
- html: 'Dado que el JPG no soporta transparencias, nuestro algoritmo rellena el fondo con un color blanco sólido antes de "pintar" el PNG encima. Una vez compuesta la imagen, ejecutamos el método de exportación nativo, generando un flujo de bytes que tu ordenador descarga directamente.',
128
+ html: "Le JPG ne gère pas la transparence: les zones transparentes sont donc remplies de blanc avant le rendu. Le Canvas génère ensuite le flux JPEG enregistré directement sur l'appareil.",
129
129
  },
130
130
  {
131
131
  type: 'tip',
132
- title: 'Consejo SEO: El Peso Ideal',
133
- html: 'Google penaliza activamente los sitios web lentos. Si tu Largest Contentful Paint (LCP) es alto por culpa de un PNG de cabecera de 2MB, convertirlo a un JPG de 200KB puede mejorar tus métricas de PageSpeed instantáneamente sin diferencias visuales.',
132
+ title: 'Conseil SEO: le poids idéal',
133
+ html: 'Une image plus légère peut améliorer le LCP et les indicateurs PageSpeed. Réduire un PNG volumineux en JPG est particulièrement utile pour les en-têtes et les pages riches en images.',
134
134
  },
135
135
  {
136
136
  type: 'title',
137
- text: 'Seguridad para Empresas y Profesionales',
137
+ text: 'Sécurité pour les entreprises et les professionnels',
138
138
  level: 3,
139
139
  },
140
140
  {
141
141
  type: 'paragraph',
142
- html: 'Si trabajas en sectores sensibles como la banca, la salud o el derecho, subir archivos a conversores online es una violación de seguridad. Nuestra herramienta funciona como una "caja negra": lo que pasa dentro se queda en tu RAM. Es la única forma segura de trabajar con documentos confidenciales.',
142
+ html: 'Pour la banque, la santé ou le droit, envoyer des fichiers à un service externe peut présenter un risque. Ici le traitement reste dans la RAM du navigateur, sans transfert vers un cloud.',
143
143
  },
144
144
  {
145
145
  type: 'title',
146
- text: 'Compatibilidad del Resultado',
146
+ text: 'Compatibilité du résultat',
147
147
  level: 3,
148
148
  },
149
149
  {
150
150
  type: 'list',
151
151
  icon: 'mdi:check-circle',
152
152
  items: [
153
- 'Visores de Windows, macOS y dispositivos móviles.',
154
- 'Redes sociales (Instagram, LinkedIn, etc).',
155
- 'Herramientas de ofimática (Word, PowerPoint).',
156
- 'Gestores de contenido (WordPress, Shopify).',
153
+ 'Visionneuses Windows, macOS et appareils mobiles.',
154
+ 'Réseaux sociaux (Instagram, LinkedIn, etc.).',
155
+ 'Outils bureautiques (Word, PowerPoint).',
156
+ 'Gestionnaires de contenu (WordPress, Shopify).',
157
157
  ],
158
158
  },
159
159
  {
160
160
  type: 'title',
161
- text: 'Conclusión: Optimiza como un Pro',
161
+ text: 'Conclusion: optimisez avec méthode',
162
162
  level: 3,
163
163
  },
164
164
  {
165
165
  type: 'paragraph',
166
- html: 'Este convertidor no es solo una página más; es una pieza de ingeniería diseñada para facilitarte la vida. Ya seas un desarrollador o un usuario doméstico, aquí tienes la solución definitiva para ahorrar megabytes y mantener tus datos a salvo.',
166
+ html: 'Ce convertisseur associe rapidité et confidentialité. Développeur ou particulier, vous pouvez alléger vos images sans remettre vos fichiers à un service tiers.',
167
167
  },
168
168
  ];
169
169