2025-11-07 17:02:06 +03:00
|
|
|
import { t } from "i18next";
|
|
|
|
|
|
2024-04-22 17:20:23 +03:00
|
|
|
export const capitalizeFirstLetter = (text: string): string => {
|
|
|
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
|
|
|
|
};
|
2024-10-02 01:05:16 +03:00
|
|
|
|
|
|
|
|
export const capitalizeAll = (text: string): string => {
|
|
|
|
|
return text
|
2024-11-05 18:29:07 +03:00
|
|
|
.replaceAll("_", " ")
|
2024-10-02 01:05:16 +03:00
|
|
|
.split(" ")
|
|
|
|
|
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
|
|
|
.join(" ");
|
|
|
|
|
};
|
2025-10-27 22:58:31 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Generates a fixed-length hash from a camera name for use as a valid camera identifier.
|
|
|
|
|
* Works safely with Unicode input while outputting Latin-only identifiers.
|
|
|
|
|
*
|
|
|
|
|
* @param name - The original camera name/display name
|
|
|
|
|
* @param prefix - Optional prefix for the identifier
|
|
|
|
|
* @returns A valid camera identifier (lowercase, alphanumeric, max 8 chars)
|
|
|
|
|
*/
|
|
|
|
|
export function generateFixedHash(name: string, prefix: string = "id"): string {
|
2025-11-08 15:44:30 +03:00
|
|
|
// Use the full UTF-8 bytes of the name and compute an FNV-1a 32-bit hash.
|
|
|
|
|
// This is deterministic, fast, works with Unicode and avoids collisions from
|
|
|
|
|
// simple truncation of base64 output.
|
2025-10-27 22:58:31 +03:00
|
|
|
const utf8Bytes = new TextEncoder().encode(name);
|
|
|
|
|
|
2025-11-08 15:44:30 +03:00
|
|
|
// FNV-1a 32-bit hash algorithm
|
|
|
|
|
let hash = 0x811c9dc5; // FNV offset basis
|
|
|
|
|
for (let i = 0; i < utf8Bytes.length; i++) {
|
|
|
|
|
hash ^= utf8Bytes[i];
|
|
|
|
|
// Multiply by FNV prime (0x01000193) with 32-bit overflow
|
|
|
|
|
hash = (hash >>> 0) * 0x01000193;
|
|
|
|
|
// Ensure 32-bit unsigned integer
|
|
|
|
|
hash >>>= 0;
|
2025-10-27 22:58:31 +03:00
|
|
|
}
|
|
|
|
|
|
2025-11-08 15:44:30 +03:00
|
|
|
// Convert to an 8-character lowercase hex string
|
|
|
|
|
const hashHex = (hash >>> 0).toString(16).padStart(8, "0").toLowerCase();
|
2025-10-27 22:58:31 +03:00
|
|
|
|
2025-11-08 15:44:30 +03:00
|
|
|
// Ensure the first character is a letter to avoid an identifier that's purely
|
|
|
|
|
// numeric (isValidId forbids all-digit IDs). If it starts with a digit,
|
|
|
|
|
// replace with 'a'. This is extremely unlikely but a simple safeguard.
|
|
|
|
|
const safeHash = /^[0-9]/.test(hashHex[0]) ? `a${hashHex.slice(1)}` : hashHex;
|
|
|
|
|
|
|
|
|
|
return `${prefix}_${safeHash}`;
|
2025-10-27 22:58:31 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Checks if a string is a valid camera name identifier.
|
|
|
|
|
* Valid camera names contain only ASCII letters, numbers, underscores, and hyphens.
|
|
|
|
|
*
|
|
|
|
|
* @param name - The camera name to validate
|
|
|
|
|
* @returns True if the name is valid, false otherwise
|
|
|
|
|
*/
|
|
|
|
|
export function isValidId(name: string): boolean {
|
2025-11-04 17:57:47 +03:00
|
|
|
return /^[a-zA-Z0-9_-]+$/.test(name) && !/^\d+$/.test(name);
|
2025-10-27 22:58:31 +03:00
|
|
|
}
|
2025-11-07 17:02:06 +03:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Formats a list of strings into a human-readable format with proper localization.
|
|
|
|
|
* Handles different cases for empty, single-item, two-item, and multi-item lists.
|
|
|
|
|
*
|
|
|
|
|
* @param item - The array of strings to format
|
|
|
|
|
* @returns A formatted string representation of the list
|
|
|
|
|
*/
|
|
|
|
|
export function formatList(item: string[]): string {
|
|
|
|
|
if (item.length === 0) return "";
|
|
|
|
|
if (item.length === 1) return item[0];
|
|
|
|
|
if (item.length === 2) {
|
|
|
|
|
return t("list.two", {
|
|
|
|
|
0: item[0],
|
|
|
|
|
1: item[1],
|
|
|
|
|
ns: "common",
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const separatorWithSpace = t("list.separatorWithSpace", { ns: "common" });
|
|
|
|
|
const allButLast = item.slice(0, -1).join(separatorWithSpace);
|
|
|
|
|
return t("list.many", {
|
|
|
|
|
items: allButLast,
|
|
|
|
|
last: item[item.length - 1],
|
|
|
|
|
});
|
|
|
|
|
}
|