@jjlmoya/utils-pets 1.13.0 → 1.14.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,13 +1,15 @@
1
1
  {
2
2
  "name": "@jjlmoya/utils-pets",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
7
7
  "exports": {
8
8
  ".": "./src/index.ts",
9
9
  "./data": "./src/data.ts",
10
- "./entries": "./src/entries.ts"
10
+ "./entries": "./src/entries.ts",
11
+ "./runtime/*": "./src/tool/*/index.ts",
12
+ "./category-seo": "./src/category/seo.astro"
11
13
  },
12
14
  "files": [
13
15
  "src",
@@ -5,7 +5,7 @@ export const content: CategoryLocaleContent = {
5
5
  title: 'Outils pour Animaux de Compagnie',
6
6
  description: 'Calculatrices et utilitaires gratuits pour les propriétaires d\'animaux. Découvrez l\'âge réel de votre chien ou chat et calculez leur ration quotidienne avec précision vétérinaire.',
7
7
  seo: [
8
- { type: 'title', text: 'Nutrition et Bien-être Animal : La Science au Service de votre Animal', level: 2 },
8
+ { type: 'title', text: 'Nutrition et Bien-être Animal: La Science au Service de votre Animal', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
11
  html: 'Prendre soin d\'un chien, d\'un chat ou d\'un lapin va bien au-delà de leur donner de la nourriture et de l\'affection. Cela implique de comprendre leur biologie, leur rythme de vieillissement et leurs besoins énergétiques spécifiques. Dans cette section, nous proposons des <strong>calculatrices vétérinaires gratuites</strong> qui appliquent des formules scientifiques validées pour vous aider à prendre de meilleures décisions concernant la santé de votre compagnon.',
@@ -14,16 +14,16 @@ export const content: CategoryLocaleContent = {
14
14
  type: 'paragraph',
15
15
  html: 'De la curiosité de savoir combien d\'années humaines a réellement votre chien, jusqu\'au calcul précis de la ration quotidienne basé sur le RER (Resting Energy Requirement), nos outils transforment des concepts vétérinaires complexes en réponses claires et exploitables pour le propriétaire responsable.',
16
16
  },
17
- { type: 'title', text: 'L\'Âge Réel de votre Animal : Au-delà du Mythe du "Fois 7"', level: 2 },
17
+ { type: 'title', text: 'L\'Âge Réel de votre Animal: Au-delà du Mythe du "Fois 7"', level: 2 },
18
18
  {
19
19
  type: 'paragraph',
20
20
  html: 'La croyance populaire qu\'une année de chien équivaut à sept années humaines est démystifiée depuis des décennies par la médecine vétérinaire moderne. La réalité est que les chiens mûrissent de façon exponentielle au cours de leurs deux premières années de vie, atteignant l\'équivalent de 15 ans humains à 12 mois et de 24 ans à 24 mois. Ensuite, le rythme ralentit et dépend énormément de la <strong>taille de l\'animal</strong>.',
21
21
  },
22
22
  {
23
23
  type: 'paragraph',
24
- html: 'Les chats, quant à eux, suivent une courbe de vieillissement plus linéaire : après les deux premières années de développement accéléré, chaque année féline équivaut approximativement à quatre années humaines. Connaître le stade de vie de votre animal n\'est pas seulement une curiosité, c\'est une information médicale pertinente qui détermine le moment des bilans gériatriques et l\'ajustement du régime alimentaire.',
24
+ html: 'Les chats, quant à eux, suivent une courbe de vieillissement plus linéaire: après les deux premières années de développement accéléré, chaque année féline équivaut approximativement à quatre années humaines. Connaître le stade de vie de votre animal n\'est pas seulement une curiosité, c\'est une information médicale pertinente qui détermine le moment des bilans gériatriques et l\'ajustement du régime alimentaire.',
25
25
  },
26
- { type: 'title', text: 'Nutrition de Précision : Calculez la Ration Quotidienne Exacte', level: 2 },
26
+ { type: 'title', text: 'Nutrition de Précision: Calculez la Ration Quotidienne Exacte', level: 2 },
27
27
  {
28
28
  type: 'paragraph',
29
29
  html: 'L\'obésité touche plus de 50 % des animaux de compagnie dans les pays développés et est la cause directe de problèmes articulaires, cardiovasculaires et d\'une espérance de vie réduite. Notre calculatrice de ration quotidienne applique la formule vétérinaire standard RER (70 × poids^0.75) multipliée par le facteur DER correspondant à l\'espèce, au stade de vie et au niveau d\'activité de votre animal.',
@@ -8,11 +8,11 @@ export const content: CategoryLocaleContent = {
8
8
  { type: 'title', text: 'Питание и Благополучие Животных: Наука на Службе Вашего Питомца', level: 2 },
9
9
  {
10
10
  type: 'paragraph',
11
- html: 'Забота о собаке, кошке или кролике это гораздо больше, чем просто еда и ласка. Это требует понимания их биологии, ритма старения и специфических потребностей в энергии. В этом разделе мы предлагаем <strong>бесплатные ветеринарные калькуляторы</strong>, которые применяют проверенные научные формулы, чтобы помочь вам принимать более обоснованные решения в вопросах здоровья вашего спутника.',
11
+ html: 'Забота о собаке, кошке или кролике - это гораздо больше, чем просто еда и ласка. Это требует понимания их биологии, ритма старения и специфических потребностей в энергии. В этом разделе мы предлагаем <strong>бесплатные ветеринарные калькуляторы</strong>, которые применяют проверенные научные формулы, чтобы помочь вам принимать более обоснованные решения в вопросах здоровья вашего спутника.',
12
12
  },
13
13
  {
14
14
  type: 'paragraph',
15
- html: 'От любопытства узнать, сколько человеческих лет на самом деле вашей собаке, до точного расчета дневного рациона на основе RER (Resting Energy Requirement) наши инструменты превращают сложные ветеринарные концепции в четкие и практичные ответы для ответственного владельца домашнего животного.',
15
+ html: 'От любопытства узнать, сколько человеческих лет на самом деле вашей собаке, до точного расчета дневного рациона на основе RER (Resting Energy Requirement) - наши инструменты превращают сложные ветеринарные концепции в четкие и практичные ответы для ответственного владельца домашнего животного.',
16
16
  },
17
17
  { type: 'title', text: 'Реальный Возраст Вашего Питомца: За Пределами Мифа об "Умножении на 7"', level: 2 },
18
18
  {
@@ -21,7 +21,7 @@ export const content: CategoryLocaleContent = {
21
21
  },
22
22
  {
23
23
  type: 'paragraph',
24
- html: 'Кошки, с другой стороны, следуют более линейной кривой старения: после первых двух лет ускоренного развития каждый кошачий год примерно эквивалентен четырем человеческим годам. Знание этапа жизни вашего питомца это не просто любопытство, это важная медицинская информация: она определяет, когда планировать гериатрические осмотры, когда корректировать диету и какие признаки здоровья следует контролировать.',
24
+ html: 'Кошки, с другой стороны, следуют более линейной кривой старения: после первых двух лет ускоренного развития каждый кошачий год примерно эквивалентен четырем человеческим годам. Знание этапа жизни вашего питомца - это не просто любопытство, это важная медицинская информация: она определяет, когда планировать гериатрические осмотры, когда корректировать диету и какие признаки здоровья следует контролировать.',
25
25
  },
26
26
  { type: 'title', text: 'Точное Питание: Рассчитайте Точный Дневной Рацион', level: 2 },
27
27
  {
@@ -42,7 +42,7 @@ export const content: CategoryLocaleContent = {
42
42
  },
43
43
  {
44
44
  type: 'paragraph',
45
- html: 'Инвестиции в знания о биологии вашего питомца самая эффективная форма профилактической медицины. Здоровый вес и своевременная ветеринарная помощь могут добавить годы качественной жизни вашему верному спутнику. Научный подход к уходу гарантирует, что ваш питомец не просто выживет, но и преуспеет на каждом этапе своей жизни.',
45
+ html: 'Инвестиции в знания о биологии вашего питомца - самая эффективная форма профилактической медицины. Здоровый вес и своевременная ветеринарная помощь могут добавить годы качественной жизни вашему верному спутнику. Научный подход к уходу гарантирует, что ваш питомец не просто выживет, но и преуспеет на каждом этапе своей жизни.',
46
46
  },
47
47
  {
48
48
  type: 'paragraph',
@@ -14,7 +14,7 @@ export const content: CategoryLocaleContent = {
14
14
  type: 'paragraph',
15
15
  html: '从 好奇 想 知道 您 的 狗 实际上 有 多少 岁 的 人类 年龄 , 到 基于 RER ( 静息 能量 需求 ) 的 每日 喂食量 的 精确 计算 , 我们 的 工具 将 复杂 的 兽医学 概念 转化 为 对 负责任 的 宠物 主人 而言 清晰 、 可 操作 的 答案 。',
16
16
  },
17
- { type: 'title', text: '您 宠物 的 真实 年龄 : 超越 乘以 7 的 误区', level: 2 },
17
+ { type: 'title', text: '您 宠物 的 真实 年龄 : 超越 \" 乘以 7 \" 的 误区', level: 2 },
18
18
  {
19
19
  type: 'paragraph',
20
20
  html: '一个 狗 年 等于 七个 人类 年 的 流行 观念 几十年来 一直 被 现代 兽医学 所 否定 。 现实 情况 是 , 狗 在 生命 的 前 两年 呈 指数 级 成熟 , 在 12 个月 时 达到 相当于 人类 15 岁 的 水平 , 在 24 个月 时 达到 24 岁 。 随后 , 速度 会 变慢 , 并 极大 地 取决于 <strong>动物 的 体型</strong> : 体重 超过 45 公斤 的 巨型 犬 的 衰老 速度 比 小型 犬 快 多 达 60% 。',
package/src/data.ts CHANGED
@@ -1,6 +1,6 @@
1
+ export { petsCategory } from './category';
1
2
  export { petAge } from './tool/petAge';
2
3
  export { petRation } from './tool/petRation';
3
- export { petsCategory } from './category';
4
4
 
5
5
  export type { PetAgeUI, PetAgeLocaleContent } from './tool/petAge';
6
6
  export type { PetRationUI, PetRationLocaleContent } from './tool/petRation';
@@ -10,7 +10,12 @@ interface Props {
10
10
  hasSidebar?: boolean;
11
11
  }
12
12
 
13
- const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Astro.props;
13
+ const {
14
+ title,
15
+ currentLocale = "es",
16
+ localeUrls = {},
17
+ hasSidebar = false,
18
+ } = Astro.props;
14
19
  ---
15
20
 
16
21
  <!doctype html>
@@ -78,6 +83,7 @@ const { title, currentLocale = "es", localeUrls = {}, hasSidebar = false } = Ast
78
83
  transition:
79
84
  background-color 0.3s ease,
80
85
  color 0.3s ease;
86
+ font-family: Inter, sans-serif;
81
87
  }
82
88
 
83
89
  main {
@@ -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,70 @@
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
+ expect(content).not.toContain('--');
68
+ });
69
+ });
70
+ });
@@ -0,0 +1,198 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { readdirSync, readFileSync } from 'fs';
3
+ import { join, relative } from 'path';
4
+ import { ALL_TOOLS } from '../tools';
5
+ import type { SEOSection, ToolLocaleContent } from '../types';
6
+
7
+ const srcDir = join(process.cwd(), 'src');
8
+ const toolDir = join(srcDir, 'tool');
9
+ const geometryReads = [
10
+ 'offsetWidth',
11
+ 'offsetHeight',
12
+ 'offsetTop',
13
+ 'offsetLeft',
14
+ 'clientWidth',
15
+ 'clientHeight',
16
+ 'clientTop',
17
+ 'clientLeft',
18
+ 'scrollWidth',
19
+ 'scrollHeight',
20
+ 'scrollTop',
21
+ 'scrollLeft',
22
+ 'getBoundingClientRect',
23
+ 'getClientRects',
24
+ 'computedStyle',
25
+ 'getComputedStyle',
26
+ ];
27
+ const domWrites = [
28
+ '.style.',
29
+ '.classList.add',
30
+ '.classList.remove',
31
+ '.classList.toggle',
32
+ '.appendChild',
33
+ '.insertBefore',
34
+ '.prepend',
35
+ '.append',
36
+ '.remove',
37
+ '.innerHTML',
38
+ '.textContent',
39
+ '.setAttribute',
40
+ ];
41
+
42
+ function findFiles(dir: string, extensions: string[]): string[] {
43
+ const files: string[] = [];
44
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
45
+ const fullPath = join(dir, entry.name);
46
+ if (entry.isDirectory()) files.push(...findFiles(fullPath, extensions));
47
+ else if (extensions.some((extension) => entry.name.endsWith(extension))) files.push(fullPath);
48
+ }
49
+ return files;
50
+ }
51
+
52
+ function relativePath(file: string): string {
53
+ return relative(process.cwd(), file).replace(/\\/g, '/');
54
+ }
55
+
56
+ function findFormControls(content: string, tagName: 'input' | 'select'): RegExpMatchArray[] {
57
+ return Array.from(content.matchAll(new RegExp(`<${tagName}\\b[^>]*>`, 'gi')));
58
+ }
59
+
60
+ function attrValue(tag: string, attr: string): string | null {
61
+ const match = tag.match(new RegExp(`\\b${attr}\\s*=\\s*(?:"([^"]+)"|'([^']+)'|\\{([^}]+)\\})`, 'i'));
62
+ return match?.[1] ?? match?.[2] ?? match?.[3] ?? null;
63
+ }
64
+
65
+ function booleanAttr(tag: string, attr: string): boolean {
66
+ return new RegExp(`\\b${attr}\\b`, 'i').test(tag);
67
+ }
68
+
69
+ function controlStartIndex(content: string, tag: RegExpMatchArray): number {
70
+ return tag.index ?? content.indexOf(tag[0]);
71
+ }
72
+
73
+ function hasWrappingLabel(content: string, tag: RegExpMatchArray): boolean {
74
+ const index = controlStartIndex(content, tag);
75
+ const before = content.slice(0, index);
76
+ const labelOpen = before.lastIndexOf('<label');
77
+ const labelClose = before.lastIndexOf('</label>');
78
+ const nextLabelClose = content.indexOf('</label>', index + tag[0].length);
79
+ return labelOpen > labelClose && nextLabelClose !== -1;
80
+ }
81
+
82
+ function hasAccessibleName(content: string, tag: RegExpMatchArray): boolean {
83
+ const source = tag[0];
84
+ if (attrValue(source, 'aria-label')) return true;
85
+ if (attrValue(source, 'aria-labelledby')) return true;
86
+ const id = attrValue(source, 'id');
87
+ if (id && hasExplicitLabel(content, id)) return true;
88
+ return hasWrappingLabel(content, tag);
89
+ }
90
+
91
+ function isVisuallyHiddenFileInput(tag: string): boolean {
92
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
93
+ const attributes = `${attrValue(tag, 'style') ?? ''} ${attrValue(tag, 'class') ?? ''}`.toLowerCase();
94
+ const hiddenPatterns = ['display:none', 'display: none', 'file-input'];
95
+ return type === 'file' && hiddenPatterns.some((pattern) => attributes.includes(pattern));
96
+ }
97
+
98
+ function isIgnoredInput(tag: string): boolean {
99
+ const type = attrValue(tag, 'type')?.toLowerCase() ?? 'text';
100
+ return ['hidden', 'button', 'submit', 'reset'].includes(type) || booleanAttr(tag, 'aria-hidden') || isVisuallyHiddenFileInput(tag);
101
+ }
102
+
103
+ function controlFailures(content: string, tagName: 'input' | 'select'): string[] {
104
+ return findFormControls(content, tagName)
105
+ .filter((tag) => tagName !== 'input' || !isIgnoredInput(tag[0]))
106
+ .filter((tag) => !hasAccessibleName(content, tag))
107
+ .map((tag) => tag[0]);
108
+ }
109
+
110
+ function explicitLabelMessage(tagName: string, path: string, failures: string[]): string {
111
+ return `${tagName} controls without label, wrapping label, aria-label or aria-labelledby in ${path}:\n${failures.join('\n')}`;
112
+ }
113
+
114
+ function hasExplicitLabel(content: string, id: string): boolean {
115
+ const escapedId = id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
116
+ return (
117
+ new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*["']${escapedId}["'][^>]*>`, 'i').test(content)
118
+ || new RegExp(`<label\\b[^>]*\\bfor\\s*=\\s*\\{${escapedId}\\}[^>]*>`, 'i').test(content)
119
+ );
120
+ }
121
+
122
+ function headingLevels(sections: SEOSection[]): number[] {
123
+ return sections
124
+ .filter((section) => section.type === 'title')
125
+ .map((section) => Number('level' in section ? section.level : 0))
126
+ .filter((level) => Number.isInteger(level) && level > 0);
127
+ }
128
+
129
+ function findHeadingLevelJumps(levels: number[]): string[] {
130
+ const failures: string[] = [];
131
+ levels.forEach((level, index) => {
132
+ const previous = index === 0 ? 1 : levels[index - 1];
133
+ if (previous && level > previous + 1) {
134
+ failures.push(`h${previous} -> h${level}`);
135
+ }
136
+ });
137
+ return failures;
138
+ }
139
+
140
+ function hasDomWriteBeforeGeometryRead(content: string): boolean {
141
+ const normalized = content.replace(/\s+/g, ' ');
142
+ return domWrites.some((write) => {
143
+ const writeIndex = normalized.indexOf(write);
144
+ if (writeIndex === -1) return false;
145
+ return geometryReads.some((read) => normalized.indexOf(read, writeIndex + write.length) !== -1);
146
+ });
147
+ }
148
+
149
+ describe('PageSpeed best-practice guards', () => {
150
+ const astroToolFiles = findFiles(toolDir, ['.astro']);
151
+ const scriptFiles = findFiles(toolDir, ['.astro', '.ts', '.js']);
152
+
153
+ astroToolFiles.forEach((file) => {
154
+ const displayPath = relativePath(file);
155
+
156
+ it(`${displayPath} labels every input with an explicit label`, () => {
157
+ const content = readFileSync(file, 'utf-8');
158
+ const failures = controlFailures(content, 'input');
159
+
160
+ expect(failures, explicitLabelMessage('Input', displayPath, failures)).toEqual([]);
161
+ });
162
+
163
+ it(`${displayPath} labels every select with an explicit label`, () => {
164
+ const content = readFileSync(file, 'utf-8');
165
+ const failures = controlFailures(content, 'select');
166
+
167
+ expect(failures, explicitLabelMessage('Select', displayPath, failures)).toEqual([]);
168
+ });
169
+ });
170
+
171
+ ALL_TOOLS.forEach((tool) => {
172
+ Object.entries(tool.entry.i18n).forEach(([locale, loader]) => {
173
+ it(`${tool.entry.id}/${locale} keeps SEO headings sequential`, async () => {
174
+ if (!loader) return;
175
+ const content = (await loader()) as ToolLocaleContent;
176
+ const levels = headingLevels(content.seo);
177
+ const failures = findHeadingLevelJumps(levels);
178
+
179
+ expect(
180
+ failures,
181
+ `SEO headings in ${tool.entry.id}/${locale} skip levels: ${failures.join(', ')}`,
182
+ ).toEqual([]);
183
+ });
184
+ });
185
+ });
186
+
187
+ scriptFiles.forEach((file) => {
188
+ const displayPath = relativePath(file);
189
+
190
+ it(`${displayPath} avoids static forced-reflow patterns`, () => {
191
+ const content = readFileSync(file, 'utf-8');
192
+ expect(
193
+ hasDomWriteBeforeGeometryRead(content),
194
+ `${displayPath} appears to read layout geometry after DOM/style mutations. Split writes and reads across frames or measure before mutating.`,
195
+ ).toBe(false);
196
+ });
197
+ });
198
+ });
@@ -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
+ });
@@ -116,6 +116,7 @@ const { ui } = Astro.props;
116
116
  id="birth-year-input"
117
117
  placeholder="2021"
118
118
  class="age-year-input"
119
+ aria-label={ui.birthYearLabel}
119
120
  />
120
121
  </div>
121
122
  </div>
@@ -72,10 +72,10 @@ const seo: PetAgeLocaleContent['seo'] = [
72
72
  type: 'summary',
73
73
  title: 'Ce que vous devez savoir sur le vieillissement des animaux',
74
74
  items: [
75
- 'La "règle de 7" est un mythe : les chiens mûrissent les 2 premières années.',
75
+ 'La "règle de 7" est un mythe: les chiens mûrissent les 2 premières années.',
76
76
  'Un chien de 1 an équivaut à 15 ans humains ; un chien de 2 ans équivaut à 24 ans humains.',
77
77
  'Les grands chiens vieillissent plus vite que les petits après 2 ans.',
78
- 'Les chats suivent une courbe plus linéaire : l\'année de chat équivaut à 4 ans humains après 2 ans.',
78
+ 'Les chats suivent une courbe plus linéaire: l\'année de chat équivaut à 4 ans humains après 2 ans.',
79
79
  'L\'âge biologique est un meilleur prédicteur des besoins de santé que l\'âge chronologique.',
80
80
  'Des dépistages réguliers peuvent aider à détecter les problèmes liés à l\'âge.',
81
81
  ],
@@ -52,7 +52,7 @@ const faq: PetAgeLocaleContent['faq'] = [
52
52
  },
53
53
  {
54
54
  question: 'Как стареют кошки по сравнению с людьми?',
55
- answer: 'Кошка заканчивает свое младенчество в один месяц, подростковый возраст в 6 месяцев и достигает взрослости (эквивалент 24 человеческих лет) в 2 года. После этого каждый год жизни кошки равен примерно 4 человеческим годам. Эта закономерность довольно последовательна у различных пород кошек, в отличие от значительных изменений, наблюдаемых у собак.',
55
+ answer: 'Кошка заканчивает свое младенчество в один месяц, подростковый возраст - в 6 месяцев и достигает взрослости (эквивалент 24 человеческих лет) в 2 года. После этого каждый год жизни кошки равен примерно 4 человеческим годам. Эта закономерность довольно последовательна у различных пород кошек, в отличие от значительных изменений, наблюдаемых у собак.',
56
56
  },
57
57
  {
58
58
  question: 'В каком возрасте питомец считается Сеньором?',
@@ -62,7 +62,7 @@ const faq: PetAgeLocaleContent['faq'] = [
62
62
 
63
63
  const howTo: PetAgeLocaleContent['howTo'] = [
64
64
  { name: 'Выберите вид питомца', text: 'Выберите, собака у вас или кошка, чтобы применить правильную биологическую формулу для их специфической скорости метаболизма.' },
65
- { name: 'Укажите размер (только для собак)', text: 'Для собак вес является ключевым фактором в определении биологического возраста. Укажите, маленькая она (до 10 кг), средняя (1025 кг), большая (2545 кг) или гигантская (более 45 кг).' },
65
+ { name: 'Укажите размер (только для собак)', text: 'Для собак вес является ключевым фактором в определении биологического возраста. Укажите, маленькая она (до 10 кг), средняя (10-25 кг), большая (25-45 кг) или гигантская (более 45 кг).' },
66
66
  { name: 'Введите год рождения', text: 'Введите год рождения вашего питомца, чтобы рассчитать его хронологический возраст. Наша система обработает эту информацию с учетом установленных кривых старения.' },
67
67
  { name: 'Проанализируйте жизненный этап', text: 'Проверьте результат, чтобы узнать, находится ли ваш питомец в стадии младенчества, юности, взрослости или старости по сравнению с человеком, и поймите его текущие потребности.' },
68
68
  ];
@@ -72,8 +72,8 @@ const seo: PetAgeLocaleContent['seo'] = [
72
72
  type: 'summary',
73
73
  title: 'Что нужно знать о старении домашних животных',
74
74
  items: [
75
- '"Правило 7" это миф: собаки созревают экспоненциально в течение первых 2 лет.',
76
- 'Собака в возрасте 1 года это как 15 человеческих лет; 2 года эквивалентны 24 годам.',
75
+ '"Правило 7" - это миф: собаки созревают экспоненциально в течение первых 2 лет.',
76
+ 'Собака в возрасте 1 года - это как 15 человеческих лет; 2 года эквивалентны 24 годам.',
77
77
  'Крупные собаки стареют быстрее маленьких после 2 лет из-за биологической нагрузки.',
78
78
  'Кошки следуют более линейной кривой: каждый кошачий год равен 4 человеческим годам после 2 лет.',
79
79
  'Биологический возраст лучше предсказывает потребности в здоровье, чем просто хронологический возраст.',
@@ -83,7 +83,7 @@ const seo: PetAgeLocaleContent['seo'] = [
83
83
  { type: 'title', text: 'Научная правда о возрасте вашего питомца', level: 2 },
84
84
  {
85
85
  type: 'paragraph',
86
- html: 'Знаете ли вы, что 2-летняя кошка уже обладает зрелостью 24-летнего человека? Узнайте, почему правило умножения на 7 это миф, и как понимание биологической реальности может улучшить уход за вашим питомцем. Старый миф о том, что "1 собачий год равен 7 человеческим", это чрезмерное упрощение, которое не учитывает огромных изменений в развитии в начале жизни. Питомцы созревают с головокружительной скоростью в течение первых двух лет жизни, достигая половой и физической зрелости очень быстро. После этого кривая старения выравнивается и значительно замедляется.',
86
+ html: 'Знаете ли вы, что 2-летняя кошка уже обладает зрелостью 24-летнего человека? Узнайте, почему правило умножения на 7 - это миф, и как понимание биологической реальности может улучшить уход за вашим питомцем. Старый миф о том, что "1 собачий год равен 7 человеческим", - это чрезмерное упрощение, которое не учитывает огромных изменений в развитии в начале жизни. Питомцы созревают с головокружительной скоростью в течение первых двух лет жизни, достигая половой и физической зрелости очень быстро. После этого кривая старения выравнивается и значительно замедляется.',
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
@@ -130,12 +130,12 @@ const seo: PetAgeLocaleContent['seo'] = [
130
130
  type: 'card',
131
131
  title: 'Сеньор (старше 7 лет)',
132
132
  icon: 'mdi:heart-pulse',
133
- html: 'На морде начинает появляться седина, и питомец склонен больше спать. Их метаболизм значительно замедляется, и они могут проявлять меньше интереса к активным играм. С этого момента ветеринарные осмотры должны проходить каждые шесть месяцев. Следите за признаками артрита, изменениями в состоянии зубов и любыми изменениями в поведении, которые могут указывать на дискомфорт. Раннее обнаружение возрастных недугов лучший способ обеспечить высокое качество их жизни.',
133
+ html: 'На морде начинает появляться седина, и питомец склонен больше спать. Их метаболизм значительно замедляется, и они могут проявлять меньше интереса к активным играм. С этого момента ветеринарные осмотры должны проходить каждые шесть месяцев. Следите за признаками артрита, изменениями в состоянии зубов и любыми изменениями в поведении, которые могут указывать на дискомфорт. Раннее обнаружение возрастных недугов - лучший способ обеспечить высокое качество их жизни.',
134
134
  },
135
135
  {
136
136
  type: 'tip',
137
137
  title: 'Ветеринарный совет по старению',
138
- html: 'Рассчитанный возраст это средняя биологическая оценка, основанная на обширных наборах данных. Такие факторы, как конкретная порода, генетика, диета и образ жизни, могут существенно повлиять на фактическую скорость старения вашего питомца. Для получения индивидуальной консультации и комплексной оценки здоровья всегда консультируйтесь со своим ветеринаром, который знает вашего спутника лучше всех.',
138
+ html: 'Рассчитанный возраст - это средняя биологическая оценка, основанная на обширных наборах данных. Такие факторы, как конкретная порода, генетика, диета и образ жизни, могут существенно повлиять на фактическую скорость старения вашего питомца. Для получения индивидуальной консультации и комплексной оценки здоровья всегда консультируйтесь со своим ветеринаром, который знает вашего спутника лучше всех.',
139
139
  },
140
140
  ];
141
141
 
@@ -4,7 +4,7 @@ import type { PetAgeUI, PetAgeLocaleContent } from '../index';
4
4
 
5
5
  const slug = 'pet-age-calculator';
6
6
  const title = '宠物年龄换算器';
7
- const description = '了解您的爱犬或爱猫换算成人类后的真实年龄。告别过时的“乘7规则”。使用我们基于最新兽医学研究的计算器,精准掌握宠物的生命阶段。';
7
+ const description = '了解您的爱犬或爱猫换算成人类后的真实年龄。告别过时的\"乘7规则\"。使用我们基于最新兽医学研究的计算器,精准掌握宠物的生命阶段。';
8
8
 
9
9
  const ui: PetAgeUI = {
10
10
  toolTitle: '年龄计算器',
@@ -43,7 +43,7 @@ const ui: PetAgeUI = {
43
43
 
44
44
  const faq: PetAgeLocaleContent['faq'] = [
45
45
  {
46
- question: '“狗的一岁等于人类的七岁”是真的吗?',
46
+ question: '\"狗的一岁等于人类的七岁\"是真的吗?',
47
47
  answer: '不 是 真的。 这 是 一个 过度 简化 的 规则, 已经被 现代 兽医学 研究所 否定。 宠物 在 生命 的 最初 两年 成长 极其 迅速, 之后 这个 过程 会 随着 体重 和 品种 的 不同 而 显著 放缓。 最新 的 研究 发现 , 这种 传统 的 乘 7 比例 完全 无法 描绘 宠物 生物学 层面 的 老化 曲线。',
48
48
  },
49
49
  {
@@ -55,7 +55,7 @@ const faq: PetAgeLocaleContent['faq'] = [
55
55
  answer: '猫 在 一个月 大时 结束 婴儿期, 6 个月 大时 进入 青春期, 并在 2 岁 时 达到 成年 (相当于 人类 的 24 岁)。 之后, 猫 的 每一个 寿年 大约 相当于 人类 的 4 岁。 与 犬 类 相比 , 猫 的 衰老 曲线 相对 稳定 且 线性。',
56
56
  },
57
57
  {
58
- question: '宠物多大岁数被认为是“老年”?',
58
+ question: '宠物多大岁数被认为是\"老年\"?',
59
59
  answer: '通常 而言, 当 宠物 达到 其 预期 寿命 的 75% 时 就 被 视为 老年。 对于 猫 来说, 通常 是 从 7 到 11 岁 开始, 具体 取决于 它们的 健康 状况。 高龄 宠物 需要 更加 频繁 的 医疗 检查 和 专门 的 营养 支持。',
60
60
  },
61
61
  ];
@@ -72,7 +72,7 @@ const seo: PetAgeLocaleContent['seo'] = [
72
72
  type: 'summary',
73
73
  title: '关于宠物衰老您需要了解的知识',
74
74
  items: [
75
- '“乘 7 规则” 是 一个 误区 : 宠物 在 最初 的 2 年 呈 指数 级 成熟。',
75
+ '\"乘 7 规则\" 是 一个 误区 : 宠物 在 最初 的 2 年 呈 指数 级 成熟。',
76
76
  '1 岁 的 狗 相当于 15 岁 的 人类 ; 2 岁 的 狗 相当于 24 岁 的 人类。',
77
77
  '2 岁 之后, 大型 犬 的 衰老 速度 快于 小型 犬。',
78
78
  '猫 遵循 更加 线性 的 曲线 : 2 岁 后 每一个 猫 寿年 等于 人类 的 4 岁。',
@@ -83,7 +83,7 @@ const seo: PetAgeLocaleContent['seo'] = [
83
83
  { type: 'title', text: '关于您宠物年龄的科学真相', level: 2 },
84
84
  {
85
85
  type: 'paragraph',
86
- html: '您 知道 吗 ? 一只 2 岁 的 猫 已经 具备了 人类 24 岁 的 成熟度。 探索 为什么 “乘 7 规则” 只是 段子。 那个 1 个 狗 年 等于 7 个 人 类 年” 的 老 传说 只是 一种 过度 的 简化。 生物学 上 的 现实 要 有趣 得 多 : 宠物 在 生命 的 最初 两年 以 惊人的 速度 成熟, 极快 地 达到 性成熟 和 体成熟。 之后, 这种 衰老 曲线 会 趋于 平缓 并 放缓。',
86
+ html: '您 知道 吗 ? 一只 2 岁 的 猫 已经 具备了 人类 24 岁 的 成熟度。 探索 为什么 \"乘 7 规则\" 只是 段子。 那个 \"1 个 狗 年 等于 7 个 人 类 年\" 的 老 传说 只是 一种 过度 的 简化。 生物学 上 的 现实 要 有趣 得 多 : 宠物 在 生命 的 最初 两年 以 惊人的 速度 成熟, 极快 地 达到 性成熟 和 体成熟。 之后, 这种 衰老 曲线 会 趋于 平缓 并 放缓。',
87
87
  },
88
88
  {
89
89
  type: 'paragraph',
@@ -92,7 +92,7 @@ const seo: PetAgeLocaleContent['seo'] = [
92
92
  { type: 'title', text: '计算背后的科学原理', level: 3 },
93
93
  {
94
94
  type: 'paragraph',
95
- html: '此 工具 采用 <strong>美国 兽医 协会 (AAHA)</strong> 的 官方 指南 以及 关于 犬 类 DNA 甲基化 的 最新 研究 模型 , 旨在 为 您 提供 尽可能 准确 的 估算 。 DNA 甲基化 分析 被 称为 “表观 遗传 钟” , 通过 检测 遗传物质 上 的 化学 变化 来 揭示 生命体 内部 的 生物 年龄 。',
95
+ html: '此 工具 采用 <strong>美国 兽医 协会 (AAHA)</strong> 的 官方 指南 以及 关于 犬 类 DNA 甲基化 的 最新 研究 模型 , 旨在 为 您 提供 尽可能 准确 的 估算 。 DNA 甲基化 分析 被 称为 \"表观 遗传 钟\" , 通过 检测 遗传物质 上 的 化学 变化 来 揭示 生命体 内部 的 生物 年龄 。',
96
96
  },
97
97
  {
98
98
  type: 'paragraph',
@@ -50,7 +50,7 @@ const { ui } = Astro.props;
50
50
 
51
51
  <div class="input-item">
52
52
  <div class="label-row-v2">
53
- <label class="item-label">{ui.labelWeight}</label>
53
+ <label class="item-label" for="weightRange">{ui.labelWeight}</label>
54
54
  <div class="val-badge"><span id="weightDisplay">10</span> kg</div>
55
55
  </div>
56
56
  <input type="range" id="weightRange" min="1" max="85" step="0.5" value="10" class="range-v2" />
@@ -86,7 +86,7 @@ const { ui } = Astro.props;
86
86
  </div>
87
87
  <div class="custom-ratio-link" id="showManualSlider">{ui.manualAdjust}</div>
88
88
  <div class="manual-slider-box" id="manualSliderBox" style="display: none;">
89
- <input type="range" id="dietRatioRange" min="0" max="100" step="5" value="0" class="range-v2 ratio-slider" />
89
+ <input type="range" id="dietRatioRange" min="0" max="100" step="5" value="0" class="range-v2 ratio-slider" aria-label={ui.wetPercentLabel} />
90
90
  <div class="ratio-labels">
91
91
  <span>{ui.wetPercentLabel}</span>
92
92
  <strong id="ratioValDisplay">0%</strong>
@@ -36,7 +36,7 @@ const ui: PetRationUI = {
36
36
  kcalUnit: 'kcal/jour',
37
37
  statusPuppy: 'Phase de croissance rapide',
38
38
  statusSenior: 'Métabolisme mature',
39
- statusLargeDog: 'Grande race : Soin articulaire',
39
+ statusLargeDog: 'Grande race: Soin articulaire',
40
40
  statusDefault: 'Poids équilibré',
41
41
  };
42
42
 
@@ -71,7 +71,7 @@ const seo: PetRationLocaleContent['seo'] = [
71
71
  type: 'summary',
72
72
  title: 'Principes Clés de la Nutrition Vétérinaire',
73
73
  items: [
74
- 'La formule RER est : 70 × (poids en kg)^0,75. C\'est la base scientifique de tout calcul calorique vétérinaire moderne.',
74
+ 'La formule RER est: 70 × (poids en kg)^0,75. C\'est la base scientifique de tout calcul calorique vétérinaire moderne.',
75
75
  'Le DER multiplie le RER par le facteur de mode de vie spécifique de l\'animal pour fournir un besoin quotidien.',
76
76
  'Un chien castré a besoin de jusqu\'à 20 pour cent de calories en moins qu\'un chien entier en raison des changements hormonaux.',
77
77
  'L\'obésité touche 50 pour cent des animaux de compagnie aujourd\'hui et réduit considérablement leur espérance et leur qualité de vida.',
@@ -82,16 +82,16 @@ const seo: PetRationLocaleContent['seo'] = [
82
82
  { type: 'title', text: 'Guide Complet pour Calculer la Ration Journalière de votre Animal pour une Santé Optimale', level: 2 },
83
83
  {
84
84
  type: 'paragraph',
85
- html: 'Nourrir correctement un chien ou un chat n\'est pas seulement une question de remplir son bol chaque matin. La nutrition est le pilier fondamental de sa santé, de sa longévité et de son bien-être émotionnel. Un <strong>calculateur de ration journalière pour animaux</strong> est un outil essentiel pour tout propriétaire responsable qui souhaite éviter les deux problèmes les plus courants de la nutrition vétérinaire actuelle : la malnutrition par manque de nutriments et, surtout, l\'obésité, qui touche plus de 50 % des animaux de compagnie dans les pays développés.',
85
+ html: 'Nourrir correctement un chien ou un chat n\'est pas seulement une question de remplir son bol chaque matin. La nutrition est le pilier fondamental de sa santé, de sa longévité et de son bien-être émotionnel. Un <strong>calculateur de ration journalière pour animaux</strong> est un outil essentiel pour tout propriétaire responsable qui souhaite éviter les deux problèmes les plus courants de la nutrition vétérinaire actuelle: la malnutrition par manque de nutriments et, surtout, l\'obésité, qui touche plus de 50 % des animaux de compagnie dans les pays développés.',
86
86
  },
87
87
  {
88
88
  type: 'paragraph',
89
89
  html: 'Dans ce guide, nous explorerons les fondements scientifiques des calculs caloriques, comment interpréter les étiquettes des aliments et comment des facteurs tels que l\'âge, le niveau d\'activité physique et le statut physiologique influencent la quantité de nourriture dont votre compagnon a besoin chaque jour. Nous vous aiderons à comprendre comment de petits ajustements dans la ration journalière peuvent faire une énorme différence dans la prévention du diabète, des problèmes articulaires et de la santé cardiaque de votre animal.',
90
90
  },
91
- { type: 'title', text: 'La Science derrière les Calculs : Comprendre le RER et le DER', level: 3 },
91
+ { type: 'title', text: 'La Science derrière les Calculs: Comprendre le RER et le DER', level: 3 },
92
92
  {
93
93
  type: 'paragraph',
94
- html: 'Pour déterminer exactement combien un animal doit manger chaque jour, les vétérinaires et les nutritionnistes utilisent deux concepts clés : le RER et le DER. Ces acronymes signifient respectivement Besoin Énergétique au Repos et Besoin Énergétique Quotidien. Comprendre comment ils interagissent est crucial pour gérer le poids et le bien-être de votre animal de manière sûre et contrôlée.',
94
+ html: 'Pour déterminer exactement combien un animal doit manger chaque jour, les vétérinaires et les nutritionnistes utilisent deux concepts clés: le RER et le DER. Ces acronymes signifient respectivement Besoin Énergétique au Repos et Besoin Énergétique Quotidien. Comprendre comment ils interagissent est crucial pour gérer le poids et le bien-être de votre animal de manière sûre et contrôlée.',
95
95
  },
96
96
  {
97
97
  type: 'paragraph',
@@ -100,7 +100,7 @@ const seo: PetRationLocaleContent['seo'] = [
100
100
  {
101
101
  type: 'stats',
102
102
  items: [
103
- { label: 'RER : Énergie au repos', value: '70 × kg^0,75' },
103
+ { label: 'RER: Énergie au repos', value: '70 × kg^0,75' },
104
104
  { label: 'Facteur chiot', value: 'de x2.5 à x3.5' },
105
105
  { label: 'Facteur adulte actif', value: 'de x1.6 à x2.0' },
106
106
  { label: 'Facteur senior', value: 'de x1.0 à x1.4' },
@@ -88,7 +88,7 @@ const seo: PetRationLocaleContent['seo'] = [
88
88
  type: 'paragraph',
89
89
  html: '본 가이드 에서는 칼로리 계산 뒤에 숨겨진 과학적 근거 , 사료 라벨 을 해석 하는 방법 , 그리고 연령 , 활동 수준 , 생리학적 상태 가 매일 필요로 하는 사료 양 에 어떻게 구체적 으로 영향 을 미치 는지 설명 합니다 . 일일 급여량 의 작은 조절 이 당뇨병 , 관절 문제 , 심장 건강 을 예방 하는 데 얼마나 큰 차이 를 만드는지 이해 하도록 도와 드립니다 .',
90
90
  },
91
- { type: 'title', text: '계산 뒤 의 과학 : RER 과 DER 의 연관성 이해 하기', level: 3 },
91
+ { type: 'title', text: '계산 뒤 의 과학: RER 과 DER 의 연관성 이해 하기', level: 3 },
92
92
  {
93
93
  type: 'paragraph',
94
94
  html: '동물 이 매일 얼마나 먹어야 하는지 정확히 결정 하기 위해 수의사 와 영양학자 들 은 두 가지 핵심 개념 인 RER 과 DER 을 사용 합니다 . 이러한 약어 는 각각 Resting Energy Requirement ( 휴식기 에너지 요구량 ) 와 Daily Energy Requirement ( 일일 에너지 요구량 ) 를 의미 합니다 . 이들 의 상호작용 을 이해 하는 것 은 반려동물 의 체중 과 건강 을 안전 하고 통제 된 방식 으로 관리 하는 데 중요 합니다 .',
@@ -43,7 +43,7 @@ const ui: PetRationUI = {
43
43
  const faq: PetRationLocaleContent['faq'] = [
44
44
  {
45
45
  question: 'Что такое RER и почему это важно?',
46
- answer: 'RER (Resting Energy Requirement) это минимальное количество энергии, необходимое вашему питомцу для поддержания основных жизненных функций в состоянии покоя. Это научная основа для расчета дневного рациона, помогающая избежать недоедания или переедания. Точный расчет RER позволяет владельцам корректировать питание в зависимости от метаболических потребностей конкретного животного.',
46
+ answer: 'RER (Resting Energy Requirement) - это минимальное количество энергии, необходимое вашему питомцу для поддержания основных жизненных функций в состоянии покоя. Это научная основа для расчета дневного рациона, помогающая избежать недоедания или переедания. Точный расчет RER позволяет владельцам корректировать питание в зависимости от метаболических потребностей конкретного животного.',
47
47
  },
48
48
  {
49
49
  question: 'Как стерилизация влияет на рацион моего питомца?',
@@ -75,14 +75,14 @@ const seo: PetRationLocaleContent['seo'] = [
75
75
  'DER получается путем умножения RER на коэффициент образа жизни и физиологического состояния животного.',
76
76
  'Кастрированной собаке или кошке требуется до 20% меньше калорий из-за снижения уровня половых гормонов.',
77
77
  'Ожирение затрагивает более 50% домашних животных в мире и значительно сокращает их ожидаемую продолжительность жизни.',
78
- 'Правильный расчет порции это форма превентивной медицины, доступная каждому владельцу.',
78
+ 'Правильный расчет порции - это форма превентивной медицины, доступная каждому владельцу.',
79
79
  'Использование качественных весов для взвешивания корма гарантирует точность соблюдения диеты.',
80
80
  ],
81
81
  },
82
82
  { type: 'title', text: 'Полное руководство по расчету дневного рациона вашего питомца для здоровья и долголетия', level: 2 },
83
83
  {
84
84
  type: 'paragraph',
85
- html: 'Правильное кормление собаки или кошки это значительно больше, чем просто наполнение их миски каждое утро. Питание является фундаментальным столпом их здоровья, физического долголетия и даже эмоционального благополучия. <strong>Калькулятор дневного рациона для домашних животных</strong> это важный инструмент для любого ответственного владельца, который хочет избежать двух самых распространенных проблем в современном ветеринарном питании: скрытого недоедания из-за нехватки нутриентов и, особенно, ожирения, которое поражает более половины домашних животных в развитых странах.',
85
+ html: 'Правильное кормление собаки или кошки - это значительно больше, чем просто наполнение их миски каждое утро. Питание является фундаментальным столпом их здоровья, физического долголетия и даже эмоционального благополучия. <strong>Калькулятор дневного рациона для домашних животных</strong> - это важный инструмент для любого ответственного владельца, который хочет избежать двух самых распространенных проблем в современном ветеринарном питании: скрытого недоедания из-за нехватки нутриентов и, особенно, ожирения, которое поражает более половины домашних животных в развитых странах.',
86
86
  },
87
87
  {
88
88
  type: 'paragraph',
@@ -123,16 +123,16 @@ const seo: PetRationLocaleContent['seo'] = [
123
123
  {
124
124
  type: 'tip',
125
125
  title: 'Важное примечание ветеринарного диетолога',
126
- html: 'Каждый питомец это уникальный организм со своим типом обмена веществ. Калькулятор предлагает надежную и научно обоснованную отправную точку, но вы всегда должны следить за реальной кондицией тела, прощупывая ребра вашего питомца. Если вы можете почувствовать их под небольшим слоем жира, не нажимая сильно, значит вес вашего друга находится в зоне нормы. Если ребра не прощупываются, пора снижать калорийность.',
126
+ html: 'Каждый питомец - это уникальный организм со своим типом обмена веществ. Калькулятор предлагает надежную и научно обоснованную отправную точку, но вы всегда должны следить за реальной кондицией тела, прощупывая ребра вашего питомца. Если вы можете почувствовать их под небольшим слоем жира, не нажимая сильно, значит вес вашего друга находится в зоне нормы. Если ребра не прощупываются, пора снижать калорийность.',
127
127
  },
128
128
  { type: 'title', text: 'Специфика питания кошек в сравнении с собаками', level: 3 },
129
129
  {
130
130
  type: 'paragraph',
131
- html: 'Кошки это облигатные хищники, их белковый метаболизм настроен на получение энергии в основном из животных протеинов. У собак обмен веществ более гибкий, но мы все равно должны быть крайне осторожны с избытком углеводов. И тем, и другим видам жизненно необходим строго контролируемый рацион, который позволяет избежать чрезмерной нагрузки на опорно-двигательный аппарат и сердечно-сосудистую систему, особенно в зрелом возрасте.',
131
+ html: 'Кошки - это облигатные хищники, их белковый метаболизм настроен на получение энергии в основном из животных протеинов. У собак обмен веществ более гибкий, но мы все равно должны быть крайне осторожны с избытком углеводов. И тем, и другим видам жизненно необходим строго контролируемый рацион, который позволяет избежать чрезмерной нагрузки на опорно-двигательный аппарат и сердечно-сосудистую систему, особенно в зрелом возрасте.',
132
132
  },
133
133
  {
134
134
  type: 'paragraph',
135
- html: 'Уделить несколько минут расчету точного рациона это, пожалуй, самая эффективная и дешевая форма профилактической медицины, доступная владельцам сегодня. Здоровый и стабильный вес радикально снижает риск развития сахарного диабета, хронической болезни почек и болезней суставов. Ваш ветеринар при каждом плановом визите может подтвердить правильность ваших расчетов, оценив индекс упитанности питомца.',
135
+ html: 'Уделить несколько минут расчету точного рациона - это, пожалуй, самая эффективная и дешевая форма профилактической медицины, доступная владельцам сегодня. Здоровый и стабильный вес радикально снижает риск развития сахарного диабета, хронической болезни почек и болезней суставов. Ваш ветеринар при каждом плановом визите может подтвердить правильность ваших расчетов, оценив индекс упитанности питомца.',
136
136
  },
137
137
  ];
138
138
 
@@ -82,16 +82,16 @@ const seo: PetRationLocaleContent['seo'] = [
82
82
  { type: 'title', text: 'Evcil Hayvanınızın Sağlığı İçin Günlük Mama Hesaplama Rehberi', level: 2 },
83
83
  {
84
84
  type: 'paragraph',
85
- html: 'Bir köpeği veya kediyi doğru şekilde beslemek , sadece her sabah kasesini doldurmaktan çok daha fazlasıdır . Beslenme , evcil hayvanınızın sağlığının , uzun ömürlülüğünün ve duygusal refahının temel taşıdır . <strong>Evcil hayvan günlük mama hesaplayıcısı</strong> , modern veteriner hekimliğinde en sık karşılaşılan iki sorunu önlemek isteyen her sorumlu sahip için vazgeçilmez bir araçtır : besin eksikliği ve gelişmiş ülkelerdeki hayvanların yarısından fazlasını etkileyen obezite .',
85
+ html: 'Bir köpeği veya kediyi doğru şekilde beslemek , sadece her sabah kasesini doldurmaktan çok daha fazlasıdır . Beslenme , evcil hayvanınızın sağlığının , uzun ömürlülüğünün ve duygusal refahının temel taşıdır . <strong>Evcil hayvan günlük mama hesaplayıcısı</strong> , modern veteriner hekimliğinde en sık karşılaşılan iki sorunu önlemek isteyen her sorumlu sahip için vazgeçilmez bir araçtır: besin eksikliği ve gelişmiş ülkelerdeki hayvanların yarısından fazlasını etkileyen obezite .',
86
86
  },
87
87
  {
88
88
  type: 'paragraph',
89
89
  html: 'Bu kılavuzda , kalori hesaplamalarının arkasındaki bilimsel temelleri , mama etiketlerini nasıl yorumlayacağınızı ve yaş , aktivite seviyesi gibi faktörlerin günlük mama miktarını nasıl etkilediğini inceleyeceğiz . Günlük porsiyondaki küçük ayarlamaların diyabet , eklem sorunları ve kalp sağlığı üzerindeki büyük etkilerini anlamanıza yardımcı olacağız . Evcil hayvanınızın ideal kilosunu koruması , yaşam kalitesini doğrudan artıran en önemli faktördür .',
90
90
  },
91
- { type: 'title', text: 'Hesaplamaların Arkasındaki Bilim : RER ve DER İlişkisi', level: 3 },
91
+ { type: 'title', text: 'Hesaplamaların Arkasındaki Bilim: RER ve DER İlişkisi', level: 3 },
92
92
  {
93
93
  type: 'paragraph',
94
- html: 'Bir hayvanın günde ne kadar yemesi gerektiğini hassas bir şekilde belirlemek için veteriner hekimler ve beslenme uzmanları iki ana kavram kullanır : RER ve DER . Bu kısaltmalar sırasıyla Dinlenme Enerji Gereksinimi ve Günlük Enerji Gereksinimi anlamına gelir . Bunların birbiriyle nasıl etkileşime girdiğini anlamak , kilonun güvenli ve kontrollü bir şekilde yönetilmesi için kritik bir adımdır .',
94
+ html: 'Bir hayvanın günde ne kadar yemesi gerektiğini hassas bir şekilde belirlemek için veteriner hekimler ve beslenme uzmanları iki ana kavram kullanır: RER ve DER . Bu kısaltmalar sırasıyla Dinlenme Enerji Gereksinimi ve Günlük Enerji Gereksinimi anlamına gelir . Bunların birbiriyle nasıl etkileşime girdiğini anlamak , kilonun güvenli ve kontrollü bir şekilde yönetilmesi için kritik bir adımdır .',
95
95
  },
96
96
  {
97
97
  type: 'paragraph',
@@ -47,7 +47,7 @@ const faq: PetRationLocaleContent['faq'] = [
47
47
  },
48
48
  {
49
49
  question: '绝育对宠物的喂食量有什么影响?',
50
- answer: '绝育 后 的 动物 基础 代谢 通常 会 降低 约 15% 至 20% 。 为了 避免 体重 快速 增加 , 您 可能 需要 适当 减少 喂食 量 , 或者 在 计算器 中 将 活动 水平 调整 为 以 匹配 它们 的 总 消耗 。 绝育 后 激素 的 变化 会 影响 脂肪 的 代谢 模式 , 因此 此时 的 饮食 监测 显得 尤为 重要 。',
50
+ answer: '绝育 后 的 动物 基础 代谢 通常 会 降低 约 15% 至 20% 。 为了 避免 体重 快速 增加 , 您 可能 需要 适当 减少 喂食 量 , 或者 在 计算器 中 将 活动 水平 调整 为 \"\" 以 匹配 它们 的 总 消耗 。 绝育 后 激素 的 变化 会 影响 脂肪 的 代谢 模式 , 因此 此时 的 饮食 监测 显得 尤为 重要 。',
51
51
  },
52
52
  {
53
53
  question: '这个计算器可以给幼犬使用吗?',
@@ -106,10 +106,10 @@ const seo: PetRationLocaleContent['seo'] = [
106
106
  { label: '高龄 老年 阶段 系数', value: 'x1.0 至 x1.4' },
107
107
  ],
108
108
  },
109
- { type: 'title', text: '如何 准确 提取 宠物 食品 中 的 代谢能 数值', level: 3 },
109
+ { type: 'title', text: '如何 准确 提取 宠物 食品 中 的 \" 代谢能 \" 数值', level: 3 },
110
110
  {
111
111
  type: 'paragraph',
112
- html: '为了 让 算法 算出 最为 准确 的 喂食 重量 , 您 需要 提供 粮食 的 <strong>热量 密度</strong> 。 这 通常 标注 在 保证 分析 典型 值 一栏 中 , 以 kcal/kg 为 单位 。 如果 包装 信息 缺失 , 请 务必 咨询 品牌 或 使用 本 手册 的 参考 均值 。 配合 厨房 秤 使用 , 您 就能 达到 精确 喂养 的 专业 级别 。',
112
+ html: '为了 让 算法 算出 最为 准确 的 喂食 重量 , 您 需要 提供 粮食 的 <strong>热量 密度</strong> 。 这 通常 标注 在 \" 保证 分析 \"\" 典型 值 \" 一栏 中 , 以 kcal/kg 为 单位 。 如果 包装 信息 缺失 , 请 务必 咨询 品牌 或 使用 本 手册 的 参考 均值 。 配合 厨房 秤 使用 , 您 就能 达到 精确 喂养 的 专业 级别 。',
113
113
  },
114
114
  {
115
115
  type: 'stats',