mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 07:52:10 +01:00
Localizes language names in the language pie chart
Uses `Intl.DisplayNames` to display localized language names in the language pie chart, enhancing user experience and readability. Also converts country and language values from the CSV data to ISO codes for standardization and improved data handling. Adds tooltip to display ISO language code.
This commit is contained in:
@ -1,6 +1,11 @@
|
||||
// Fetches, parses, and returns chat session data for a company from a CSV URL
|
||||
import fetch from "node-fetch";
|
||||
import { parse } from "csv-parse/sync";
|
||||
import ISO6391 from "iso-639-1";
|
||||
import countries from "i18n-iso-countries";
|
||||
|
||||
// Register locales for i18n-iso-countries
|
||||
countries.registerLocale(require("i18n-iso-countries/langs/en.json"));
|
||||
|
||||
// This type is used internally for parsing the CSV records
|
||||
interface CSVRecord {
|
||||
@ -29,8 +34,8 @@ interface SessionData {
|
||||
startTime: Date;
|
||||
endTime: Date | null;
|
||||
ipAddress?: string;
|
||||
country?: string;
|
||||
language?: string | null;
|
||||
country?: string | null; // Will store ISO 3166-1 alpha-2 country code or null/undefined
|
||||
language?: string | null; // Will store ISO 639-1 language code or null/undefined
|
||||
messagesSent: number;
|
||||
sentiment: number | null;
|
||||
escalated: boolean;
|
||||
@ -44,51 +49,136 @@ interface SessionData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes language values to a standard set
|
||||
* @param languageStr The raw language string from CSV
|
||||
* @returns A normalized language string
|
||||
* Converts country names to ISO 3166-1 alpha-2 codes
|
||||
* @param countryStr Raw country string from CSV
|
||||
* @returns ISO 3166-1 alpha-2 country code or null if not found
|
||||
*/
|
||||
function normalizeLanguage(languageStr?: string): string | null {
|
||||
if (!languageStr) return null;
|
||||
function getCountryCode(countryStr?: string): string | null | undefined {
|
||||
if (countryStr === undefined) return undefined;
|
||||
if (countryStr === null || countryStr === "") return null;
|
||||
|
||||
const normalized = languageStr.toLowerCase().trim();
|
||||
// Clean the input
|
||||
const normalized = countryStr.trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
// Map of language variations to standard names
|
||||
const languageMap: Record<string, string> = {
|
||||
// English variations
|
||||
english: "English",
|
||||
en: "English",
|
||||
eng: "English",
|
||||
// Direct ISO code check (if already a 2-letter code)
|
||||
if (normalized.length === 2 && normalized === normalized.toUpperCase()) {
|
||||
return countries.isValid(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
// Dutch variations
|
||||
dutch: "Dutch",
|
||||
nederlands: "Dutch",
|
||||
nl: "Dutch",
|
||||
nederland: "Dutch",
|
||||
netherland: "Dutch",
|
||||
netherlands: "Dutch",
|
||||
hollands: "Dutch",
|
||||
niederländisch: "Dutch",
|
||||
nizozemski: "Dutch",
|
||||
|
||||
// Other languages that might appear
|
||||
bosnian: "Bosnian",
|
||||
bs: "Bosnian",
|
||||
turkish: "Turkish",
|
||||
tr: "Turkish",
|
||||
turks: "Turkish",
|
||||
german: "German",
|
||||
de: "German",
|
||||
duits: "German",
|
||||
french: "French",
|
||||
fr: "French",
|
||||
frans: "French",
|
||||
spanish: "Spanish",
|
||||
es: "Spanish",
|
||||
spaans: "Spanish",
|
||||
// Special case for country codes used in the dataset
|
||||
const countryMapping: Record<string, string> = {
|
||||
BA: "BA", // Bosnia and Herzegovina
|
||||
NL: "NL", // Netherlands
|
||||
USA: "US", // United States
|
||||
UK: "GB", // United Kingdom
|
||||
GB: "GB", // Great Britain
|
||||
Nederland: "NL",
|
||||
Netherlands: "NL",
|
||||
Netherland: "NL",
|
||||
Holland: "NL",
|
||||
Germany: "DE",
|
||||
Deutschland: "DE",
|
||||
Belgium: "BE",
|
||||
België: "BE",
|
||||
Belgique: "BE",
|
||||
France: "FR",
|
||||
Frankreich: "FR",
|
||||
"United States": "US",
|
||||
"United States of America": "US",
|
||||
Bosnia: "BA",
|
||||
"Bosnia and Herzegovina": "BA",
|
||||
"Bosnia & Herzegovina": "BA",
|
||||
};
|
||||
|
||||
return languageMap[normalized] || "Other";
|
||||
// Check mapping
|
||||
if (normalized in countryMapping) {
|
||||
return countryMapping[normalized];
|
||||
}
|
||||
|
||||
// Try to get the code from the country name (in English)
|
||||
try {
|
||||
const code = countries.getAlpha2Code(normalized, "en");
|
||||
if (code) return code;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error converting country name to code: ${normalized}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// If all else fails, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts language names to ISO 639-1 codes
|
||||
* @param languageStr Raw language string from CSV
|
||||
* @returns ISO 639-1 language code or null if not found
|
||||
*/
|
||||
function getLanguageCode(languageStr?: string): string | null | undefined {
|
||||
if (languageStr === undefined) return undefined;
|
||||
if (languageStr === null || languageStr === "") return null;
|
||||
|
||||
// Clean the input
|
||||
const normalized = languageStr.trim();
|
||||
if (!normalized) return null;
|
||||
|
||||
// Direct ISO code check (if already a 2-letter code)
|
||||
if (normalized.length === 2 && normalized === normalized.toLowerCase()) {
|
||||
return ISO6391.validate(normalized) ? normalized : null;
|
||||
}
|
||||
|
||||
// Special case mappings
|
||||
const languageMapping: Record<string, string> = {
|
||||
english: "en",
|
||||
English: "en",
|
||||
dutch: "nl",
|
||||
Dutch: "nl",
|
||||
nederlands: "nl",
|
||||
Nederlands: "nl",
|
||||
nl: "nl",
|
||||
bosnian: "bs",
|
||||
Bosnian: "bs",
|
||||
turkish: "tr",
|
||||
Turkish: "tr",
|
||||
german: "de",
|
||||
German: "de",
|
||||
deutsch: "de",
|
||||
Deutsch: "de",
|
||||
french: "fr",
|
||||
French: "fr",
|
||||
français: "fr",
|
||||
Français: "fr",
|
||||
spanish: "es",
|
||||
Spanish: "es",
|
||||
español: "es",
|
||||
Español: "es",
|
||||
italian: "it",
|
||||
Italian: "it",
|
||||
italiano: "it",
|
||||
Italiano: "it",
|
||||
nizozemski: "nl", // "Dutch" in some Slavic languages
|
||||
};
|
||||
|
||||
// Check mapping
|
||||
if (normalized in languageMapping) {
|
||||
return languageMapping[normalized];
|
||||
}
|
||||
|
||||
// Try to get code using the ISO6391 library
|
||||
try {
|
||||
const code = ISO6391.getCode(normalized);
|
||||
if (code) return code;
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Error converting language name to code: ${normalized}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
// If all else fails, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,7 +193,7 @@ function normalizeCategory(categoryStr?: string): string | null {
|
||||
|
||||
// Define category groups using keywords
|
||||
const categoryMapping: Record<string, string[]> = {
|
||||
"Onboarding": [
|
||||
Onboarding: [
|
||||
"onboarding",
|
||||
"start",
|
||||
"begin",
|
||||
@ -131,7 +221,7 @@ function normalizeCategory(categoryStr?: string): string | null {
|
||||
"gesprek",
|
||||
"talk",
|
||||
],
|
||||
"Greeting": [
|
||||
Greeting: [
|
||||
"greeting",
|
||||
"greet",
|
||||
"hello",
|
||||
@ -199,7 +289,7 @@ function normalizeCategory(categoryStr?: string): string | null {
|
||||
"software",
|
||||
"hardware",
|
||||
],
|
||||
"Offboarding": [
|
||||
Offboarding: [
|
||||
"offboarding",
|
||||
"leave",
|
||||
"exit",
|
||||
@ -343,8 +433,8 @@ export async function fetchAndParseCsv(
|
||||
startTime: safeParseDate(r.start_time) || new Date(), // Fallback to current date if invalid
|
||||
endTime: safeParseDate(r.end_time),
|
||||
ipAddress: r.ip_address,
|
||||
country: r.country,
|
||||
language: normalizeLanguage(r.language),
|
||||
country: getCountryCode(r.country),
|
||||
language: getLanguageCode(r.language),
|
||||
messagesSent: Number(r.messages_sent) || 0,
|
||||
sentiment: mapSentimentToScore(r.sentiment),
|
||||
escalated: isTruthyValue(r.escalated),
|
||||
|
||||
105
lib/localization.ts
Normal file
105
lib/localization.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import ISO6391 from "iso-639-1";
|
||||
import countries from "i18n-iso-countries";
|
||||
|
||||
// Register locales for i18n-iso-countries
|
||||
countries.registerLocale(require("i18n-iso-countries/langs/en.json"));
|
||||
|
||||
/**
|
||||
* Get a human-readable language name from ISO 639-1 code
|
||||
* @param code The ISO 639-1 language code
|
||||
* @returns The language name or the original code if not found
|
||||
*/
|
||||
export function getLanguageName(code: string | null | undefined): string {
|
||||
if (!code) return "Unknown";
|
||||
|
||||
// Handle invalid codes
|
||||
if (code.length !== 2) return code;
|
||||
|
||||
// Try using ISO6391 library
|
||||
try {
|
||||
const name = ISO6391.getName(code);
|
||||
if (name) return name;
|
||||
} catch (e) {
|
||||
console.error(`Error getting language name for code: ${code}`, e);
|
||||
}
|
||||
|
||||
return code; // Return original code as fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a human-readable country name from ISO 3166-1 alpha-2 code
|
||||
* @param code The ISO 3166-1 alpha-2 country code
|
||||
* @returns The country name or the original code if not found
|
||||
*/
|
||||
export function getCountryName(code: string | null | undefined): string {
|
||||
if (!code) return "Unknown";
|
||||
|
||||
// Handle invalid codes
|
||||
if (code.length !== 2) return code;
|
||||
|
||||
// Try using i18n-iso-countries library
|
||||
try {
|
||||
const name = countries.getName(code, "en");
|
||||
if (name) return name;
|
||||
} catch (e) {
|
||||
console.error(`Error getting country name for code: ${code}`, e);
|
||||
}
|
||||
|
||||
return code; // Return original code as fallback
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side function to get localized language name using Intl.DisplayNames
|
||||
* @param code The ISO 639-1 language code
|
||||
* @param locale The locale to use (defaults to browser's locale)
|
||||
* @returns The localized language name
|
||||
*/
|
||||
export function getLocalizedLanguageName(
|
||||
code: string | null | undefined,
|
||||
locale?: string,
|
||||
): string {
|
||||
if (typeof window === "undefined" || !code) return getLanguageName(code);
|
||||
|
||||
try {
|
||||
// Check if Intl.DisplayNames is supported
|
||||
if (typeof Intl !== "undefined" && "DisplayNames" in Intl) {
|
||||
const userLocale = locale || navigator.language || "en";
|
||||
const displayNames = new Intl.DisplayNames([userLocale], {
|
||||
type: "language",
|
||||
});
|
||||
return displayNames.of(code) || getLanguageName(code);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error getting localized language name for code: ${code}`, e);
|
||||
}
|
||||
|
||||
return getLanguageName(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* Client-side function to get localized country name using Intl.DisplayNames
|
||||
* @param code The ISO 3166-1 alpha-2 country code
|
||||
* @param locale The locale to use (defaults to browser's locale)
|
||||
* @returns The localized country name
|
||||
*/
|
||||
export function getLocalizedCountryName(
|
||||
code: string | null | undefined,
|
||||
locale?: string,
|
||||
): string {
|
||||
if (typeof window === "undefined" || !code) return getCountryName(code);
|
||||
|
||||
try {
|
||||
// Check if Intl.DisplayNames is supported
|
||||
if (typeof Intl !== "undefined" && "DisplayNames" in Intl) {
|
||||
const userLocale = locale || navigator.language || "en";
|
||||
const displayNames = new Intl.DisplayNames([userLocale], {
|
||||
type: "region",
|
||||
});
|
||||
return displayNames.of(code) || getCountryName(code);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error getting localized country name for code: ${code}`, e);
|
||||
}
|
||||
|
||||
return getCountryName(code);
|
||||
}
|
||||
@ -42,7 +42,10 @@ export interface ChatSession {
|
||||
userId?: string | null;
|
||||
category?: string | null;
|
||||
language?: string | null;
|
||||
country?: string | null;
|
||||
ipAddress?: string | null;
|
||||
sentiment?: number | null;
|
||||
messagesSent?: number;
|
||||
startTime: Date;
|
||||
endTime?: Date | null;
|
||||
createdAt: Date;
|
||||
@ -55,6 +58,7 @@ export interface ChatSession {
|
||||
tokens?: number;
|
||||
tokensEur?: number;
|
||||
initialMsg?: string;
|
||||
fullTranscriptUrl?: string | null;
|
||||
}
|
||||
|
||||
export interface DayMetrics {
|
||||
|
||||
Reference in New Issue
Block a user