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

@@ -28,20 +28,20 @@ export const auditStates = [
"OUTDATED",
] as const;
export function getAuditStateLabel(__: Translator, state: (typeof auditStates)[number]) {
export function getAuditStateLabel(t: Translator, state: (typeof auditStates)[number]) {
switch (state) {
case "NOT_STARTED":
return __("Not Started");
return t("helpers.auditState.notStarted");
case "IN_PROGRESS":
return __("In Progress");
return t("helpers.auditState.inProgress");
case "COMPLETED":
return __("Completed");
return t("helpers.auditState.completed");
case "REJECTED":
return __("Rejected");
return t("helpers.auditState.rejected");
case "OUTDATED":
return __("Outdated");
return t("helpers.auditState.outdated");
default:
return __("Unknown");
return t("helpers.common.unknown");
}
}

View File

@@ -36,19 +36,19 @@ export const certifications = {
} as const;
export const certificationCategoryLabel = (
__: Translator,
t: Translator,
category: keyof typeof certifications,
) => {
switch (category) {
case "securityStandards":
return __("Security Standards");
return t("helpers.certificationCategory.securityStandards");
case "regulatoryLegal":
return __("Regulatory & Legal");
return t("helpers.certificationCategory.regulatoryLegal");
case "industrySpecific":
return __("Industry-Specific");
return t("helpers.certificationCategory.industrySpecific");
case "internationalGov":
return __("International & Government");
return t("helpers.certificationCategory.internationalGovernment");
default:
return __("Custom certifications");
return t("helpers.certificationCategory.custom");
}
};

View File

@@ -32,16 +32,16 @@ export function getCompliancePortalDocumentAccessStatusBadgeVariant(status: Comp
}
}
export function getCompliancePortalDocumentAccessStatusLabel(status: CompliancePortalDocumentAccessStatus, __: (key: string) => string) {
export function getCompliancePortalDocumentAccessStatusLabel(status: CompliancePortalDocumentAccessStatus, t: (key: string) => string) {
switch (status) {
case "REQUESTED":
return __("requested");
return t("helpers.compliancePortalDocumentAccessStatus.requested");
case "GRANTED":
return __("granted");
return t("helpers.compliancePortalDocumentAccessStatus.granted");
case "REJECTED":
return __("rejected");
return t("helpers.compliancePortalDocumentAccessStatus.rejected");
case "REVOKED":
return __("revoked");
return t("helpers.compliancePortalDocumentAccessStatus.revoked");
}
}
@@ -68,4 +68,3 @@ export type CompliancePortalDocumentAccessInfo = ICompliancePortalDocumentAccess
type: "file",
}
)

View File

@@ -54,13 +54,13 @@ export const getCompliancePortalVisibilityLabel = (visibility: CompliancePortalV
}
};
export function getCompliancePortalVisibilityOptions(__: Translator) {
export function getCompliancePortalVisibilityOptions(t: Translator) {
return compliancePortalVisibilities.map((visibility) => ({
value: visibility,
label: __({
"NONE": "None",
"PRIVATE": "Private",
"PUBLIC": "Public",
label: t({
"NONE": "helpers.compliancePortalVisibility.none",
"PRIVATE": "helpers.compliancePortalVisibility.private",
"PUBLIC": "helpers.compliancePortalVisibility.public",
}[visibility]),
variant: getCompliancePortalVisibilityVariant(visibility),
}));

View File

@@ -31,19 +31,19 @@ export const controlMaturityLevels = [
export type ControlMaturityLevel = (typeof controlMaturityLevels)[number];
export function getControlMaturityLevelLabel(__: Translator, level: string) {
export function getControlMaturityLevelLabel(t: Translator, level: string) {
switch (level) {
case "NONE":
return __("0 - None");
return t("helpers.controlMaturityLevel.none");
case "INITIAL":
return __("1 - Initial");
return t("helpers.controlMaturityLevel.initial");
case "MANAGED":
return __("2 - Managed");
return t("helpers.controlMaturityLevel.managed");
case "DEFINED":
return __("3 - Defined");
return t("helpers.controlMaturityLevel.defined");
case "QUANTITATIVELY_MANAGED":
return __("4 - Quantitatively Managed");
return t("helpers.controlMaturityLevel.quantitativelyManaged");
case "OPTIMIZING":
return __("5 - Optimizing");
return t("helpers.controlMaturityLevel.optimizing");
}
}

View File

@@ -18,8 +18,6 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
type Translator = (s: string) => string;
// ISO 3166-1 alpha-2 country codes and supported regions.
export const countries = [
"GLOBAL",
@@ -52,271 +50,17 @@ export const countries = [
export type CountryCode = typeof countries[number];
const countryNames: Record<CountryCode, (t: Translator) => string> = {
"GLOBAL": (__) => __("Global"),
"AD": (__) => __("Andorra"),
"AE": (__) => __("United Arab Emirates"),
"AF": (__) => __("Afghanistan"),
"AG": (__) => __("Antigua and Barbuda"),
"AI": (__) => __("Anguilla"),
"AL": (__) => __("Albania"),
"AM": (__) => __("Armenia"),
"AO": (__) => __("Angola"),
"AQ": (__) => __("Antarctica"),
"AR": (__) => __("Argentina"),
"AS": (__) => __("American Samoa"),
"AT": (__) => __("Austria"),
"AU": (__) => __("Australia"),
"AW": (__) => __("Aruba"),
"AX": (__) => __("Åland Islands"),
"AZ": (__) => __("Azerbaijan"),
"BA": (__) => __("Bosnia and Herzegovina"),
"BB": (__) => __("Barbados"),
"BD": (__) => __("Bangladesh"),
"BE": (__) => __("Belgium"),
"BF": (__) => __("Burkina Faso"),
"BG": (__) => __("Bulgaria"),
"BH": (__) => __("Bahrain"),
"BI": (__) => __("Burundi"),
"BJ": (__) => __("Benin"),
"BL": (__) => __("Saint Barthélemy"),
"BM": (__) => __("Bermuda"),
"BN": (__) => __("Brunei"),
"BO": (__) => __("Bolivia"),
"BQ": (__) => __("Bonaire, Sint Eustatius and Saba"),
"BR": (__) => __("Brazil"),
"BS": (__) => __("Bahamas"),
"BT": (__) => __("Bhutan"),
"BV": (__) => __("Bouvet Island"),
"BW": (__) => __("Botswana"),
"BY": (__) => __("Belarus"),
"BZ": (__) => __("Belize"),
"CA": (__) => __("Canada"),
"CC": (__) => __("Cocos (Keeling) Islands"),
"CD": (__) => __("Congo (DRC)"),
"CF": (__) => __("Central African Republic"),
"CG": (__) => __("Congo (Republic)"),
"CH": (__) => __("Switzerland"),
"CI": (__) => __("Côte d'Ivoire"),
"CK": (__) => __("Cook Islands"),
"CL": (__) => __("Chile"),
"CM": (__) => __("Cameroon"),
"CN": (__) => __("China"),
"CO": (__) => __("Colombia"),
"CR": (__) => __("Costa Rica"),
"CU": (__) => __("Cuba"),
"CV": (__) => __("Cape Verde"),
"CW": (__) => __("Curaçao"),
"CX": (__) => __("Christmas Island"),
"CY": (__) => __("Cyprus"),
"CZ": (__) => __("Czechia"),
"DE": (__) => __("Germany"),
"DJ": (__) => __("Djibouti"),
"DK": (__) => __("Denmark"),
"DM": (__) => __("Dominica"),
"DO": (__) => __("Dominican Republic"),
"DZ": (__) => __("Algeria"),
"EC": (__) => __("Ecuador"),
"EE": (__) => __("Estonia"),
"EG": (__) => __("Egypt"),
"EH": (__) => __("Western Sahara"),
"ER": (__) => __("Eritrea"),
"ES": (__) => __("Spain"),
"ET": (__) => __("Ethiopia"),
"EU": (__) => __("EU"),
"FI": (__) => __("Finland"),
"FJ": (__) => __("Fiji"),
"FK": (__) => __("Falkland Islands"),
"FM": (__) => __("Micronesia"),
"FO": (__) => __("Faroe Islands"),
"FR": (__) => __("France"),
"GA": (__) => __("Gabon"),
"GB": (__) => __("United Kingdom"),
"GD": (__) => __("Grenada"),
"GE": (__) => __("Georgia"),
"GF": (__) => __("French Guiana"),
"GG": (__) => __("Guernsey"),
"GH": (__) => __("Ghana"),
"GI": (__) => __("Gibraltar"),
"GL": (__) => __("Greenland"),
"GM": (__) => __("Gambia"),
"GN": (__) => __("Guinea"),
"GP": (__) => __("Guadeloupe"),
"GQ": (__) => __("Equatorial Guinea"),
"GR": (__) => __("Greece"),
"GT": (__) => __("Guatemala"),
"GU": (__) => __("Guam"),
"GW": (__) => __("Guinea-Bissau"),
"GY": (__) => __("Guyana"),
"HK": (__) => __("Hong Kong"),
"HM": (__) => __("Heard Island and McDonald Islands"),
"HN": (__) => __("Honduras"),
"HR": (__) => __("Croatia"),
"HT": (__) => __("Haiti"),
"HU": (__) => __("Hungary"),
"ID": (__) => __("Indonesia"),
"IE": (__) => __("Ireland"),
"IL": (__) => __("Israel"),
"IM": (__) => __("Isle of Man"),
"IN": (__) => __("India"),
"IO": (__) => __("British Indian Ocean Territory"),
"IQ": (__) => __("Iraq"),
"IR": (__) => __("Iran"),
"IS": (__) => __("Iceland"),
"IT": (__) => __("Italy"),
"JE": (__) => __("Jersey"),
"JM": (__) => __("Jamaica"),
"JO": (__) => __("Jordan"),
"JP": (__) => __("Japan"),
"KE": (__) => __("Kenya"),
"KG": (__) => __("Kyrgyzstan"),
"KH": (__) => __("Cambodia"),
"KI": (__) => __("Kiribati"),
"KM": (__) => __("Comoros"),
"KN": (__) => __("Saint Kitts and Nevis"),
"KP": (__) => __("North Korea"),
"KR": (__) => __("South Korea"),
"KW": (__) => __("Kuwait"),
"KY": (__) => __("Cayman Islands"),
"KZ": (__) => __("Kazakhstan"),
"LA": (__) => __("Laos"),
"LB": (__) => __("Lebanon"),
"LC": (__) => __("Saint Lucia"),
"LI": (__) => __("Liechtenstein"),
"LK": (__) => __("Sri Lanka"),
"LR": (__) => __("Liberia"),
"LS": (__) => __("Lesotho"),
"LT": (__) => __("Lithuania"),
"LU": (__) => __("Luxembourg"),
"LV": (__) => __("Latvia"),
"LY": (__) => __("Libya"),
"MA": (__) => __("Morocco"),
"MC": (__) => __("Monaco"),
"MD": (__) => __("Moldova"),
"ME": (__) => __("Montenegro"),
"MF": (__) => __("Saint Martin"),
"MG": (__) => __("Madagascar"),
"MH": (__) => __("Marshall Islands"),
"MK": (__) => __("North Macedonia"),
"ML": (__) => __("Mali"),
"MM": (__) => __("Myanmar"),
"MN": (__) => __("Mongolia"),
"MO": (__) => __("Macau"),
"MP": (__) => __("Northern Mariana Islands"),
"MQ": (__) => __("Martinique"),
"MR": (__) => __("Mauritania"),
"MS": (__) => __("Montserrat"),
"MT": (__) => __("Malta"),
"MU": (__) => __("Mauritius"),
"MV": (__) => __("Maldives"),
"MW": (__) => __("Malawi"),
"MX": (__) => __("Mexico"),
"MY": (__) => __("Malaysia"),
"MZ": (__) => __("Mozambique"),
"NA": (__) => __("Namibia"),
"NC": (__) => __("New Caledonia"),
"NE": (__) => __("Niger"),
"NF": (__) => __("Norfolk Island"),
"NG": (__) => __("Nigeria"),
"NI": (__) => __("Nicaragua"),
"NL": (__) => __("Netherlands"),
"NO": (__) => __("Norway"),
"NP": (__) => __("Nepal"),
"NR": (__) => __("Nauru"),
"NU": (__) => __("Niue"),
"NZ": (__) => __("New Zealand"),
"OM": (__) => __("Oman"),
"PA": (__) => __("Panama"),
"PE": (__) => __("Peru"),
"PF": (__) => __("French Polynesia"),
"PG": (__) => __("Papua New Guinea"),
"PH": (__) => __("Philippines"),
"PK": (__) => __("Pakistan"),
"PL": (__) => __("Poland"),
"PM": (__) => __("Saint Pierre and Miquelon"),
"PN": (__) => __("Pitcairn"),
"PR": (__) => __("Puerto Rico"),
"PS": (__) => __("Palestine"),
"PT": (__) => __("Portugal"),
"PW": (__) => __("Palau"),
"PY": (__) => __("Paraguay"),
"QA": (__) => __("Qatar"),
"RE": (__) => __("Réunion"),
"RO": (__) => __("Romania"),
"RS": (__) => __("Serbia"),
"RU": (__) => __("Russia"),
"RW": (__) => __("Rwanda"),
"SA": (__) => __("Saudi Arabia"),
"SB": (__) => __("Solomon Islands"),
"SC": (__) => __("Seychelles"),
"SD": (__) => __("Sudan"),
"SE": (__) => __("Sweden"),
"SG": (__) => __("Singapore"),
"SH": (__) => __("Saint Helena"),
"SI": (__) => __("Slovenia"),
"SJ": (__) => __("Svalbard and Jan Mayen"),
"SK": (__) => __("Slovakia"),
"SL": (__) => __("Sierra Leone"),
"SM": (__) => __("San Marino"),
"SN": (__) => __("Senegal"),
"SO": (__) => __("Somalia"),
"SR": (__) => __("Suriname"),
"SS": (__) => __("South Sudan"),
"ST": (__) => __("São Tomé and Príncipe"),
"SV": (__) => __("El Salvador"),
"SX": (__) => __("Sint Maarten"),
"SY": (__) => __("Syria"),
"SZ": (__) => __("Eswatini"),
"TC": (__) => __("Turks and Caicos Islands"),
"TD": (__) => __("Chad"),
"TF": (__) => __("French Southern Territories"),
"TG": (__) => __("Togo"),
"TH": (__) => __("Thailand"),
"TJ": (__) => __("Tajikistan"),
"TK": (__) => __("Tokelau"),
"TL": (__) => __("Timor-Leste"),
"TM": (__) => __("Turkmenistan"),
"TN": (__) => __("Tunisia"),
"TO": (__) => __("Tonga"),
"TR": (__) => __("Turkey"),
"TT": (__) => __("Trinidad and Tobago"),
"TV": (__) => __("Tuvalu"),
"TW": (__) => __("Taiwan"),
"TZ": (__) => __("Tanzania"),
"UA": (__) => __("Ukraine"),
"UG": (__) => __("Uganda"),
"UM": (__) => __("United States Minor Outlying Islands"),
"US": (__) => __("USA"),
"UY": (__) => __("Uruguay"),
"UZ": (__) => __("Uzbekistan"),
"VA": (__) => __("Vatican City"),
"VC": (__) => __("Saint Vincent and the Grenadines"),
"VE": (__) => __("Venezuela"),
"VG": (__) => __("Virgin Islands (British)"),
"VI": (__) => __("Virgin Islands (U.S.)"),
"VN": (__) => __("Vietnam"),
"VU": (__) => __("Vanuatu"),
"WF": (__) => __("Wallis and Futuna"),
"WS": (__) => __("Samoa"),
"YE": (__) => __("Yemen"),
"YT": (__) => __("Mayotte"),
"ZA": (__) => __("South Africa"),
"ZM": (__) => __("Zambia"),
"ZW": (__) => __("Zimbabwe"),
};
export function getCountryName(__: Translator, code: CountryCode): string {
const translator = countryNames[code];
return translator ? translator(__) : code;
export function getCountryName(language: string, code: CountryCode): string {
return new Intl.DisplayNames(language, { type: "region" }).of(code) ?? code;
}
export function getCountryOptions(__: Translator) {
export function getCountryOptions(lang: string) {
return countries.map(code => ({
value: code,
label: getCountryName(__, code),
label: getCountryName(lang, code),
}));
}
export function getCountryLabel(__: Translator, code: CountryCode): string {
return getCountryName(__, code);
export function getCountryLabel(lang: string, code: CountryCode): string {
return getCountryName(lang, code);
}

View File

@@ -44,42 +44,38 @@ export const getCustomDomainStatusBadgeVariant = (sslStatus: Status) => {
export const getCustomDomainStatusBadgeLabel = (
sslStatus: Status,
__: (key: string) => string,
t: (key: string) => string,
) => {
if (sslStatus === "ACTIVE") {
return __("Active");
return t("helpers.customDomain.status.active");
}
if (sslStatus === "PROVISIONING" || sslStatus === "RENEWING") {
return __("Provisioning");
return t("helpers.customDomain.status.provisioning");
}
if (sslStatus === "PENDING") {
return __("Pending");
return t("helpers.customDomain.status.pending");
}
if (sslStatus === "FAILED") {
return __("Failed");
return t("helpers.customDomain.status.failed");
}
if (sslStatus === "EXPIRED") {
return __("Expired");
return t("helpers.customDomain.status.expired");
}
return __("Unknown");
return t("helpers.common.unknown");
};
const provisioningErrorMessages: Record<string, string> = {
DNS_CNAME: "DNS is not configured correctly yet. Check the CNAME record.",
DNS_CAA: "DNS CAA records do not allow our certificate authority.",
ACME_RATE_LIMITED:
"Certificate authority is temporarily rate-limiting. We will retry automatically.",
ACME_INVALID_ORDER:
"Domain ownership could not be verified. We will retry automatically.",
ACME_TEMPORARY:
"Certificate provisioning hit a temporary error. We will retry automatically.",
ACME_FAILED:
"Certificate provisioning failed. Contact support if this persists.",
DNS_CNAME: "helpers.customDomain.provisioningError.dnsCname",
DNS_CAA: "helpers.customDomain.provisioningError.dnsCaa",
ACME_RATE_LIMITED: "helpers.customDomain.provisioningError.rateLimited",
ACME_INVALID_ORDER: "helpers.customDomain.provisioningError.invalidOrder",
ACME_TEMPORARY: "helpers.customDomain.provisioningError.temporary",
ACME_FAILED: "helpers.customDomain.provisioningError.failed",
};
export const getCertificateProvisioningErrorMessage = (
provisioningError: string | null | undefined,
__: (key: string) => string,
t: (key: string) => string,
) => {
if (!provisioningError) {
return null;
@@ -87,10 +83,8 @@ export const getCertificateProvisioningErrorMessage = (
const knownMessage = provisioningErrorMessages[provisioningError];
if (knownMessage) {
return __(knownMessage);
return t(knownMessage);
}
return __(
"Certificate provisioning failed. Contact support if this persists.",
);
return t("helpers.customDomain.provisioningError.failed");
};

View File

@@ -36,13 +36,6 @@ export function todayAsDateInput(): string {
return `${year}-${month}-${day}`;
}
export function formatDate(dateInput?: string | null): string {
if (!dateInput) return "";
const date = parseDate(dateInput);
return date.toLocaleDateString();
}
export function parseDate(dateString: string): Date {
if (dateString.includes("T")) {
return new Date(dateString);
@@ -54,36 +47,3 @@ export function parseDate(dateString: string): Date {
parts[2] ? parseInt(parts[2], 10) : 1,
);
}
export function formatDuration(duration?: string | null, __?: (s: string) => string): string | null {
if (!duration || !__) 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 `${amount} ${amount === 1 ? __("Minute") : __("Minutes")}`;
} else if (unit === "H") {
return `${amount} ${amount === 1 ? __("Hour") : __("Hours")}`;
}
}
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 ? __("Week") : __("Weeks")}`;
} else if (unit === "D") {
const days = amount;
if (days % 7 === 0 && days > 0) {
const weeks = days / 7;
return `${weeks} ${weeks === 1 ? __("Week") : __("Weeks")}`;
}
return `${days} ${days === 1 ? __("Day") : __("Days")}`;
}
}
return null;
}

View File

@@ -22,39 +22,39 @@ type Translator = (s: string) => string;
export const documentTypes = ["OTHER", "GOVERNANCE", "POLICY", "PROCEDURE", "PLAN", "REGISTER", "RECORD", "REPORT", "TEMPLATE", "STATEMENT_OF_APPLICABILITY"] as const;
export function getDocumentTypeLabel(__: Translator, type: string) {
export function getDocumentTypeLabel(t: Translator, type: string) {
switch (type) {
case "OTHER":
return __("Other");
return t("helpers.documentType.other");
case "GOVERNANCE":
return __("Governance");
return t("helpers.documentType.governance");
case "POLICY":
return __("Policy");
return t("helpers.documentType.policy");
case "PROCEDURE":
return __("Procedure");
return t("helpers.documentType.procedure");
case "PLAN":
return __("Plan");
return t("helpers.documentType.plan");
case "REGISTER":
return __("Register");
return t("helpers.documentType.register");
case "RECORD":
return __("Record");
return t("helpers.documentType.record");
case "REPORT":
return __("Report");
return t("helpers.documentType.report");
case "TEMPLATE":
return __("Template");
return t("helpers.documentType.template");
case "STATEMENT_OF_APPLICABILITY":
return __("Statement of Applicability");
return t("helpers.documentType.statementOfApplicability");
}
}
export const documentWriteModes = ["AUTHORED", "GENERATED"] as const;
export function getDocumentWriteModeLabel(__: Translator, writeMode: string) {
export function getDocumentWriteModeLabel(t: Translator, writeMode: string) {
switch (writeMode) {
case "AUTHORED":
return __("Authored");
return t("helpers.documentWriteMode.authored");
case "GENERATED":
return __("Generated");
return t("helpers.documentWriteMode.generated");
}
}
@@ -65,15 +65,15 @@ export const documentClassifications = [
"SECRET",
] as const;
export function getDocumentClassificationLabel(__: Translator, classification: string) {
export function getDocumentClassificationLabel(t: Translator, classification: string) {
switch (classification) {
case "PUBLIC":
return __("Public");
return t("helpers.documentClassification.public");
case "INTERNAL":
return __("Internal");
return t("helpers.documentClassification.internal");
case "CONFIDENTIAL":
return __("Confidential");
return t("helpers.documentClassification.confidential");
case "SECRET":
return __("Secret");
return t("helpers.documentClassification.secret");
}
}

View File

@@ -28,49 +28,6 @@ export const DURATION_UNITS: { value: string; label: string; singular: string; s
{ value: "years", label: "years", singular: "year", seconds: 31536000, snap: 21 * 24 * 3600 },
] as const;
// Tracker types whose data persists until explicitly cleared. When such a
// tracker has no max-age, its lifetime is "persistent" rather than "session"
// (cookies and session storage are cleared when the session/tab ends).
const PERSISTENT_TRACKER_TYPES = new Set([
"LOCAL_STORAGE",
"INDEXED_DB",
"CACHE_STORAGE",
]);
export function humanizeSeconds(
seconds: number | null,
trackerType?: string | null,
): string {
if (seconds === null || seconds <= 0) {
return trackerType && PERSISTENT_TRACKER_TYPES.has(trackerType)
? "persistent"
: "session";
}
let remaining = seconds;
const parts: string[] = [];
for (const {label, singular, 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} ${count === 1 ? singular : label}`);
}
}
return parts.length > 0 ? parts.join(", ") : "session";
}
export function toMaxAgeSeconds(value: string, unit: string): number | null {
const trimmed = value.trim();
if (trimmed === "" || !/^\d+(\.\d+)?$/.test(trimmed)) return null;

View File

@@ -96,7 +96,6 @@ export {
type CompliancePortalVisibility as CompliancePageVisibility,
} from "./compliancePortalVisibility";
export { promisifyMutation } from "./relay";
export { fileType, fileSize } from "./file";
export {
acceptDocument,
acceptSpreadsheet,
@@ -112,11 +111,9 @@ export {
formatDate,
toDateInput,
todayAsDateInput,
formatDuration,
parseDate,
} from "./date";
export {
humanizeSeconds,
DURATION_UNITS,
toMaxAgeSeconds,
fromMaxAgeSeconds,

View File

@@ -29,21 +29,21 @@ export const measureStates = [
"NOT_IMPLEMENTED",
] as const;
export function getMeasureStateLabel(__: Translator, state: string) {
export function getMeasureStateLabel(t: Translator, state: string) {
switch (state) {
case "IMPLEMENTED":
return __("Implemented");
return t("helpers.measureState.implemented");
case "IN_PROGRESS":
return __("In Progress");
return t("helpers.measureState.inProgress");
case "NOT_APPLICABLE":
return __("Not Applicable");
return t("helpers.measureState.notApplicable");
case "NOT_STARTED":
return __("Not Started");
return t("helpers.measureState.notStarted");
case "UNKNOWN":
return __("Unknown");
return t("helpers.common.unknown");
case "NOT_IMPLEMENTED":
return __("Not Implemented");
return t("helpers.measureState.notImplemented");
default:
return __("Unknown");
return t("helpers.common.unknown");
}
}

View File

@@ -54,13 +54,13 @@ export const getObligationStatusLabel = (status: ObligationStatus) => {
}
};
export function getObligationStatusOptions(__: Translator) {
export function getObligationStatusOptions(t: Translator) {
return obligationStatuses.map((status) => ({
value: status,
label: __({
"NON_COMPLIANT": "Non-compliant",
"PARTIALLY_COMPLIANT": "Partially compliant",
"COMPLIANT": "Compliant",
label: t({
"NON_COMPLIANT": "helpers.obligationStatus.nonCompliant",
"PARTIALLY_COMPLIANT": "helpers.obligationStatus.partiallyCompliant",
"COMPLIANT": "helpers.obligationStatus.compliant",
}[status]),
}));
}

View File

@@ -38,12 +38,12 @@ export const getObligationTypeLabel = (type: ObligationType) => {
}
};
export function getObligationTypeOptions(__: Translator) {
export function getObligationTypeOptions(t: Translator) {
return obligationTypes.map((type) => ({
value: type,
label: __({
"LEGAL": "Legal",
"CONTRACTUAL": "Contractual",
label: t({
"LEGAL": "helpers.obligationType.legal",
"CONTRACTUAL": "helpers.obligationType.contractual",
}[type]),
}));
}

View File

@@ -26,32 +26,32 @@ export const peopleRoles = [
"SERVICE_ACCOUNT",
] as const;
export function getRoles(__: Translator) {
export function getRoles(t: Translator) {
return [
{
value: "EMPLOYEE",
label: __("Employee"),
label: t("helpers.peopleRole.employee"),
},
{
value: "CONTRACTOR",
label: __("Contractor"),
label: t("helpers.peopleRole.contractor"),
},
{
value: "SERVICE_ACCOUNT",
label: __("Service account"),
label: t("helpers.peopleRole.serviceAccount"),
},
];
}
export function getRole(__: Translator, role?: string): string {
export function getRole(t: Translator, role?: string): string {
switch (role) {
case "EMPLOYEE":
return __("Employee");
return t("helpers.peopleRole.employee");
case "CONTRACTOR":
return __("Contractor");
return t("helpers.peopleRole.contractor");
case "SERVICE_ACCOUNT":
return __("Service account");
return t("helpers.peopleRole.serviceAccount");
default:
return __("Unknown");
return t("helpers.common.unknown");
}
}

View File

@@ -69,16 +69,16 @@ export const getStatusLabel = (status: RegistryStatus) => {
}
};
export function getStatusOptions(__: Translator) {
export function getStatusOptions(t: Translator) {
return registryStatuses.map((status) => ({
value: status,
label: __({
"OPEN": "Open",
"IN_PROGRESS": "In Progress",
"CLOSED": "Closed",
"RISK_ACCEPTED": "Risk Accepted",
"MITIGATED": "Mitigated",
"FALSE_POSITIVE": "False Positive",
label: t({
"OPEN": "helpers.registryStatus.open",
"IN_PROGRESS": "helpers.registryStatus.inProgress",
"CLOSED": "helpers.registryStatus.closed",
"RISK_ACCEPTED": "helpers.registryStatus.riskAccepted",
"MITIGATED": "helpers.registryStatus.mitigated",
"FALSE_POSITIVE": "helpers.registryStatus.falsePositive",
}[status]),
}));
}

View File

@@ -46,23 +46,23 @@ export const rightsRequestStates = [
"REJECTED",
] as const;
const rightsRequestTypeLabels: Record<RightsRequestType, string> = {
"ACCESS": "Access",
"DELETION": "Deletion",
"RECTIFICATION": "Rectification",
"PORTABILITY": "Portability",
"OBJECTION": "Objection",
"COMPLAINT": "Complaint",
const rightsRequestTypeKeys: Record<RightsRequestType, string> = {
"ACCESS": "helpers.rightsRequestType.access",
"DELETION": "helpers.rightsRequestType.deletion",
"RECTIFICATION": "helpers.rightsRequestType.rectification",
"PORTABILITY": "helpers.rightsRequestType.portability",
"OBJECTION": "helpers.rightsRequestType.objection",
"COMPLAINT": "helpers.rightsRequestType.complaint",
};
export function getRightsRequestTypeLabel(__: Translator, type: RightsRequestType) {
return __(rightsRequestTypeLabels[type] ?? type);
export function getRightsRequestTypeLabel(t: Translator, type: RightsRequestType) {
return t(rightsRequestTypeKeys[type]);
}
export function getRightsRequestTypeOptions(__: Translator) {
export function getRightsRequestTypeOptions(t: Translator) {
return rightsRequestTypes.map((type) => ({
value: type,
label: __(rightsRequestTypeLabels[type]),
label: t(rightsRequestTypeKeys[type]),
}));
}
@@ -83,20 +83,20 @@ export const getRightsRequestStateVariant = (
}
};
const rightsRequestStateLabels: Record<RightsRequestState, string> = {
"TODO": "To Do",
"IN_PROGRESS": "In Progress",
"DONE": "Done",
"REJECTED": "Rejected",
const rightsRequestStateKeys: Record<RightsRequestState, string> = {
"TODO": "helpers.rightsRequestState.todo",
"IN_PROGRESS": "helpers.rightsRequestState.inProgress",
"DONE": "helpers.rightsRequestState.done",
"REJECTED": "helpers.rightsRequestState.rejected",
};
export function getRightsRequestStateLabel(__: Translator, state: RightsRequestState) {
return __(rightsRequestStateLabels[state] ?? state);
export function getRightsRequestStateLabel(t: Translator, state: RightsRequestState) {
return t(rightsRequestStateKeys[state]);
}
export function getRightsRequestStateOptions(__: Translator) {
export function getRightsRequestStateOptions(t: Translator) {
return rightsRequestStates.map((state) => ({
value: state,
label: __(rightsRequestStateLabels[state]),
label: t(rightsRequestStateKeys[state]),
}));
}

View File

@@ -20,97 +20,97 @@
type Translator = (s: string) => string;
export function getRiskImpacts(__: Translator) {
export function getRiskImpacts(t: Translator) {
return [
{
value: 1,
label: __("Negligible"),
label: t("helpers.riskImpact.negligible"),
},
{
value: 2,
label: __("Low"),
label: t("helpers.riskImpact.low"),
},
{
value: 3,
label: __("Moderate"),
label: t("helpers.riskImpact.moderate"),
},
{
value: 4,
label: __("Significant"),
label: t("helpers.riskImpact.significant"),
},
{
value: 5,
label: __("Catastrophic"),
label: t("helpers.riskImpact.catastrophic"),
},
];
}
export function getTreatment(__: Translator, treatment?: string): string {
export function getTreatment(t: Translator, treatment?: string): string {
switch (treatment) {
case "MITIGATED":
return __("Mitigate");
return t("helpers.riskTreatment.mitigate");
case "ACCEPTED":
return __("Accept");
return t("helpers.riskTreatment.accept");
case "TRANSFERRED":
return __("Transfer");
return t("helpers.riskTreatment.transfer");
case "AVOIDED":
return __("Avoid");
return t("helpers.riskTreatment.avoid");
default:
return __("Unknown");
return t("helpers.common.unknown");
}
}
export function getRiskLikelihoods(__: Translator) {
export function getRiskLikelihoods(t: Translator) {
return [
{
value: 1,
label: __("Improbable"),
label: t("helpers.riskLikelihood.improbable"),
},
{
value: 2,
label: __("Remote"),
label: t("helpers.riskLikelihood.remote"),
},
{
value: 3,
label: __("Occasional"),
label: t("helpers.riskLikelihood.occasional"),
},
{
value: 4,
label: __("Probable"),
label: t("helpers.riskLikelihood.probable"),
},
{
value: 5,
label: __("Frequent"),
label: t("helpers.riskLikelihood.frequent"),
},
];
}
function getRiskSeverities(__: Translator) {
function getRiskSeverities(t: Translator) {
return [
{
min: 15,
variant: "danger",
label: __("Critical"),
label: t("helpers.riskSeverity.critical"),
bg: "bg-danger",
color: "text-txt-danger",
},
{
min: 5,
variant: "warning",
label: __("High"),
label: t("helpers.riskSeverity.high"),
bg: "bg-warning",
color: "text-txt-warning",
},
{
min: 0,
variant: "neutral",
label: __("Low"),
label: t("helpers.riskSeverity.low"),
bg: "bg-txt-quaternary",
color: "text-txt-primary",
},
] as const;
}
export function getSeverity(__: Translator, score: number) {
return getRiskSeverities(__).find((s) => score >= s.min);
export function getSeverity(t: Translator, score: number) {
return getRiskSeverities(t).find((s) => score >= s.min);
}

View File

@@ -33,23 +33,23 @@ type Badge = {
variant: BadgeVariant;
};
export function getTrackerTypeBadge(type: string, __: Translator): Badge {
export function getTrackerTypeBadge(type: string, t: Translator): Badge {
switch (type) {
case "COOKIE": return { label: __("Cookie"), variant: "warning" };
case "LOCAL_STORAGE": return { label: __("localStorage"), variant: "info" };
case "SESSION_STORAGE": return { label: __("sessionStorage"), variant: "highlight" };
case "INDEXED_DB": return { label: __("IndexedDB"), variant: "success" };
case "CACHE_STORAGE": return { label: __("Cache Storage"), variant: "outline" };
case "COOKIE": return { label: t("helpers.trackerType.cookie"), variant: "warning" };
case "LOCAL_STORAGE": return { label: t("helpers.trackerType.localStorage"), variant: "info" };
case "SESSION_STORAGE": return { label: t("helpers.trackerType.sessionStorage"), variant: "highlight" };
case "INDEXED_DB": return { label: t("helpers.trackerType.indexedDb"), variant: "success" };
case "CACHE_STORAGE": return { label: t("helpers.trackerType.cacheStorage"), variant: "outline" };
default: return { label: type, variant: "neutral" };
}
}
export function getTrackerSourceBadge(source: string, __: Translator): Badge {
export function getTrackerSourceBadge(source: string, t: Translator): Badge {
switch (source) {
case "SCRIPT": return { label: __("Script"), variant: "info" };
case "PRE_EXISTING": return { label: __("Pre-existing"), variant: "outline" };
case "HTTP": return { label: __("HTTP"), variant: "neutral" };
case "EXTENSION": return { label: __("Extension"), variant: "warning" };
case "SCRIPT": return { label: t("helpers.trackerSource.script"), variant: "info" };
case "PRE_EXISTING": return { label: t("helpers.trackerSource.preExisting"), variant: "outline" };
case "HTTP": return { label: t("helpers.trackerSource.http"), variant: "neutral" };
case "EXTENSION": return { label: t("helpers.trackerSource.extension"), variant: "warning" };
default: return { label: source, variant: "neutral" };
}
}

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");
});
});

View File

@@ -18,15 +18,6 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { describe, it, expect } from "vitest";
import { fileSize } from "./file";
describe("file", () => {
it("should display a file size in a human readable format", () => {
const fakeTranslator = (s: string) => s;
expect(fileSize(fakeTranslator, 4911)).toMatchInlineSnapshot(
`"4.8 KB"`,
);
expect(fileSize(fakeTranslator, 20)).toMatchInlineSnapshot(`"20 B"`);
});
});
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"
}
}

View File

@@ -18,37 +18,18 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
/**
* Return the file size in a human readable format
*/
export function fileSize(__: (s: string) => string, size: number): string {
if (size < 0) return "";
if (size === 0) return `0 ${__("B")}`;
type Translator = (key: string, options?: { count?: number }) => string;
const units = [__("B"), __("KB"), __("MB"), __("GB"), __("TB")];
const i = Math.floor(Math.log(size) / Math.log(1024));
export function fileSize(size: number, t: Translator): string {
if (size < 0) return "";
if (size === 0) return `0 ${t("size.B")}`;
// Don't go beyond available units
const unitIndex = Math.min(i, units.length - 1);
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;
// Convert to the appropriate unit with 2 decimal places
const convertedSize = size / Math.pow(1024, unitIndex);
const formattedSize = Math.round(convertedSize * 100) / 100;
return `${formattedSize} ${units[unitIndex]}`;
}
type FileInfo = {
type: string;
mimeType: string;
};
export function fileType(__: (s: string) => string, info: FileInfo): string {
if (
info.type !== "FILE" ||
(info.mimeType !== "text/uri-list" && info.mimeType !== "text/uri")
) {
return __("Document");
}
return __("Link");
return `${formattedSize} ${t(`size.${units[unitIndex]}`)}`;
}

View File

@@ -38,6 +38,7 @@
"cmdk": "^1.1.1",
"mermaid": "^11.15.0",
"react-dropzone": "^15.0.0",
"react-i18next": "^17.0.10",
"react-intersection-observer": "^10.0.3",
"react-markdown": "^10.1.0",
"rehype-raw": "^7.0.0",

View File

@@ -19,9 +19,9 @@
// SOFTWARE.
import { useRefSync } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { useCallback } from "react";
import { type FileRejection, useDropzone } from "react-dropzone";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { IconPageCross, IconUpload } from "../Icons";
@@ -79,7 +79,7 @@ export const dropzone = tv({
const MB = 1024 * 1024;
export function Dropzone(props: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const onDropRef = useRefSync(props.onDrop);
const onDrop = useCallback(
(files: File[]) => {
@@ -94,7 +94,7 @@ export function Dropzone(props: Props) {
onDrop,
maxSize: props.maxSize ? props.maxSize * MB : undefined,
});
const error = getDropzoneError(__, fileRejections);
const error = getDropzoneError(t, fileRejections);
const { wrapper, zone, title, description } = dropzone({
...props,
isDragActive,
@@ -109,7 +109,7 @@ export function Dropzone(props: Props) {
? (
<div className={title({ isDragActive: true })}>
<Spinner />
{__("Uploading file")}
{t("ui.dropzone.uploading")}
</div>
)
: (
@@ -122,7 +122,7 @@ export function Dropzone(props: Props) {
: (
<IconUpload size={20} />
)}
{error ?? __("Drag and drop or browse files")}
{error ?? t("ui.dropzone.browse")}
</div>
<div className={description()}>{props.description}</div>
</div>
@@ -133,7 +133,7 @@ export function Dropzone(props: Props) {
}
function getDropzoneError(
__: (s: string) => string,
t: (key: string) => string,
fileRejections: readonly FileRejection[],
) {
if (fileRejections.length === 0) {
@@ -143,10 +143,10 @@ function getDropzoneError(
const code = fileRejections[0].errors[0].code;
switch (code) {
case "file-invalid-type":
return __("This file is not supported");
return t("ui.dropzone.errors.unsupported");
case "file-too-large":
return __("This file is too large");
return t("ui.dropzone.errors.tooLarge");
default:
return __("Something went wrong");
return t("ui.dropzone.errors.generic");
}
}

View File

@@ -19,8 +19,8 @@
// SOFTWARE.
import { useRefSync } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { type ReactNode, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { useInView } from "react-intersection-observer";
import { Spinner } from "../Spinner/Spinner";
@@ -32,7 +32,7 @@ type Props = {
};
export function InfiniteScrollTrigger({ children, onView, loading }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const { ref, inView } = useInView({
threshold: 0,
});
@@ -49,7 +49,7 @@ export function InfiniteScrollTrigger({ children, onView, loading }: Props) {
{children ?? (
<>
<Spinner size={16} />
{__("Loading")}
{t("ui.loading")}
</>
)}
</div>

View File

@@ -18,8 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { getDocumentClassificationLabel } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -28,7 +27,7 @@ type Props = {
};
export function DocumentClassificationBadge({ classification }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
// Choose badge variant based on classification level
const variant = (() => {
@@ -48,7 +47,7 @@ export function DocumentClassificationBadge({ classification }: Props) {
return (
<Badge variant={variant}>
{getDocumentClassificationLabel(__, classification)}
{t(`ui.documentClassification.${classification.toLowerCase()}`)}
</Badge>
);
}

View File

@@ -18,8 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { getDocumentTypeLabel } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -28,6 +27,10 @@ type Props = {
};
export function DocumentTypeBadge({ type }: Props) {
const { __ } = useTranslate();
return <Badge variant="neutral">{getDocumentTypeLabel(__, type)}</Badge>;
const { t } = useTranslation();
return (
<Badge variant="neutral">
{t(`ui.documentType.${type.toLowerCase()}`)}
</Badge>
);
}

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -27,7 +27,7 @@ type Props = {
};
export function DocumentVersionBadge(props: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const variant = {
DRAFT: "neutral",
@@ -36,9 +36,9 @@ export function DocumentVersionBadge(props: Props) {
} as const;
const label = {
DRAFT: __("Draft"),
PENDING_APPROVAL: __("Pending approval"),
PUBLISHED: __("Published"),
DRAFT: t("ui.documentVersionBadge.draft"),
PENDING_APPROVAL: t("ui.documentVersionBadge.pendingApproval"),
PUBLISHED: t("ui.documentVersionBadge.published"),
} as const;
return <Badge variant={variant[props.state]}>{label[props.state]}</Badge>;

View File

@@ -19,9 +19,8 @@
// SOFTWARE.
import type { measureStates } from "@probo/helpers";
import { getMeasureStateLabel } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -42,10 +41,10 @@ const stateToVariant: Record<
};
export function MeasureBadge({ state }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
return (
<Badge variant={stateToVariant[state]}>
{getMeasureStateLabel(__, state)}
{t(`ui.measureState.${state.toLowerCase()}`)}
</Badge>
);
}

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -51,30 +51,30 @@ const badgeVariant = (level: string | number) => {
};
export function RiskBadge({ level }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const label = () => {
if (typeof level === "number") {
if (level >= 15) {
return __("High");
return t("ui.risk.severity.high");
}
if (level >= 8) {
return __("Medium");
return t("ui.risk.severity.medium");
}
return __("Low");
return t("ui.risk.severity.low");
}
switch (level) {
case "CRITICAL":
return __("Critical");
return t("ui.risk.severity.critical");
case "HIGH":
return __("High");
return t("ui.risk.severity.high");
case "LOW":
return __("Low");
return t("ui.risk.severity.low");
case "MEDIUM":
return __("Medium");
return t("ui.risk.severity.medium");
case "NONE":
return __("None");
return t("ui.risk.severity.none");
default:
return __("Low");
return t("ui.risk.severity.low");
}
};
return <Badge variant={badgeVariant(level)}>{label()}</Badge>;

View File

@@ -18,8 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { getSeverity } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -28,7 +27,15 @@ type Props = {
};
export function SeverityBadge({ score }: Props) {
const { __ } = useTranslate();
const severity = getSeverity(__, score);
return <Badge variant={severity?.variant}>{severity?.label}</Badge>;
const { t } = useTranslation();
const severity = score >= 15
? { variant: "danger" as const, key: "critical" }
: score >= 5
? { variant: "warning" as const, key: "high" }
: { variant: "neutral" as const, key: "low" };
return (
<Badge variant={severity.variant}>
{t(`ui.risk.severity.${severity.key}`)}
</Badge>
);
}

View File

@@ -18,7 +18,6 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import {
Cancel,
Content,
@@ -29,6 +28,7 @@ import {
} from "@radix-ui/react-alert-dialog";
import { Root as Portal } from "@radix-ui/react-portal";
import { type ComponentProps, useCallback, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { create } from "zustand";
import { combine } from "zustand/middleware";
@@ -70,7 +70,7 @@ const useConfirmStore = create(
*/
export function useConfirm() {
const open = useConfirmStore(state => state.open);
const { __ } = useTranslate();
const { t } = useTranslation();
return useCallback(
(cb: State["onConfirm"], props: Omit<State, "onConfirm">) => {
@@ -78,12 +78,12 @@ export function useConfirm() {
onConfirm: cb,
...props,
message: props.message,
title: props.title ?? __("Are you sure ?"),
title: props.title ?? t("ui.confirmDialog.defaultTitle"),
variant: props.variant ?? "danger",
label: props.label ?? __("Delete"),
label: props.label ?? t("ui.actions.delete"),
});
},
[open, __],
[open, t],
);
}
@@ -109,7 +109,7 @@ function ConfirmDialogContent() {
const onConfirm = useConfirmStore(state => state.onConfirm);
const close = useConfirmStore(state => state.close);
const { __ } = useTranslate();
const { t } = useTranslation();
const isOpen = !!message;
const [loading, setLoading] = useState(false);
@@ -156,7 +156,7 @@ function ConfirmDialogContent() {
<footer className={dialogStyles.footer}>
<Cancel asChild>
<Button disabled={loading} variant="tertiary">
{__("Cancel")}
{t("ui.actions.cancel")}
</Button>
</Cancel>
<Button

View File

@@ -18,7 +18,6 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import {
Close,
Content,
@@ -42,6 +41,7 @@ import {
useRef,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Button } from "../../Atoms/Button/Button";
@@ -210,14 +210,14 @@ export function DialogFooter({
// switches to justify-between; otherwise it keeps the default right alignment.
start?: ReactNode;
}) {
const { __ } = useTranslate();
const { t } = useTranslation();
const { footer } = dialog();
return (
<footer className={footer({ className: clsx(start != null && "justify-between", className) })}>
{start != null && <div className="flex items-center gap-2">{start}</div>}
<div className="flex items-center gap-2">
<Close asChild>
<Button variant="secondary">{exitLabel ?? __("Cancel")}</Button>
<Button variant="secondary">{exitLabel ?? t("ui.actions.cancel")}</Button>
</Close>
{children}
</div>

View File

@@ -18,8 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import type { HTMLAttributes } from "react";
import { useTranslation } from "react-i18next";
import { Button } from "../../Atoms/Button/Button";
import { IconPlusLarge } from "../../Atoms/Icons";
@@ -60,7 +60,7 @@ const parse = (value: string): { amount: number; unit: string } => {
};
export function DurationPicker({ value, onValueChange, ...props }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
if (!value) {
return (
<div>
@@ -92,10 +92,10 @@ export function DurationPicker({ value, onValueChange, ...props }: Props) {
onValueChange={(v: string) =>
onValueChange(stringify(amount, v))}
>
<Option value="M">{__("Minutes")}</Option>
<Option value="H">{__("Hours")}</Option>
<Option value="D">{__("Days")}</Option>
<Option value="W">{__("Weeks")}</Option>
<Option value="M">{t("ui.durationPicker.minutes")}</Option>
<Option value="H">{t("ui.durationPicker.hours")}</Option>
<Option value="D">{t("ui.durationPicker.days")}</Option>
<Option value="W">{t("ui.durationPicker.weeks")}</Option>
</Select>
</div>
);

View File

@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Button } from "../../Atoms/Button/Button";
import { Dropdown, DropdownItem } from "../../Atoms/Dropdown/Dropdown";
@@ -128,7 +128,7 @@ type Props = {
};
export function FrameworkSelector({ disabled, onSelect }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
return (
<Dropdown
toggle={(
@@ -137,7 +137,7 @@ export function FrameworkSelector({ disabled, onSelect }: Props) {
iconAfter={IconChevronDown}
disabled={disabled}
>
{__("New framework")}
{t("ui.frameworkSelector.new")}
</Button>
)}
>
@@ -154,7 +154,7 @@ export function FrameworkSelector({ disabled, onSelect }: Props) {
}
function FrameworkItem(props: { framework?: Framework; onClick: () => void }) {
const { __ } = useTranslate();
const { t } = useTranslation();
if (!props.framework) {
return (
<DropdownItem onClick={props.onClick} className="">
@@ -163,10 +163,10 @@ function FrameworkItem(props: { framework?: Framework; onClick: () => void }) {
</div>
<div className="space-y-[2px]">
<div className="text-sm font-medium">
{__("Custom framework")}
{t("ui.frameworkSelector.custom.title")}
</div>
<div className="text-xs text-txt-secondary">
{__("Start from scratch")}
{t("ui.frameworkSelector.custom.description")}
</div>
</div>
</DropdownItem>

View File

@@ -18,10 +18,10 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { getMeasureStateLabel, measureStates } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { measureStates } from "@probo/helpers";
import { clsx } from "clsx";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import type { MeasureBadge } from "../Badge/MeasureBadge";
@@ -42,7 +42,7 @@ const stateToColor: Record<MeasureState, string> = {
};
export function MeasureImplementation({ measures, className }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const counts = measures.reduce(
(acc, measure) => {
acc[measure.state] = (acc[measure.state] ?? 0) + 1;
@@ -58,7 +58,7 @@ export function MeasureImplementation({ measures, className }: Props) {
return (
<div className={clsx("space-y-3", className)}>
<h2 className="text-base font-medium">
{__("Measure implementation")}
{t("ui.measureImplementation.title")}
</h2>
<div className="h-2 rounded overflow-hidden bg-highlight flex justify-stretch item-stretch">
{measureStates.map(state => (
@@ -76,7 +76,7 @@ export function MeasureImplementation({ measures, className }: Props) {
<div className="mr-auto">
{percent}
%
{__("Complete")}
{t("ui.measureImplementation.complete")}
</div>
)}
{measureStates.map(state => (
@@ -91,7 +91,7 @@ export function MeasureImplementation({ measures, className }: Props) {
)}
>
</div>
{getMeasureStateLabel(__, state)}
{t(`ui.measureState.${state.toLowerCase()}`)}
</div>
))}
</div>

View File

@@ -23,8 +23,8 @@ import {
getRiskLikelihoods,
getSeverity,
} from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { clsx } from "clsx";
import { useTranslation } from "react-i18next";
import { Card } from "../../Atoms/Card/Card";
@@ -48,29 +48,31 @@ const getColor = (score: number): string => {
};
export function RiskOverview({ type, risk }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const impact = risk?.[`${type}Impact`] ?? 0;
const likelihood = risk?.[`${type}Likelihood`] ?? 0;
const severity = getSeverity(__, impact * likelihood);
const severity = getSeverity(t, impact * likelihood);
return (
<Card padded>
<h2 className="font-semibold text-base mb-6">
{type === "inherent" ? __("Initial Risk") : __("Residual Risk")}
{type === "inherent"
? t("ui.risk.initial")
: t("ui.risk.residual")}
</h2>
<div className="grid grid-cols-2 gap-4 mb-4">
<RiskOverviewBadge
label={__("Impact")}
label={t("ui.risk.impact.label")}
textCb={getRiskImpacts}
score={impact}
/>
<RiskOverviewBadge
label={__("Likelihood")}
label={t("ui.risk.likelihood.label")}
textCb={getRiskLikelihoods}
score={likelihood}
/>
</div>
<div className="space-y-2">
<div className="font-medium text-xs">{__("Score")}</div>
<div className="font-medium text-xs">{t("ui.risk.score")}</div>
<div
className={clsx(
severity?.bg,
@@ -94,17 +96,17 @@ function RiskOverviewBadge({
label: string;
textCb: (t: (s: string) => string) => { value: number; label: string }[];
}) {
const { __ } = useTranslate();
const { t } = useTranslation();
return (
<div className="space-y-2">
<div className="font-medium text-xs">{__(label)}</div>
<div className="font-medium text-xs">{label}</div>
<div
className={clsx(
getColor(score),
"py-2 text-sm font-semibold rounded-lg text-txt-invert text-center",
)}
>
{textCb(__).find(i => i.value === score)?.label}
{textCb(t).find(i => i.value === score)?.label}
{" "}
(
{score}

View File

@@ -19,9 +19,9 @@
// SOFTWARE.
import { getRiskImpacts, getRiskLikelihoods, groupBy } from "@probo/helpers";
import { useTranslate } from "@probo/i18n";
import { clsx } from "clsx";
import { Fragment, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router";
import { Card } from "../../Atoms/Card/Card";
@@ -66,12 +66,16 @@ const cellKey = (impact: number, likelihood: number) =>
* Displays a grid of risk grouped by impact & likelihood
*/
export function RisksChart({ organizationId, type, risks }: Props) {
const { __ } = useTranslate();
const { t } = useTranslation();
const legend = [__("Low"), __("High"), __("Critical")];
const legend = [
t("ui.risk.severity.low"),
t("ui.risk.severity.high"),
t("ui.risk.severity.critical"),
];
const impacts = getRiskImpacts(__).reverse();
const likelihoods = getRiskLikelihoods(__);
const impacts = getRiskImpacts(t).reverse();
const likelihoods = getRiskLikelihoods(t);
const impactField
= type === "inherent" ? "inherentImpact" : "residualImpact";
const likelihoodField
@@ -88,8 +92,8 @@ export function RisksChart({ organizationId, type, risks }: Props) {
<div className="flex justify-between items-center mb-6">
<h2 className="font-semibold text-lg">
{type === "inherent"
? __("Initial Risk")
: __("Residual Risk")}
? t("ui.risk.initial")
: t("ui.risk.residual")}
</h2>
<div className="flex gap-3">
{legend.map((label, i) => (
@@ -114,7 +118,7 @@ export function RisksChart({ organizationId, type, risks }: Props) {
className="text-xs font-medium flex-none text-center"
style={{ writingMode: "sideways-lr" }}
>
{__("Impact")}
{t("ui.risk.impact.label")}
</div>
<div className="grid grid-cols-[90px_1fr_1fr_1fr_1fr_1fr] gap-1 w-full">
{impacts.map(impact => (
@@ -158,7 +162,7 @@ export function RisksChart({ organizationId, type, risks }: Props) {
)
{likelihood.value === 3 && (
<div className="text-xs text-txt-primary font-medium flex-none text-center mt-3">
{__("Likelihood")}
{t("ui.risk.likelihood.label")}
</div>
)}
</div>
@@ -180,7 +184,7 @@ function RisksChartCell({
likelihood: number;
organizationId: string;
}) {
const { __ } = useTranslate();
const { t } = useTranslation();
const level = getLevel(impact * likelihood);
const baseClass
= "flex items-center justify-center aspect-square rounded-xl text-txt-invert text-sm font-semibold";
@@ -189,9 +193,9 @@ function RisksChartCell({
}
const infos = [
{ label: __("Number of risks"), value: risks.length },
{ label: __("Impact"), value: impact },
{ label: __("Likelihood"), value: likelihood },
{ label: t("ui.risk.numberOfRisks"), value: risks.length },
{ label: t("ui.risk.impact.label"), value: impact },
{ label: t("ui.risk.likelihood.label"), value: likelihood },
];
return (
@@ -224,7 +228,9 @@ function RisksChartCell({
<div className="text-txt-primary">{impact * likelihood}</div>
</div>
<DropdownSeparator className="my-3" />
<div className="text-txt-secondary mb-1">{__("Linked Risks")}</div>
<div className="text-txt-secondary mb-1">
{t("ui.risk.linkedRisks")}
</div>
{risks.map(risk => (
<DropdownItem key={risk.id} asChild>
<Link

View File

@@ -18,29 +18,29 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Option } from "../../Atoms/Select/Select";
export function ImpactOptions() {
const { __ } = useTranslate();
const { t } = useTranslation();
const descriptions = {
LOW: {
label: __("Low"),
description: __("Minimal impact on business"),
label: t("ui.risk.severity.low"),
description: t("ui.impactOptions.low"),
},
MEDIUM: {
label: __("Medium"),
description: __("Moderate impact on business"),
label: t("ui.risk.severity.medium"),
description: t("ui.impactOptions.medium"),
},
HIGH: {
label: __("High"),
description: __("Significant business impact"),
label: t("ui.risk.severity.high"),
description: t("ui.impactOptions.high"),
},
CRITICAL: {
label: __("Critical"),
description: __("Critical to business operations"),
label: t("ui.risk.severity.critical"),
description: t("ui.impactOptions.critical"),
},
} as const;

View File

@@ -18,33 +18,33 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
import { useTranslate } from "@probo/i18n";
import { useTranslation } from "react-i18next";
import { Option } from "../../Atoms/Select/Select";
export function SentitivityOptions() {
const { __ } = useTranslate();
const { t } = useTranslation();
const descriptions = {
NONE: {
label: __("None"),
description: __("No sensitive data"),
label: t("ui.risk.severity.none"),
description: t("ui.sensitivityOptions.none"),
},
LOW: {
label: __("Low"),
description: __("Public or non-sensitive data"),
label: t("ui.risk.severity.low"),
description: t("ui.sensitivityOptions.low"),
},
MEDIUM: {
label: __("Medium"),
description: __("Internal/restricted data"),
label: t("ui.risk.severity.medium"),
description: t("ui.sensitivityOptions.medium"),
},
HIGH: {
label: __("High"),
description: __("Confidential data"),
label: t("ui.risk.severity.high"),
description: t("ui.sensitivityOptions.high"),
},
CRITICAL: {
label: __("Critical"),
description: __("Regulated/PII/financial data"),
label: t("ui.risk.severity.critical"),
description: t("ui.sensitivityOptions.critical"),
},
} as const;

View File

@@ -19,9 +19,9 @@
// SOFTWARE.
import { useStateWithRef } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import { Command } from "cmdk";
import { Fragment, type ReactNode } from "react";
import { useTranslation } from "react-i18next";
import { tv } from "tailwind-variants";
import { Badge } from "../../Atoms/Badge/Badge";
@@ -54,7 +54,7 @@ export function SelectCell<T extends NonNullable<unknown>>(props: Props<T>) {
props.defaultValue,
);
const cellRef = useEditableCellRef();
const { __ } = useTranslate();
const { t } = useTranslation();
const filteredValue = Array.isArray(value) ? value.filter(v => v !== undefined) : value ? [value] : [];
const usedKeys = new Set<string>(filteredValue.map(getKey).filter(Boolean) as string[]);
const { onUpdate } = useEditableRowContext();
@@ -105,7 +105,7 @@ export function SelectCell<T extends NonNullable<unknown>>(props: Props<T>) {
{props.multiple && (
<Command.Input
className={classNames.input()}
placeholder={__("Search")}
placeholder={t("ui.actions.search")}
/>
)}
<Command.List>