@jjlmoya/utils-finance 1.31.0 → 1.33.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-finance",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.33.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"lint": "eslint src/ --max-warnings 0 && stylelint \"src/**/*.{css,astro}\"",
|
|
28
28
|
"check": "astro check",
|
|
29
29
|
"type-check": "astro check",
|
|
30
|
-
"test": "vitest run",
|
|
30
|
+
"test": "vitest run --reporter=verbose --testTimeout=30000",
|
|
31
31
|
"preversion": "npm run lint && npm run test && npm run build",
|
|
32
32
|
"postversion": "git push && git push --tags",
|
|
33
33
|
"patch": "npm version patch",
|
package/src/category/i18n/en.ts
CHANGED
|
@@ -71,7 +71,7 @@ export const content: CategoryLocaleContent = {
|
|
|
71
71
|
{
|
|
72
72
|
type: 'tip',
|
|
73
73
|
title: 'Strategic Savings Advice',
|
|
74
|
-
html: '<p><strong>2026 Strategy:</strong> In a persistent inflation environment, keeping cash idle
|
|
74
|
+
html: '<p><strong>2026 Strategy:</strong> In a persistent inflation environment, keeping cash idle can reduce its purchasing power. Use the compound interest simulator to evaluate how small changes in return rate may affect your retirement.</p>',
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
type: 'title',
|
package/src/category/i18n/es.ts
CHANGED
|
@@ -4,7 +4,7 @@ export const content: CategoryLocaleContent = {
|
|
|
4
4
|
slug: 'finanzas',
|
|
5
5
|
title: 'Herramientas y Calculadoras Financieras de Precisión',
|
|
6
6
|
description:
|
|
7
|
-
'Optimiza tus finanzas con herramientas gratuitas online
|
|
7
|
+
'Optimiza tus finanzas con herramientas gratuitas online: hipotecas, interés compuesto, alquileres e intereses de demora para 2026.',
|
|
8
8
|
seo: [
|
|
9
9
|
{
|
|
10
10
|
type: 'title',
|
|
@@ -48,7 +48,7 @@ export const content: CategoryLocaleContent = {
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
type: 'paragraph',
|
|
51
|
-
html: 'En el ámbito legal y tributario, los plazos y porcentajes son críticos. Proporcionamos herramientas
|
|
51
|
+
html: 'En el ámbito legal y tributario, los plazos y porcentajes son críticos. Proporcionamos herramientas de referencia para calcular el <strong>interés legal del dinero (3,25% en 2026)</strong> y los intereses de demora para deudas civiles o con Hacienda. Comprueba siempre la norma vigente antes de tomar una decisión.',
|
|
52
52
|
},
|
|
53
53
|
{
|
|
54
54
|
type: 'title',
|
|
@@ -62,7 +62,7 @@ export const content: CategoryLocaleContent = {
|
|
|
62
62
|
{
|
|
63
63
|
type: 'list',
|
|
64
64
|
items: [
|
|
65
|
-
'<strong>Datos
|
|
65
|
+
'<strong>Datos de referencia:</strong> Tasas de interés, límites de IPC y variables fiscales indicadas para el ejercicio 2026.',
|
|
66
66
|
'<strong>Privacidad Total:</strong> Tus datos financieros personales se procesan estrictamente en el navegador sin almacenamiento en servidores.',
|
|
67
67
|
'<strong>Rigor Matemático:</strong> Algoritmos de amortización y cálculo de interés real siguiendo estándares bancarios europeos.',
|
|
68
68
|
'<strong>Transparencia Legal:</strong> Herramientas adaptadas a la legislación española vigente (Ley de Vivienda, Modelos Tributarios).',
|
|
@@ -71,7 +71,7 @@ export const content: CategoryLocaleContent = {
|
|
|
71
71
|
{
|
|
72
72
|
type: 'tip',
|
|
73
73
|
title: 'Consejo Estratégico de Ahorro',
|
|
74
|
-
html: '<p><strong>Estrategia 2026:</strong> En un entorno de inflación persistente, mantener el efectivo inactivo
|
|
74
|
+
html: '<p><strong>Estrategia 2026:</strong> En un entorno de inflación persistente, mantener el efectivo inactivo puede reducir su poder adquisitivo. Usa el simulador de interés compuesto para valorar cómo pequeños cambios en la rentabilidad pueden afectar a tu jubilación.</p>',
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
type: 'title',
|
package/src/category/i18n/pl.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { CategoryLocaleContent } from '../../types';
|
|
|
3
3
|
export const content: CategoryLocaleContent = {
|
|
4
4
|
slug: 'finanse',
|
|
5
5
|
title: 'Precyzyjne Narzędzia i Kalkulatory Finansowe',
|
|
6
|
-
description: 'Zoptymalizuj
|
|
6
|
+
description: 'Zoptymalizuj finanse dzięki darmowym narzędziom online: kredyty hipoteczne, odsetki składane, czynsz i odsetki za zwłokę w 2026 roku.',
|
|
7
7
|
seo: [
|
|
8
8
|
{
|
|
9
9
|
type: 'title',
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { CategoryLocaleContent } from '../types';
|
|
3
|
+
|
|
4
|
+
const EXPECTED_LOCALES = [
|
|
5
|
+
'de', 'en', 'es', 'fr', 'id', 'it', 'ja', 'ko', 'nl', 'pl', 'pt', 'ru', 'sv', 'tr', 'zh',
|
|
6
|
+
] as const;
|
|
7
|
+
const COMPACT_LOCALES = new Set(['ja', 'ko', 'zh']);
|
|
8
|
+
|
|
9
|
+
const localeModules = import.meta.glob('../category/i18n/*.ts', { eager: true }) as Record<
|
|
10
|
+
string,
|
|
11
|
+
{ content: CategoryLocaleContent }
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
function textFrom(value: unknown): string {
|
|
15
|
+
if (typeof value === 'string') return value.replace(/<[^>]*>/g, ' ');
|
|
16
|
+
if (Array.isArray(value)) return value.map(textFrom).join(' ');
|
|
17
|
+
if (value && typeof value === 'object') return Object.values(value).map(textFrom).join(' ');
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const STRONG_CLAIM = /\b(?:garantiz\w*|guarante\w*|validat(?:ed)|valid(?:ated|ado|ada|ados|adas|ée|ées)|actualizad\w*)\b/iu;
|
|
22
|
+
|
|
23
|
+
function getContents(): Map<string, CategoryLocaleContent> {
|
|
24
|
+
return new Map(
|
|
25
|
+
Object.entries(localeModules).map(([file, module]) => [file.match(/\/([a-z]{2})\.ts$/u)?.[1] ?? file, module.content]),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function validateContent(locale: string, content: CategoryLocaleContent): string[] {
|
|
30
|
+
const failures: string[] = [];
|
|
31
|
+
const check = (condition: boolean, message: string) => { if (!condition) failures.push(`${locale}: ${message}`); };
|
|
32
|
+
const minimumTitleLength = COMPACT_LOCALES.has(locale) ? 4 : 12;
|
|
33
|
+
const minimumDescriptionLength = COMPACT_LOCALES.has(locale) ? 20 : 60;
|
|
34
|
+
check(content.title.trim().length >= minimumTitleLength && content.title.trim().length <= 70, 'title must use a useful 4/12–70 character length');
|
|
35
|
+
check(content.description.trim().length >= minimumDescriptionLength && content.description.trim().length <= 180, 'description must use a useful 20/60–180 character length');
|
|
36
|
+
check(content.seo.length >= 2, 'SEO needs at least two sections');
|
|
37
|
+
check(content.seo.some((section) => section.type === 'paragraph'), 'SEO needs visible explanatory copy');
|
|
38
|
+
|
|
39
|
+
const seoText = textFrom(content.seo).replace(/\s+/g, ' ').trim();
|
|
40
|
+
const minimumSeoLength = COMPACT_LOCALES.has(locale) ? 120 : 240;
|
|
41
|
+
check(seoText.length >= minimumSeoLength, 'SEO copy must contain the minimum visible copy for its script');
|
|
42
|
+
check(content.seo.filter((section) => section.type === 'title').length >= 1, 'SEO needs a section heading');
|
|
43
|
+
check(!/[�]/u.test(seoText), 'SEO contains replacement characters');
|
|
44
|
+
check(!/ {2,}/u.test(seoText), 'SEO contains duplicated whitespace');
|
|
45
|
+
check(!STRONG_CLAIM.test(seoText), 'SEO contains an unsupported strong claim');
|
|
46
|
+
|
|
47
|
+
check(content.slug.trim().length > 0 && /^[a-z0-9-]+$/u.test(content.slug), 'slug must be a non-empty URL-safe value');
|
|
48
|
+
return failures;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('Category SEO quality contract', () => {
|
|
52
|
+
it('has one content file for every configured locale', () => {
|
|
53
|
+
const contents = getContents();
|
|
54
|
+
expect([...contents.keys()].sort()).toEqual([...EXPECTED_LOCALES].sort());
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('keeps metadata, SEO structure and copy quality above the minimum in every locale', () => {
|
|
58
|
+
const contents = getContents();
|
|
59
|
+
const english = contents.get('en');
|
|
60
|
+
expect(english, 'English category content is required as the structural reference').toBeDefined();
|
|
61
|
+
|
|
62
|
+
const failures = EXPECTED_LOCALES.flatMap((locale) => {
|
|
63
|
+
const content = contents.get(locale);
|
|
64
|
+
return content ? validateContent(locale, content) : [];
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
for (const locale of EXPECTED_LOCALES) {
|
|
68
|
+
const content = contents.get(locale);
|
|
69
|
+
if (!content) failures.push(`${locale}: category content is missing`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
expect(failures, 'category SEO quality failures').toEqual([]);
|
|
73
|
+
}, 30000);
|
|
74
|
+
});
|
|
@@ -33,7 +33,7 @@ async function verifyLocaleParity(
|
|
|
33
33
|
expect(
|
|
34
34
|
locSeoCount,
|
|
35
35
|
`Locale ${loc} SEO sections count (${locSeoCount}) must match EN (${expected.seo})`,
|
|
36
|
-
).
|
|
36
|
+
).toBeGreaterThanOrEqual(expected.seo);
|
|
37
37
|
expect(
|
|
38
38
|
locFaqCount,
|
|
39
39
|
`Locale ${loc} FAQ items count (${locFaqCount}) must match EN (${expected.faq})`,
|
|
@@ -47,7 +47,7 @@ async function verifyLocaleParity(
|
|
|
47
47
|
describe('SEO & i18n Structural Parity Suite', () => {
|
|
48
48
|
ALL_ENTRIES.forEach((entry) => {
|
|
49
49
|
describe(`Tool: ${entry.id}`, () => {
|
|
50
|
-
it('all 15 locales should
|
|
50
|
+
it('all 15 locales should provide at least the complete English SEO section set', async () => {
|
|
51
51
|
const enContent = await entry.i18n.en?.();
|
|
52
52
|
expect(enContent).toBeDefined();
|
|
53
53
|
const expected: ExpectedCounts = {
|