@jjlmoya/utils-tools 1.12.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 +1 -1
- package/src/category/i18n/de.ts +3 -3
- package/src/category/i18n/en.ts +1 -1
- package/src/category/i18n/es.ts +1 -1
- package/src/category/i18n/fr.ts +16 -16
- package/src/category/i18n/id.ts +1 -1
- package/src/category/i18n/nl.ts +1 -1
- package/src/category/i18n/pl.ts +1 -1
- package/src/category/i18n/pt.ts +1 -1
- package/src/category/i18n/ru.ts +10 -10
- package/src/category/i18n/sv.ts +1 -1
- package/src/category/i18n/tr.ts +1 -1
- package/src/category/i18n/zh.ts +4 -4
- package/src/layouts/PreviewLayout.astro +7 -2
- package/src/pages/[locale]/[slug].astro +30 -13
- package/src/tests/diacritics_density.test.ts +118 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/locale_completeness.test.ts +3 -20
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tool/date-diff-calculator/bibliography.astro +2 -10
- package/src/tool/date-diff-calculator/bibliography.ts +7 -0
- package/src/tool/date-diff-calculator/i18n/de.ts +8 -11
- package/src/tool/date-diff-calculator/i18n/en.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/es.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/fr.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/id.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/it.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/ja.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/ko.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/nl.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/pl.ts +5 -8
- package/src/tool/date-diff-calculator/i18n/pt.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/ru.ts +8 -11
- package/src/tool/date-diff-calculator/i18n/sv.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/tr.ts +4 -7
- package/src/tool/date-diff-calculator/i18n/zh.ts +7 -10
- package/src/tool/date-diff-calculator/seo.astro +2 -1
- package/src/tool/drive-direct-link/bibliography.astro +2 -10
- package/src/tool/drive-direct-link/bibliography.ts +6 -0
- package/src/tool/drive-direct-link/i18n/de.ts +8 -10
- package/src/tool/drive-direct-link/i18n/en.ts +4 -6
- package/src/tool/drive-direct-link/i18n/es.ts +5 -7
- package/src/tool/drive-direct-link/i18n/fr.ts +5 -7
- package/src/tool/drive-direct-link/i18n/id.ts +4 -6
- package/src/tool/drive-direct-link/i18n/it.ts +4 -6
- package/src/tool/drive-direct-link/i18n/ja.ts +4 -6
- package/src/tool/drive-direct-link/i18n/ko.ts +4 -6
- package/src/tool/drive-direct-link/i18n/nl.ts +4 -6
- package/src/tool/drive-direct-link/i18n/pl.ts +8 -10
- package/src/tool/drive-direct-link/i18n/pt.ts +4 -6
- package/src/tool/drive-direct-link/i18n/ru.ts +12 -14
- package/src/tool/drive-direct-link/i18n/sv.ts +4 -6
- package/src/tool/drive-direct-link/i18n/tr.ts +4 -6
- package/src/tool/drive-direct-link/i18n/zh.ts +8 -10
- package/src/tool/drive-direct-link/seo.astro +2 -1
- package/src/tool/email-list-cleaner/bibliography.astro +2 -10
- package/src/tool/email-list-cleaner/bibliography.ts +7 -0
- package/src/tool/email-list-cleaner/i18n/de.ts +6 -9
- package/src/tool/email-list-cleaner/i18n/en.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/es.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/fr.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/id.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/it.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/ja.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/ko.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/nl.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/pl.ts +6 -9
- package/src/tool/email-list-cleaner/i18n/pt.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/ru.ts +11 -14
- package/src/tool/email-list-cleaner/i18n/sv.ts +5 -8
- package/src/tool/email-list-cleaner/i18n/tr.ts +4 -7
- package/src/tool/email-list-cleaner/i18n/zh.ts +7 -10
- package/src/tool/email-list-cleaner/seo.astro +2 -1
- package/src/tool/env-badge-spain/bibliography.astro +2 -10
- package/src/tool/env-badge-spain/bibliography.ts +6 -0
- package/src/tool/env-badge-spain/i18n/de.ts +4 -6
- package/src/tool/env-badge-spain/i18n/en.ts +4 -6
- package/src/tool/env-badge-spain/i18n/es.ts +4 -6
- package/src/tool/env-badge-spain/i18n/fr.ts +4 -6
- package/src/tool/env-badge-spain/i18n/id.ts +4 -6
- package/src/tool/env-badge-spain/i18n/it.ts +4 -6
- package/src/tool/env-badge-spain/i18n/ja.ts +4 -6
- package/src/tool/env-badge-spain/i18n/ko.ts +4 -6
- package/src/tool/env-badge-spain/i18n/nl.ts +4 -6
- package/src/tool/env-badge-spain/i18n/pl.ts +4 -6
- package/src/tool/env-badge-spain/i18n/pt.ts +4 -6
- package/src/tool/env-badge-spain/i18n/ru.ts +14 -16
- package/src/tool/env-badge-spain/i18n/sv.ts +4 -6
- package/src/tool/env-badge-spain/i18n/tr.ts +4 -6
- package/src/tool/env-badge-spain/i18n/zh.ts +4 -6
- package/src/tool/env-badge-spain/seo.astro +2 -1
- package/src/tool/morse-beacon/bibliography.astro +2 -10
- package/src/tool/morse-beacon/bibliography.ts +7 -0
- package/src/tool/morse-beacon/i18n/de.ts +8 -11
- package/src/tool/morse-beacon/i18n/en.ts +6 -9
- package/src/tool/morse-beacon/i18n/es.ts +6 -9
- package/src/tool/morse-beacon/i18n/fr.ts +9 -12
- package/src/tool/morse-beacon/i18n/id.ts +6 -9
- package/src/tool/morse-beacon/i18n/it.ts +6 -9
- package/src/tool/morse-beacon/i18n/ja.ts +6 -9
- package/src/tool/morse-beacon/i18n/ko.ts +6 -9
- package/src/tool/morse-beacon/i18n/nl.ts +6 -9
- package/src/tool/morse-beacon/i18n/pl.ts +7 -10
- package/src/tool/morse-beacon/i18n/pt.ts +6 -9
- package/src/tool/morse-beacon/i18n/ru.ts +7 -10
- package/src/tool/morse-beacon/i18n/sv.ts +6 -9
- package/src/tool/morse-beacon/i18n/tr.ts +6 -9
- package/src/tool/morse-beacon/i18n/zh.ts +7 -10
- package/src/tool/morse-beacon/seo.astro +2 -1
- package/src/tool/password-generator/bibliography.astro +2 -10
- package/src/tool/password-generator/bibliography.ts +8 -0
- package/src/tool/password-generator/i18n/de.ts +9 -13
- package/src/tool/password-generator/i18n/en.ts +4 -8
- package/src/tool/password-generator/i18n/es.ts +4 -8
- package/src/tool/password-generator/i18n/fr.ts +5 -9
- package/src/tool/password-generator/i18n/id.ts +4 -8
- package/src/tool/password-generator/i18n/it.ts +4 -8
- package/src/tool/password-generator/i18n/ja.ts +4 -8
- package/src/tool/password-generator/i18n/ko.ts +4 -8
- package/src/tool/password-generator/i18n/nl.ts +4 -8
- package/src/tool/password-generator/i18n/pl.ts +7 -11
- package/src/tool/password-generator/i18n/pt.ts +4 -8
- package/src/tool/password-generator/i18n/ru.ts +8 -12
- package/src/tool/password-generator/i18n/sv.ts +4 -8
- package/src/tool/password-generator/i18n/tr.ts +4 -8
- package/src/tool/password-generator/i18n/zh.ts +7 -11
- package/src/tool/password-generator/seo.astro +2 -1
- package/src/tool/routes/bibliography.astro +2 -10
- package/src/tool/routes/bibliography.ts +9 -0
- package/src/tool/routes/i18n/de.ts +5 -10
- package/src/tool/routes/i18n/en.ts +4 -9
- package/src/tool/routes/i18n/es.ts +4 -9
- package/src/tool/routes/i18n/fr.ts +7 -12
- package/src/tool/routes/i18n/id.ts +4 -9
- package/src/tool/routes/i18n/it.ts +4 -9
- package/src/tool/routes/i18n/ja.ts +4 -9
- package/src/tool/routes/i18n/ko.ts +4 -9
- package/src/tool/routes/i18n/nl.ts +4 -9
- package/src/tool/routes/i18n/pl.ts +4 -9
- package/src/tool/routes/i18n/pt.ts +4 -9
- package/src/tool/routes/i18n/ru.ts +7 -12
- package/src/tool/routes/i18n/sv.ts +4 -9
- package/src/tool/routes/i18n/tr.ts +4 -9
- package/src/tool/routes/i18n/zh.ts +5 -10
- package/src/tool/routes/seo.astro +2 -1
- package/src/tool/rule-of-three/bibliography.astro +2 -10
- package/src/tool/rule-of-three/bibliography.ts +6 -0
- package/src/tool/rule-of-three/i18n/de.ts +6 -8
- package/src/tool/rule-of-three/i18n/en.ts +4 -6
- package/src/tool/rule-of-three/i18n/es.ts +4 -6
- package/src/tool/rule-of-three/i18n/fr.ts +11 -13
- package/src/tool/rule-of-three/i18n/id.ts +4 -6
- package/src/tool/rule-of-three/i18n/it.ts +4 -6
- package/src/tool/rule-of-three/i18n/ja.ts +4 -6
- package/src/tool/rule-of-three/i18n/ko.ts +4 -6
- package/src/tool/rule-of-three/i18n/nl.ts +6 -8
- package/src/tool/rule-of-three/i18n/pl.ts +6 -8
- package/src/tool/rule-of-three/i18n/pt.ts +4 -6
- package/src/tool/rule-of-three/i18n/ru.ts +7 -9
- package/src/tool/rule-of-three/i18n/sv.ts +4 -6
- package/src/tool/rule-of-three/i18n/tr.ts +5 -7
- package/src/tool/rule-of-three/i18n/zh.ts +6 -8
- package/src/tool/rule-of-three/seo.astro +2 -1
- package/src/tool/seo-content-optimizer/bibliography.astro +2 -10
- package/src/tool/seo-content-optimizer/bibliography.ts +6 -0
- package/src/tool/seo-content-optimizer/i18n/de.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/en.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/es.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/fr.ts +7 -9
- package/src/tool/seo-content-optimizer/i18n/id.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/it.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/ja.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/ko.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/nl.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/pl.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/pt.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/ru.ts +5 -7
- package/src/tool/seo-content-optimizer/i18n/sv.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/tr.ts +4 -6
- package/src/tool/seo-content-optimizer/i18n/zh.ts +4 -6
- package/src/tool/seo-content-optimizer/seo.astro +2 -1
- package/src/tool/speed-reader/bibliography.astro +2 -10
- package/src/tool/speed-reader/bibliography.ts +10 -0
- package/src/tool/speed-reader/i18n/de.ts +7 -13
- package/src/tool/speed-reader/i18n/en.ts +6 -12
- package/src/tool/speed-reader/i18n/es.ts +4 -10
- package/src/tool/speed-reader/i18n/fr.ts +13 -19
- package/src/tool/speed-reader/i18n/id.ts +6 -12
- package/src/tool/speed-reader/i18n/it.ts +6 -12
- package/src/tool/speed-reader/i18n/ja.ts +4 -10
- package/src/tool/speed-reader/i18n/ko.ts +4 -10
- package/src/tool/speed-reader/i18n/nl.ts +6 -12
- package/src/tool/speed-reader/i18n/pl.ts +7 -13
- package/src/tool/speed-reader/i18n/pt.ts +6 -12
- package/src/tool/speed-reader/i18n/ru.ts +14 -20
- package/src/tool/speed-reader/i18n/sv.ts +6 -12
- package/src/tool/speed-reader/i18n/tr.ts +4 -10
- package/src/tool/speed-reader/i18n/zh.ts +8 -14
- package/src/tool/speed-reader/seo.astro +2 -1
- package/src/tool/text-pixel-calculator/bibliography.astro +2 -10
- package/src/tool/text-pixel-calculator/bibliography.ts +6 -0
- package/src/tool/text-pixel-calculator/i18n/de.ts +6 -8
- package/src/tool/text-pixel-calculator/i18n/en.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/es.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/fr.ts +5 -7
- package/src/tool/text-pixel-calculator/i18n/id.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/it.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/ja.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/ko.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/nl.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/pl.ts +7 -9
- package/src/tool/text-pixel-calculator/i18n/pt.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/ru.ts +8 -10
- package/src/tool/text-pixel-calculator/i18n/sv.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/tr.ts +4 -6
- package/src/tool/text-pixel-calculator/i18n/zh.ts +7 -9
- package/src/tool/text-pixel-calculator/seo.astro +2 -1
- package/src/tool/whatsapp-link/bibliography.astro +2 -10
- package/src/tool/whatsapp-link/bibliography.ts +6 -0
- package/src/tool/whatsapp-link/i18n/de.ts +8 -10
- package/src/tool/whatsapp-link/i18n/en.ts +4 -6
- package/src/tool/whatsapp-link/i18n/es.ts +6 -8
- package/src/tool/whatsapp-link/i18n/fr.ts +6 -8
- package/src/tool/whatsapp-link/i18n/id.ts +4 -6
- package/src/tool/whatsapp-link/i18n/it.ts +4 -6
- package/src/tool/whatsapp-link/i18n/ja.ts +4 -6
- package/src/tool/whatsapp-link/i18n/ko.ts +4 -6
- package/src/tool/whatsapp-link/i18n/nl.ts +4 -6
- package/src/tool/whatsapp-link/i18n/pl.ts +8 -10
- package/src/tool/whatsapp-link/i18n/pt.ts +4 -6
- package/src/tool/whatsapp-link/i18n/ru.ts +9 -11
- package/src/tool/whatsapp-link/i18n/sv.ts +4 -6
- package/src/tool/whatsapp-link/i18n/tr.ts +4 -6
- package/src/tool/whatsapp-link/i18n/zh.ts +8 -10
- package/src/tool/whatsapp-link/seo.astro +2 -1
- package/src/tool/whatsapp-link/whatsapp-link-generator.css +41 -4
- package/src/types.ts +0 -2
|
@@ -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
|
+
});
|
|
@@ -7,28 +7,11 @@ describe('Locale Completeness Validation', () => {
|
|
|
7
7
|
describe(`Tool: ${tool.entry.id}`, () => {
|
|
8
8
|
Object.keys(tool.entry.i18n).forEach((locale) => {
|
|
9
9
|
describe(`Locale: ${locale}`, () => {
|
|
10
|
-
it('
|
|
10
|
+
it('locale should load correctly', async () => {
|
|
11
11
|
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
12
12
|
const content = (await loader?.()) as ToolLocaleContent;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
expect(
|
|
16
|
-
content.faqTitle,
|
|
17
|
-
`Tool "${tool.entry.id}" locale "${locale}" has ${content.faq.length} FAQ items but is missing faqTitle`,
|
|
18
|
-
).toBeTruthy();
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('bibliographyTitle should be defined when bibliography items exist', async () => {
|
|
23
|
-
const loader = tool.entry.i18n[locale as keyof typeof tool.entry.i18n];
|
|
24
|
-
const content = (await loader?.()) as ToolLocaleContent;
|
|
25
|
-
|
|
26
|
-
if (content.bibliography.length > 0) {
|
|
27
|
-
expect(
|
|
28
|
-
content.bibliographyTitle,
|
|
29
|
-
`Tool "${tool.entry.id}" locale "${locale}" has ${content.bibliography.length} bibliography items but is missing bibliographyTitle`,
|
|
30
|
-
).toBeTruthy();
|
|
31
|
-
}
|
|
13
|
+
expect(content).toBeDefined();
|
|
14
|
+
expect(content.slug).toBeTruthy();
|
|
32
15
|
});
|
|
33
16
|
});
|
|
34
17
|
});
|
|
@@ -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,94 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
|
|
4
|
+
type ScriptLocale = keyof typeof SCRIPT_RULES;
|
|
5
|
+
|
|
6
|
+
const SCRIPT_RULES = {
|
|
7
|
+
ja: {
|
|
8
|
+
language: 'Japanese',
|
|
9
|
+
scriptName: 'kana/kanji',
|
|
10
|
+
scriptCharacters: /[\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Han}]/gu,
|
|
11
|
+
minScriptRatio: 0.45,
|
|
12
|
+
},
|
|
13
|
+
ko: {
|
|
14
|
+
language: 'Korean',
|
|
15
|
+
scriptName: 'hangul',
|
|
16
|
+
scriptCharacters: /\p{Script=Hangul}/gu,
|
|
17
|
+
minScriptRatio: 0.55,
|
|
18
|
+
},
|
|
19
|
+
ru: {
|
|
20
|
+
language: 'Russian',
|
|
21
|
+
scriptName: 'cyrillic',
|
|
22
|
+
scriptCharacters: /\p{Script=Cyrillic}/gu,
|
|
23
|
+
minScriptRatio: 0.65,
|
|
24
|
+
},
|
|
25
|
+
zh: {
|
|
26
|
+
language: 'Chinese',
|
|
27
|
+
scriptName: 'han',
|
|
28
|
+
scriptCharacters: /\p{Script=Han}/gu,
|
|
29
|
+
minScriptRatio: 0.45,
|
|
30
|
+
},
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
const LETTERS = /\p{L}/gu;
|
|
34
|
+
const TRANSLATABLE_KEYS = ['title', 'description', 'ui', 'seo', 'faq', 'howTo'] as const;
|
|
35
|
+
|
|
36
|
+
function collectStrings(value: unknown): string[] {
|
|
37
|
+
if (typeof value === 'string') return [value];
|
|
38
|
+
if (!value || typeof value !== 'object') return [];
|
|
39
|
+
if (Array.isArray(value)) return value.flatMap(collectStrings);
|
|
40
|
+
return Object.values(value).flatMap(collectStrings);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function normalizeText(value: unknown): string {
|
|
44
|
+
return collectStrings(value).join(' ').normalize('NFC');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function translatableContent(content: Record<string, unknown>) {
|
|
48
|
+
return TRANSLATABLE_KEYS.map((key) => content[key]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function letterCount(text: string): number {
|
|
52
|
+
return text.match(LETTERS)?.length ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function scriptCount(text: string, locale: ScriptLocale): number {
|
|
56
|
+
return text.match(SCRIPT_RULES[locale].scriptCharacters)?.length ?? 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function scriptRatio(text: string, locale: ScriptLocale): number {
|
|
60
|
+
const letters = letterCount(text);
|
|
61
|
+
if (letters === 0) return 0;
|
|
62
|
+
return scriptCount(text, locale) / letters;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('Native script density validation', () => {
|
|
66
|
+
ALL_TOOLS.forEach((tool) => {
|
|
67
|
+
describe(`Tool: ${tool.entry.id}`, () => {
|
|
68
|
+
Object.keys(SCRIPT_RULES).forEach((locale) => {
|
|
69
|
+
it(`${locale} keeps most translated text in its native script`, async () => {
|
|
70
|
+
const typedLocale = locale as ScriptLocale;
|
|
71
|
+
const loader = tool.entry.i18n[typedLocale];
|
|
72
|
+
if (!loader) return;
|
|
73
|
+
|
|
74
|
+
const content = await loader();
|
|
75
|
+
const rule = SCRIPT_RULES[typedLocale];
|
|
76
|
+
const text = normalizeText(translatableContent(content as Record<string, unknown>));
|
|
77
|
+
const letters = letterCount(text);
|
|
78
|
+
const matches = scriptCount(text, typedLocale);
|
|
79
|
+
const ratio = scriptRatio(text, typedLocale);
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
ratio,
|
|
83
|
+
[
|
|
84
|
+
`Possible broken translation detected in ${tool.entry.id}/${typedLocale} (${rule.language}).`,
|
|
85
|
+
`The text has ${matches} ${rule.scriptName} characters out of ${letters} analyzed letters (${(ratio * 100).toFixed(1)}%).`,
|
|
86
|
+
`Most translatable content should be written in ${rule.scriptName} script.`,
|
|
87
|
+
'Non-translatable fields such as slug, bibliography, and schemas are ignored to avoid false positives.',
|
|
88
|
+
].join(' '),
|
|
89
|
+
).toBeGreaterThanOrEqual(rule.minScriptRatio);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ToolExportValidationResult {
|
|
4
|
+
passed: boolean;
|
|
5
|
+
failures: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function validateComponentType(
|
|
9
|
+
toolId: string,
|
|
10
|
+
componentName: string,
|
|
11
|
+
component: unknown,
|
|
12
|
+
failures: string[],
|
|
13
|
+
): void {
|
|
14
|
+
if (typeof component !== 'function') {
|
|
15
|
+
failures.push(`${toolId}: ${componentName} is not a function (${typeof component})`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function validateComponentExecution(
|
|
20
|
+
toolId: string,
|
|
21
|
+
componentName: string,
|
|
22
|
+
fn: () => Promise<unknown>,
|
|
23
|
+
failures: string[],
|
|
24
|
+
): Promise<void> {
|
|
25
|
+
try {
|
|
26
|
+
const result = await fn();
|
|
27
|
+
if (!result || typeof result !== 'object') {
|
|
28
|
+
failures.push(`${toolId}: ${componentName} import returned invalid result`);
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
failures.push(`${toolId}: ${componentName} execution error - ${error instanceof Error ? error.message : 'unknown'}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export async function validateToolExports(tools: ToolDefinition[]): Promise<ToolExportValidationResult> {
|
|
36
|
+
const failures: string[] = [];
|
|
37
|
+
|
|
38
|
+
for (const tool of tools) {
|
|
39
|
+
validateComponentType(tool.entry.id, 'Component', tool.Component, failures);
|
|
40
|
+
validateComponentType(tool.entry.id, 'SEOComponent', tool.SEOComponent, failures);
|
|
41
|
+
validateComponentType(tool.entry.id, 'BibliographyComponent', tool.BibliographyComponent, failures);
|
|
42
|
+
|
|
43
|
+
const componentFn = tool.Component as () => Promise<unknown>;
|
|
44
|
+
const seoFn = tool.SEOComponent as () => Promise<unknown>;
|
|
45
|
+
const bibFn = tool.BibliographyComponent as () => Promise<unknown>;
|
|
46
|
+
|
|
47
|
+
await validateComponentExecution(tool.entry.id, 'Component', componentFn, failures);
|
|
48
|
+
await validateComponentExecution(tool.entry.id, 'SEOComponent', seoFn, failures);
|
|
49
|
+
await validateComponentExecution(tool.entry.id, 'BibliographyComponent', bibFn, failures);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
passed: failures.length === 0,
|
|
54
|
+
failures,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { ALL_TOOLS } from '../tools';
|
|
3
|
+
import { validateToolExports } from './shared-test-helpers';
|
|
4
|
+
|
|
5
|
+
describe('Tool Exports Pattern Validation', () => {
|
|
6
|
+
describe('Component Exports Format', () => {
|
|
7
|
+
ALL_TOOLS.forEach((tool) => {
|
|
8
|
+
it(`${tool.entry.id}: Component should be a lazy-loaded function`, () => {
|
|
9
|
+
expect(typeof tool.Component).toBe('function');
|
|
10
|
+
expect(tool.Component).toBeInstanceOf(Function);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it(`${tool.entry.id}: SEOComponent should be a lazy-loaded function`, () => {
|
|
14
|
+
expect(typeof tool.SEOComponent).toBe('function');
|
|
15
|
+
expect(tool.SEOComponent).toBeInstanceOf(Function);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it(`${tool.entry.id}: BibliographyComponent should be a lazy-loaded function`, () => {
|
|
19
|
+
expect(typeof tool.BibliographyComponent).toBe('function');
|
|
20
|
+
expect(tool.BibliographyComponent).toBeInstanceOf(Function);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe('Dynamic Import Validation', () => {
|
|
26
|
+
it('all tools must have functional dynamic imports', async () => {
|
|
27
|
+
const result = await validateToolExports(ALL_TOOLS);
|
|
28
|
+
if (!result.passed) {
|
|
29
|
+
throw new Error(`Tool export validation failed:\n${result.failures.join('\n')}`);
|
|
30
|
+
}
|
|
31
|
+
expect(result.passed).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
-
import {
|
|
4
|
-
import type { KnownLocale } from '../../types';
|
|
5
|
-
|
|
6
|
-
interface Props {
|
|
7
|
-
locale?: KnownLocale;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const { locale = 'es' } = Astro.props;
|
|
11
|
-
const content = await dateDiffCalculator.i18n[locale]?.();
|
|
3
|
+
import { bibliography } from './bibliography';
|
|
12
4
|
---
|
|
13
5
|
|
|
14
|
-
|
|
6
|
+
<SharedBibliography links={bibliography} />
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
5
|
+
{ name: 'Wikipedia: History of the Gregorian Calendar', url: 'https://en.wikipedia.org/wiki/Gregorian_calendar' },
|
|
6
|
+
{ name: 'Wikipedia: Eisenhower Matrix and Time Management', url: 'https://en.wikipedia.org/wiki/Time_management#The_Eisenhower_Method' },
|
|
7
|
+
];
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -22,8 +23,8 @@ const faqData = [
|
|
|
22
23
|
];
|
|
23
24
|
|
|
24
25
|
const howToData = [
|
|
25
|
-
{ name: 'Startdatum auswählen', text: 'Geben Sie das Startdatum und die Uhrzeit ein oder klicken Sie auf
|
|
26
|
-
{ name: 'Enddatum auswählen', text: 'Definieren Sie den Zielzeitpunkt. Verwenden Sie
|
|
26
|
+
{ name: 'Startdatum auswählen', text: 'Geben Sie das Startdatum und die Uhrzeit ein oder klicken Sie auf "Heute", um sie sofort festzulegen.' },
|
|
27
|
+
{ name: 'Enddatum auswählen', text: 'Definieren Sie den Zielzeitpunkt. Verwenden Sie "Jetzt", um die bis zu diesem exakten Moment verstrichene Zeit zu messen.' },
|
|
27
28
|
{ name: 'Ergebnisse ablesen', text: 'Überprüfen Sie die Aufteilung in Tage, Stunden, Minuten und Sekunden sowie die kumulierten Summen für Wochen und Stunden.' },
|
|
28
29
|
];
|
|
29
30
|
|
|
@@ -77,21 +78,17 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Datumsdifferenz Rechner',
|
|
78
79
|
description: 'Berechnen Sie genau, wie viel Zeit zwischen zwei Daten vergangen ist oder wie lange es noch bis zu einem Ereignis dauert. Kostenloses Tool mit Ergebnissen in Tagen, Stunden und Minuten.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: Weltuhr und Zeitzonen-Konverter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: Geschichte des gregorianischen Kalenders', url: 'https://de.wikipedia.org/wiki/Gregorianischer_Kalender' },
|
|
87
|
-
{ name: 'Wikipedia: Eisenhower-Matrix und Zeitmanagement', url: 'https://de.wikipedia.org/wiki/Zeitmanagement#Die_ Eisenhower-Methode' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Datumsdifferenzrechner mit Tagen, Stunden und Minuten' },
|
|
92
89
|
{
|
|
93
90
|
type: 'paragraph',
|
|
94
|
-
html: 'Die Berechnung der <strong>Differenz zwischen zwei Daten</strong> ist eine der am häufigsten unterschätzten Aufgaben sowohl im Alltag als auch im beruflichen Umfeld. Egal, ob Sie eine Produkteinführung planen, das exakte Alter einer Person berechnen oder die verbleibende Zeit bis zu einem besonderen Ereignis messen
|
|
91
|
+
html: 'Die Berechnung der <strong>Differenz zwischen zwei Daten</strong> ist eine der am häufigsten unterschätzten Aufgaben sowohl im Alltag als auch im beruflichen Umfeld. Egal, ob Sie eine Produkteinführung planen, das exakte Alter einer Person berechnen oder die verbleibende Zeit bis zu einem besonderen Ereignis messen - ein präzises Werkzeug ist für ein effizientes Zeitmanagement unerlässlich.',
|
|
95
92
|
},
|
|
96
93
|
{ type: 'title', level: 3, text: 'Wofür wird der Datumsdifferenzrechner verwendet?' },
|
|
97
94
|
{
|
|
@@ -111,7 +108,7 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
111
108
|
{ type: 'title', level: 3, text: 'Zeitwahrnehmung und digitale Präzision' },
|
|
112
109
|
{
|
|
113
110
|
type: 'paragraph',
|
|
114
|
-
html: 'Oft neigen wir dazu, die Zeit zu runden. Wir sagen
|
|
111
|
+
html: 'Oft neigen wir dazu, die Zeit zu runden. Wir sagen "etwa ein Monat", wenn es tatsächlich noch 27 Tage und 14 Stunden sind. Durch die Verwendung eines digitalen Rechners eliminieren wir die Subjektivität und erhalten klare Daten für eine fundierte Entscheidungsfindung.',
|
|
115
112
|
},
|
|
116
113
|
{ type: 'title', level: 3, text: 'Kalendertage versus Arbeitstage' },
|
|
117
114
|
{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Date Difference Calculator',
|
|
78
79
|
description: 'Calculate exactly how much time has passed between two dates or how long until an event. Free tool with results in days, hours and minutes.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: History of the Gregorian Calendar', url: 'https://en.wikipedia.org/wiki/Gregorian_calendar' },
|
|
87
|
-
{ name: 'Wikipedia: Eisenhower Matrix and Time Management', url: 'https://en.wikipedia.org/wiki/Time_management#The_Eisenhower_Method' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Date difference calculator with days, hours and minutes' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Calculadora de Diferencia entre Fechas',
|
|
78
79
|
description: 'Calcula exactamente cuánto tiempo ha pasado entre dos fechas o cuánto falta para un evento. Resultados en días, horas y minutos de forma gratuita y segura.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: Reloj Mundial y Conversor de Husos Horarios', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: Historia del Calendario Gregoriano', url: 'https://es.wikipedia.org/wiki/Calendario_gregoriano' },
|
|
87
|
-
{ name: 'Wikipedia: Matriz de Eisenhower y Gestión del Tiempo', url: 'https://es.wikipedia.org/wiki/Matriz_de_Eisenhower' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Calculadora de diferencia entre fechas con días, horas y minutos' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Calculateur de Différence de Dates',
|
|
78
79
|
description: 'Calculez exactement combien de temps s\'est écoulé entre deux dates ou combien il reste avant un événement. Outil gratuit avec résultats en jours, heures et minutes.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date : Horloge Mondiale et Convertisseur de Fuseaux Horaires', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia : Histoire du Calendrier Grégorien', url: 'https://fr.wikipedia.org/wiki/Calendrier_gr%C3%A9gorien' },
|
|
87
|
-
{ name: 'Wikipedia : Matrice d\'Eisenhower et Gestion du Temps', url: 'https://fr.wikipedia.org/wiki/Matrice_d%27Eisenhower' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Calculateur de différence de dates en jours, heures et minutes' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Kalkulator Selisih Tanggal',
|
|
78
79
|
description: 'Hitung secara tepat berapa lama waktu yang telah berlalu antara dua tanggal atau berapa lama hingga suatu acara terjadi. Alat gratis dengan hasil dalam hari, jam, dan menit.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: Sejarah Kalender Gregorian', url: 'https://id.wikipedia.org/wiki/Kalender_Gregorius' },
|
|
87
|
-
{ name: 'Wikipedia: Manajemen Waktu — Matriks Eisenhower', url: 'https://id.wikipedia.org/wiki/Manajemen_waktu' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Kalkulator selisih tanggal dengan hari, jam, dan menit' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Calcolatore di Differenza tra Date',
|
|
78
79
|
description: 'Calcola esattamente quanto tempo è passato tra due date o quanto manca a un evento. Strumento gratuito con risultati in giorni, ore e minuti.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: History of the Gregorian Calendar', url: 'https://it.wikipedia.org/wiki/Calendario_gregoriano' },
|
|
87
|
-
{ name: 'Wikipedia: Eisenhower Matrix and Time Management', url: 'https://it.wikipedia.org/wiki/Gestione_del_tempo#Metodo_di_Eisenhower' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Calcolatore di differenza tra date con giorni, ore e minuti' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: '日付差計算機',
|
|
78
79
|
description: '2つの日付の間にどれだけの時間が経過したか、あるいはイベントまであとどれくらいかを正確に計算します。日、時、分で結果を表示する無料ツール。',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: 世界時計とタイムゾーン変換器', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: グレゴリオ暦の歴史', url: 'https://ja.wikipedia.org/wiki/グレゴリオ暦' },
|
|
87
|
-
{ name: 'Wikipedia: アイゼンハワー・マトリクスと時間管理', url: 'https://ja.wikipedia.org/wiki/時間管理' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: '日、時、分でわかる日付差計算機' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: '날짜 차이 계산기',
|
|
78
79
|
description: '두 날짜 사이에 정확히 얼마의 시간이 지났는지 또는 이벤트까지 얼마나 남았는지 계산합니다. 일, 시, 분 단위 결과를 제공하는 무료 도구.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: 그레고리력의 역사', url: 'https://ko.wikipedia.org/wiki/그레고리력' },
|
|
87
|
-
{ name: 'Wikipedia: 아이젠하워 매트릭스와 시간 관리', url: 'https://ko.wikipedia.org/wiki/시간_관리' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: '일, 시, 분 단위로 계산하는 날짜 차이 계산기' },
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
1
2
|
import type { ToolLocaleContent } from '../../../types';
|
|
2
3
|
import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
|
|
3
4
|
import type { DateDiffCalculatorUI } from '../ui';
|
|
@@ -77,15 +78,11 @@ export const content: ToolLocaleContent<DateDiffCalculatorUI> = {
|
|
|
77
78
|
title: 'Datumverschil Calculator',
|
|
78
79
|
description: 'Bereken precies hoeveel tijd er is verstreken tussen twee datums of hoelang het nog duurt tot een evenement. Gratis tool met resultaten in dagen, uren en minuten.',
|
|
79
80
|
ui,
|
|
80
|
-
|
|
81
|
+
|
|
81
82
|
faq: faqData,
|
|
82
83
|
howTo: howToData,
|
|
83
|
-
|
|
84
|
-
bibliography
|
|
85
|
-
{ name: 'Time and Date: World Clock and Time Zone Converter', url: 'https://www.timeanddate.com/worldclock/' },
|
|
86
|
-
{ name: 'Wikipedia: History of the Gregorian Calendar', url: 'https://nl.wikipedia.org/wiki/Gregoriaanse_kalender' },
|
|
87
|
-
{ name: 'Wikipedia: Eisenhower Matrix and Time Management', url: 'https://nl.wikipedia.org/wiki/Eisenhower-methode' },
|
|
88
|
-
],
|
|
84
|
+
|
|
85
|
+
bibliography,
|
|
89
86
|
schemas: [faqSchema, howToSchema, appSchema],
|
|
90
87
|
seo: [
|
|
91
88
|
{ type: 'title', level: 2, text: 'Datumverschil-calculator met dagen, uren en minuten' },
|