Add react i18next to console

Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
Jonathan
2026-07-21 16:09:41 +02:00
committed by Bryan Frimin
parent 1b7b2594eb
commit a7ff5f07bc
420 changed files with 10251 additions and 6825 deletions

View File

@@ -1,165 +0,0 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import {
createContext,
type PropsWithChildren,
useCallback,
useContext,
useEffect,
useState,
} from "react";
const defaultValue = {
lang: "en" as "en" | "fr",
translations: {} as Record<string, string>,
translate: (s: string) => s,
};
type Context = typeof defaultValue;
const TranslatorContext = createContext(defaultValue);
type Props = {
lang: "en" | "fr";
loader: (lang: string) => Promise<Record<string, string>>;
};
export function TranslatorProvider({
lang,
loader,
children,
}: PropsWithChildren<Props>) {
const [translations, setTranslations] = useState(
{} as Record<string, string>
);
const translate = useCallback<Context["translate"]>(
(s) => {
return translations[s] ? translations[s] : s;
},
[translations]
);
useEffect(() => {
loader(lang).then(setTranslations);
}, [lang]);
return (
<TranslatorContext.Provider value={{ lang, translations, translate }}>
{children}
</TranslatorContext.Provider>
);
}
const SECONDS = 1000;
const MINUTES = SECONDS * 60;
const HOURS = MINUTES * 60;
const DAYS = HOURS * 24;
const WEEKS = DAYS * 7;
const MONTHS = DAYS * 30;
const YEARS = DAYS * 365;
const relativeFormat = [
{ limit: YEARS, unit: "years" },
{ limit: MONTHS, unit: "months" },
{ limit: WEEKS, unit: "weeks" },
{ limit: DAYS, unit: "days" },
{ limit: HOURS, unit: "hours" },
{ limit: MINUTES, unit: "minutes" },
{ limit: SECONDS, unit: "seconds" },
] as const;
export function useTranslate() {
const { translate, lang } = useContext(TranslatorContext);
const dateFormat = (
date: Date | string | null | undefined,
options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
}
) => {
if (!date) {
return "";
}
if (typeof date === "string") {
return new Intl.DateTimeFormat(lang, options).format(parseDate(date));
}
return new Intl.DateTimeFormat(lang, options).format(date);
};
const relativeDateFormat = (
date: Date | string | null | undefined,
options: Intl.RelativeTimeFormatOptions = {
style: "long",
}
) => {
if (!date) {
return "";
}
const distanceInSeconds =
(date instanceof Date ? date.getTime() : parseDate(date).getTime()) -
Date.now();
const formatter = new Intl.RelativeTimeFormat(lang, options);
for (const { limit, unit } of relativeFormat) {
if (Math.abs(distanceInSeconds) > limit) {
return formatter.format(Math.round(distanceInSeconds / limit), unit);
}
}
return "";
};
return {
lang,
__: translate,
dateFormat: dateFormat,
relativeDateFormat,
dateTimeFormat: (
date: Date | string | null | undefined,
options: Intl.DateTimeFormatOptions = {
hour: "2-digit",
hour12: false,
minute: "2-digit",
day: "numeric",
month: "short",
year: "numeric",
}
) => {
return dateFormat(date, options);
},
};
}
function parseDate(date: Date | string): Date {
if (typeof date === "string") {
if (date.includes("T")) {
return new Date(date);
}
const parts = date.split("-");
return new Date(
parseInt(parts[0], 10),
parts[1] ? parseInt(parts[1], 10) - 1 : 0,
parts[2] ? parseInt(parts[2], 10) : 1
);
}
return date;
}

82
packages/i18n/date.ts Normal file
View File

@@ -0,0 +1,82 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { parseDate } from "@probo/helpers";
const relativeFormat = [
{ limit: 1000 * 60 * 60 * 24 * 365, unit: "years" },
{ limit: 1000 * 60 * 60 * 24 * 30, unit: "months" },
{ limit: 1000 * 60 * 60 * 24 * 7, unit: "weeks" },
{ limit: 1000 * 60 * 60 * 24, unit: "days" },
{ limit: 1000 * 60 * 60, unit: "hours" },
{ limit: 1000 * 60, unit: "minutes" },
{ limit: 1000, unit: "seconds" },
] as const;
export function relativeDateFormat(
language: string,
date: Date | string | null | undefined,
options: Intl.RelativeTimeFormatOptions = { style: "long" },
): string {
if (!date) return "";
const dateValue = typeof date === "string" ? parseDate(date) : date;
const distanceInMilliseconds = dateValue.getTime() - Date.now();
const formatter = new Intl.RelativeTimeFormat(language, options);
for (const { limit, unit } of relativeFormat) {
if (Math.abs(distanceInMilliseconds) >= limit) {
return formatter.format(Math.round(distanceInMilliseconds / limit), unit);
}
}
return "";
}
export function dateFormat(
language: string,
date: Date | string | null | undefined,
options: Intl.DateTimeFormatOptions = {
year: "numeric",
month: "short",
day: "numeric",
weekday: "short",
},
): string {
if (!date) return "";
const dateValue = typeof date === "string" ? parseDate(date) : date;
return new Intl.DateTimeFormat(language, options).format(dateValue);
}
export function dateTimeFormat(
language: string,
date: Date | string | null | undefined,
options: Intl.DateTimeFormatOptions = {
hour: "2-digit",
hour12: false,
minute: "2-digit",
day: "numeric",
month: "short",
year: "numeric",
},
): string {
return dateFormat(language, date, options);
}

96
packages/i18n/duration.ts Normal file
View File

@@ -0,0 +1,96 @@
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
type Translator = (key: string, options?: { count?: number }) => string;
const DURATION_UNITS = [
{ value: "seconds", seconds: 1, snap: 0 },
{ value: "minutes", seconds: 60, snap: 5 },
{ value: "hours", seconds: 3_600, snap: 5 * 60 },
{ value: "days", seconds: 86_400, snap: 2 * 3_600 },
{ value: "weeks", seconds: 604_800, snap: 12 * 3_600 },
{ value: "months", seconds: 2_592_000, snap: 2 * 24 * 3_600 },
{ value: "years", seconds: 31_536_000, snap: 21 * 24 * 3_600 },
] as const;
export function humanizeSeconds(
seconds: number | null,
t: Translator,
): string {
if (seconds === null || seconds <= 0) {
return '';
}
let remaining = seconds;
const parts: string[] = [];
for (const { value, seconds: durationInSeconds, snap } of [...DURATION_UNITS].reverse()) {
if (remaining >= durationInSeconds - snap) {
let count = Math.floor(remaining / durationInSeconds);
const leftover = remaining - count * durationInSeconds;
if (leftover >= durationInSeconds - snap) {
count++;
remaining = 0;
} else if (leftover <= snap) {
remaining = 0;
} else {
remaining = leftover;
}
parts.push(`${count} ${t(`duration.${value}`, { count })}`);
}
}
return parts.length > 0 ? parts.join(", ") : t("duration.session");
}
export function formatDuration(
duration?: string | null,
t?: Translator,
): string | null {
if (!duration || !t) return null;
const timeMatch = duration.match(/PT(\d+)([MH])/);
if (timeMatch) {
const amount = parseInt(timeMatch[1], 10) || 0;
const unit = timeMatch[2];
if (unit === "M") return t("duration.min", { count: amount });
if (unit === "H") return t("duration.hour", { count: amount });
}
const dateMatch = duration.match(/P(\d+)([DW])/);
if (dateMatch) {
const amount = parseInt(dateMatch[1], 10) || 0;
const unit = dateMatch[2];
if (unit === "W") {
return `${amount} ${amount === 1 ? t("Week") : t("Weeks")}`;
}
if (unit === "D") {
if (amount % 7 === 0 && amount > 0) {
const weeks = amount / 7;
return `${weeks} ${weeks === 1 ? t("Week") : t("Weeks")}`;
}
return `${amount} ${amount === 1 ? t("Day") : t("Days")}`;
}
}
return null;
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { describe, expect, it } from "vitest";
import { fileSize, humanizeSeconds } from "./index";
describe("fileSize", () => {
it("formats byte counts with translated units", () => {
const t = (key: string) => key.replace("size.", "");
expect(fileSize(4911, t)).toBe("4.8 KB");
expect(fileSize(20, t)).toBe("20 B");
});
});
describe("humanizeSeconds", () => {
const t = (key: string, options?: { count?: number }) => {
const unit = key.replace("duration.", "");
if (unit === "session" || unit === "persistent") return unit;
return options?.count === 1 ? unit.slice(0, -1) : unit;
};
it("formats durations with translated plural units", () => {
expect(humanizeSeconds(4570, t)).toBe("1 hour, 16 minutes, 10 seconds");
expect(humanizeSeconds(120, t)).toBe("2 minutes");
});
it("formats trackers without a max age as session or persistent", () => {
expect(humanizeSeconds(null, t)).toBe("session");
expect(humanizeSeconds(0, t, "LOCAL_STORAGE")).toBe("persistent");
});
});

23
packages/i18n/index.ts Normal file
View File

@@ -0,0 +1,23 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
export * from "./date";
export * from "./duration";
export * from "./size";

View File

@@ -2,9 +2,10 @@
"name": "@probo/i18n",
"version": "1.0.0",
"author": "",
"main": "./TranslatorProvider.tsx",
"main": "./index.ts",
"devDependencies": {
"@types/react": "^19.2.17"
"@types/react": "^19.2.17",
"vitest": "^4.1.8"
},
"peerDependencies": {
"react": "^19.0"
@@ -12,5 +13,10 @@
"description": "",
"keywords": [],
"license": "MIT",
"scripts": {}
"scripts": {
"test": "vitest run"
},
"dependencies": {
"@probo/helpers": "1.0.0"
}
}

35
packages/i18n/size.ts Normal file
View File

@@ -0,0 +1,35 @@
// Copyright (c) 2025-2026 Probo Inc <hello@probo.com>.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
type Translator = (key: string, options?: { count?: number }) => string;
export function fileSize(size: number, t: Translator): string {
if (size < 0) return "";
if (size === 0) return `0 ${t("size.B")}`;
const units = ["B", "KB", "MB", "GB", "TB"];
const unitIndex = Math.min(
Math.floor(Math.log(size) / Math.log(1024)),
units.length - 1,
);
const formattedSize = Math.round((size / 1024 ** unitIndex) * 100) / 100;
return `${formattedSize} ${t(`size.${units[unitIndex]}`)}`;
}