Add react i18next to console
Signed-off-by: Jonathan <contact@grafikart.fr>
This commit is contained in:
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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),
|
||||
}));
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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]),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
82
packages/i18n/date.ts
Normal 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
96
packages/i18n/duration.ts
Normal 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;
|
||||
}
|
||||
50
packages/i18n/index.test.ts
Normal file
50
packages/i18n/index.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]}`)}`;
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user