feat: add i18n dictionaries and service data
Locale-aware EN/FR string dictionaries and the two-service data model (CVE Watch with per-locale URLs, Convert with a single URL).
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
export const en = {
|
||||||
|
meta: {
|
||||||
|
title: 'Ombrora — Security tracking and file tools',
|
||||||
|
description:
|
||||||
|
"Ombrora is a hub for a growing set of independent tools: CVE tracking and file conversion.",
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
brand: 'Ombrora',
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: 'The Ombrora hub',
|
||||||
|
subtitle:
|
||||||
|
'Small, focused tools for security tracking and everyday file conversion — this page is the map.',
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
heading: 'Services',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
toggleToDark: 'Switch to dark mode',
|
||||||
|
toggleToLight: 'Switch to light mode',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
text: 'Ombrora',
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type Dictionary = typeof en;
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import type { Dictionary } from './en';
|
||||||
|
|
||||||
|
export const fr: Dictionary = {
|
||||||
|
meta: {
|
||||||
|
title: 'Ombrora — Veille sécurité et outils de fichiers',
|
||||||
|
description:
|
||||||
|
'Ombrora est un hub regroupant des outils indépendants : veille CVE et conversion de fichiers.',
|
||||||
|
},
|
||||||
|
nav: {
|
||||||
|
brand: 'Ombrora',
|
||||||
|
},
|
||||||
|
hero: {
|
||||||
|
title: 'Le hub des outils Ombrora',
|
||||||
|
subtitle:
|
||||||
|
'Des outils simples et ciblés pour la veille sécurité et la conversion de fichiers au quotidien — cette page en est la carte.',
|
||||||
|
},
|
||||||
|
services: {
|
||||||
|
heading: 'Services',
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
toggleToDark: 'Passer en mode sombre',
|
||||||
|
toggleToLight: 'Passer en mode clair',
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
text: 'Ombrora',
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { en } from './en';
|
||||||
|
import { fr } from './fr';
|
||||||
|
import type { Dictionary } from './en';
|
||||||
|
import type { Locale } from './locale';
|
||||||
|
|
||||||
|
const dictionaries: Record<Locale, Dictionary> = { en, fr };
|
||||||
|
|
||||||
|
export function getDictionary(locale: Locale): Dictionary {
|
||||||
|
return dictionaries[locale];
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
export type Locale = 'en' | 'fr';
|
||||||
|
|
||||||
|
export const locales: Locale[] = ['en', 'fr'];
|
||||||
|
|
||||||
|
export function otherLocale(locale: Locale): Locale {
|
||||||
|
return locale === 'en' ? 'fr' : 'en';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function swapLocalePath(pathname: string, from: Locale, to: Locale): string {
|
||||||
|
return pathname.replace(`/${from}/`, `/${to}/`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import type { Locale } from './locale';
|
||||||
|
|
||||||
|
export interface Service {
|
||||||
|
id: string;
|
||||||
|
url: string | Record<Locale, string>;
|
||||||
|
name: Record<Locale, string>;
|
||||||
|
tag: Record<Locale, string>;
|
||||||
|
description: Record<Locale, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const services: Service[] = [
|
||||||
|
{
|
||||||
|
id: 'cve',
|
||||||
|
url: {
|
||||||
|
en: 'https://cve-en.ombrora.com',
|
||||||
|
fr: 'https://cve-fr.ombrora.com',
|
||||||
|
},
|
||||||
|
name: { en: 'CVE Watch', fr: 'Veille CVE' },
|
||||||
|
tag: { en: 'Security blog', fr: 'Blog sécurité' },
|
||||||
|
description: {
|
||||||
|
en: 'Daily-tracked CVE disclosures, explained.',
|
||||||
|
fr: 'Les CVE publiées, suivies et expliquées au quotidien.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'convert',
|
||||||
|
url: 'https://convert.ombrora.com',
|
||||||
|
name: { en: 'Convert', fr: 'Convert' },
|
||||||
|
tag: { en: 'File converter', fr: 'Convertisseur de fichiers' },
|
||||||
|
description: {
|
||||||
|
en: 'Convert documents, audio, images, video, and archives.',
|
||||||
|
fr: 'Convertissez documents, audio, images, vidéos et archives.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function resolveServiceUrl(service: Service, locale: Locale): string {
|
||||||
|
return typeof service.url === 'string' ? service.url : service.url[locale];
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { en } from '../src/i18n/en';
|
||||||
|
import { fr } from '../src/i18n/fr';
|
||||||
|
import { getDictionary } from '../src/i18n';
|
||||||
|
import { otherLocale, swapLocalePath, locales } from '../src/i18n/locale';
|
||||||
|
import { services, resolveServiceUrl } from '../src/i18n/services';
|
||||||
|
|
||||||
|
describe('i18n dictionaries', () => {
|
||||||
|
it('exposes both locales', () => {
|
||||||
|
expect(locales).toEqual(['en', 'fr']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fr and en dictionaries have the same keys', () => {
|
||||||
|
expect(Object.keys(fr)).toEqual(Object.keys(en));
|
||||||
|
expect(Object.keys(fr.hero)).toEqual(Object.keys(en.hero));
|
||||||
|
expect(Object.keys(fr.theme)).toEqual(Object.keys(en.theme));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getDictionary resolves the right dictionary', () => {
|
||||||
|
expect(getDictionary('en')).toBe(en);
|
||||||
|
expect(getDictionary('fr')).toBe(fr);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('otherLocale flips locale', () => {
|
||||||
|
expect(otherLocale('en')).toBe('fr');
|
||||||
|
expect(otherLocale('fr')).toBe('en');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('swapLocalePath swaps the locale segment', () => {
|
||||||
|
expect(swapLocalePath('/en/', 'en', 'fr')).toBe('/fr/');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('services data', () => {
|
||||||
|
it('resolves a locale-specific url', () => {
|
||||||
|
const cve = services.find((s) => s.id === 'cve')!;
|
||||||
|
expect(resolveServiceUrl(cve, 'en')).toBe('https://cve-en.ombrora.com');
|
||||||
|
expect(resolveServiceUrl(cve, 'fr')).toBe('https://cve-fr.ombrora.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolves a single-url service the same for both locales', () => {
|
||||||
|
const convert = services.find((s) => s.id === 'convert')!;
|
||||||
|
expect(resolveServiceUrl(convert, 'en')).toBe('https://convert.ombrora.com');
|
||||||
|
expect(resolveServiceUrl(convert, 'fr')).toBe('https://convert.ombrora.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('has exactly two services: cve and convert', () => {
|
||||||
|
expect(services.map((s) => s.id).sort()).toEqual(['convert', 'cve']);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user