import CallingNumberKorean from "./callingNumberKorean.js"; import FloatKorean from "./floatKorean.js"; import IntegerKorean from "./integerKorean.js"; import PhoneNumberKorean from "./phoneNumberKorean.js"; import EmojiDescriptions from "./emoji-descriptions.json" with { type: "json" }; export const IsolatedSymbolMap = { "?": "물음표", "!": "느낌표", "'": "쿼트", '"': "더블쿼트", }; export const SymbolMap = { "%": "퍼센트", $: "달러", "^": "캐럿", "&": "엔드", "*": "스타", "#": "샵", "@": "엣", ".": "쩜", "-": "마이너스", "+": "플러스", _: "언더바", "=": "이퀄", "/": "슬래쉬", "~": "물결표", "\\": "역슬래쉬", "♡": "하트 ", "|": "", ">": "", "<": "", ":": "콜론", ";": "세미콜론", }; export const VersionPostfix = { a: "알파", b: "베타", }; export const LangPrefixes = { typescript: "타입스크립트", javascript: "자바스크립트", java: "자바", kotlin: "코틀린", rust: "러스트", lua: "루아", json: "제이슨", yaml: "야믈", yml: "야믈", toml: "토믈", xml: "엑스엠엘", julia: "줄리아", matlab: "매트랩", erlang: "얼랭", elxir: "엘릭서", zig: "지그", txt: "텍스트", vim: "빔", perl: "펄", php: "피에이치피", lisp: "리스프", postscript: "포스트스크립트", ghostscript: "고스트스크립트", fortran: "포트란", algol: "알골", scala: "스칼라", haskell: "하스켈", basic: "베이직", cpp: "씨플플", "c++": "씨플플", csharp: "씨샵", cs: "씨샵", "c#": "씨샵", c: "씨", h: "헤더", d: "디", awk: "에이더블류케이", pl: "펄", pwsh: "파워쉘", powershell: "파워쉘", cmd: "씨엠디", sh: "쉘", ps1: "파워셀", bat: "배치파일", bash: "베시스크립트", tex: "텍", dart: "다트", go: "고랭", python: "파이썬", swift: "스위프트", css: "씨에스에스", html: "에이치티엠엘", latex: "레이텍", md: "마크다운", markdown: "마크다운", py: "파이썬", hs: "하스켈", rs: "러스트", kt: "코틀린", js: "자스", ts: "타스", tsx: "리액트 타입스크립트", jsx: "리액트 자바스크립트", an: "에이엔", parlance: "팔렌스", }; export const LangPrefixMaxLength = (() => { let max = 0; for (const key in LangPrefixes) { max = Math.max(key.length, max); } return max; })(); export const ChoseongMap = { ㄱ: "기역", ㄴ: "니은", ㄷ: "디귿", ㄹ: "리을", ㅁ: "미음", ㅂ: "비읍", ㅅ: "시옷", ㅇ: "이응", ㅈ: "지읒", ㅊ: "치읓", ㅋ: "키읔", ㅌ: "티읕", ㅍ: "피읖", ㅎ: "히읗", ㄲ: "쌍기역", ㄸ: "쌍디귿", ㅃ: "쌍비읍", ㅆ: "쌍시옷", ㅉ: "쌍지읒", }; export const SIPrefix = { k: "킬로", ki: "키비", m: "메가", mi: "메비", g: "기가", gi: "기비", t: "테라", ti: "테비", p: "페타", pi: "페비", e: "엑사", ei: "엑시", z: "제타", zi: "제비", y: "요타", yi: "요비", }; export const LiterPrefix = { m: "밀리", "": "", }; export const MeterPrefix = { m: "밀리", c: "센치", "": "", k: "킬로", }; export const GIFMap = { "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-windy-hair-gif-19187698": "화난 일레이나", "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-sparkle-amazed-gif-18827847": "일레이나 반짝반짝!", "images-ext-1.discordapp.net/external/C3xPFuUxs16jY25AR3NvsIDezaOtib9wozhLBWejZk4/https/media.tenor.com/bUd8mk4ufwsAAAPo/anime-disappointment.mp4": "일레이나 절래절래", "images-ext-1.discordapp.net/external/SXv4qgpy2r1Gx-dNxhcfJle6AXDaH_SToRjEBYYaup0/https/media.tenor.com/nDDxJc4FDwEAAAPo/cute.mp4": "일레이나 끄덕", "tenor.com/view/majo-no-tabitabi-the-journey-of-elaina-elaina-what-gif-19011602": "당황한 일레이나", "images-ext-1.discordapp.net/external/2R41WcvNJwYMD69UKls2cDa_hEL-rzCRCFvOi2DDOVo/https/media.tenor.com/sU3RCOixDbgAAAPo/majo-no-tabitabi-the-journey-of-elaina.mp4": "일레이나 손짓", }; export const UnicodeSymbols = { "㎢": "제곱킬로미터", "㎡": "제곱미터", "↑": "위쪽 화살표", "↓": "아래쪽 화살표", "←": "왼쪽 화살표", "→": "오른쪽 화살표", "↔": "좌우 화살표", "↖": "왼쪽 위 화살표", "↗": "오른쪽 위 화살표", "↘": "오른쪽 아래 화살표", "↙": "왼쪽 아래 화살표", }; export const UnicodeSymbolsRegex = new RegExp( "[" + Object.keys(UnicodeSymbols).join() + "]", "gu", ); export function processDots(input: string): string { return input .replace(/[.,]+$/, "") .replace(/[.,]{2,}/g, "") .replace(/[.,]\s/g, " "); } export function saferKorean(input: string): string { return ( processDots(input.normalize() + " ") // Process isolated symbols .replace(/^[?!'"]+ $/, (total) => [...total] .map( (element) => IsolatedSymbolMap[element as keyof typeof IsolatedSymbolMap], ) .join(""), ) .replace(/\s\|\|\s/g, " 오얼 ") .replace(/\s&&\s/g, " 엔드 ") // Process codeblock .replace(/```([\s\S]*?)```/g, (_, content: string) => { const code = content.substring(0, LangPrefixMaxLength).toLowerCase(); let lang = ""; for (const [key, value] of Object.entries(LangPrefixes)) { if (code.startsWith(key + "\n")) { lang = value + " "; break; } } return lang + "코드블럭"; }) // Process link .replace(/[hH][tT]{2}[pP][sS]?:\/\/(\S+)/g, (_, url: string) => { const mapped = GIFMap[url as keyof typeof GIFMap] as string | undefined; if (mapped) return mapped; if (url.startsWith("tenor.com/view")) { return "움짤!"; } return "링크"; }) // Process koreans .replace(/[아ㅏ]{3,}/g, "아아아") .replace(/ㄹㅇ/g, (content: string) => { return "리얼".repeat(Math.min(Math.floor(content.length / 2), 2)); }) .replace(/(ㅇㄴ)+/g, (content: string) => { return "아니".repeat(Math.min(Math.floor(content.length / 2), 2)); }) .replace(/(ㅇㅎ)+/g, (content: string) => { return "아하".repeat(Math.min(Math.floor(content.length / 2), 2)); }) .replace(/(ㅇㅋ)+/g, (content: string) => { return "오키".repeat(Math.min(Math.floor(content.length / 2), 2)); }) .replace(/(ㅊㅋ)+/g, (content: string) => { return "추카".repeat(Math.min(Math.floor(content.length / 2), 2)); }) .replace(/ㄱ+/g, (content: string) => { if (content.length == 2) { return "고고"; } else if (content.length == 3) { return "고고고"; } return content; }) .replace(/ㅋ{2,}/g, (content) => "크".repeat(content.length)) .replace(/ㅌ{2,}/g, "틔틔") .replace(/ㄷ{2,}/g, "덜덜") .replace(/ㄴ{2,}/g, "노노") .replace(/ㅇ{2,}/g, "응응") .replace(/ㅊ{2,}/g, "추추") .replace(/ㅠ{2,}/g, "유유") .replace(/ㅜ{2,}/g, "우우") .replace( /[ㄱ-ㅎㄲㄸㅃㅆㅉ]/g, (char: string) => ChoseongMap[char as keyof typeof ChoseongMap], ) // Process number, unit .replace( /(\+\d+[\s-]+)?([\d-]+)/g, (_, prefix: string | undefined, phone: string) => { const all = (prefix ?? "") + phone; if (!phone.includes("-")) return all; return PhoneNumberKorean.convert(all); }, ) .replace( /([\d,]+)([kKMmgGtTpPeEzZyY][iI]?)[bB]/g, (_, num: string, mod: string) => { // 10kib => 십키비바이트 num = IntegerKorean.convertFromString(num); mod = SIPrefix[mod.toLowerCase() as keyof typeof SIPrefix]; return `${num} ${mod}바이트 `; }, ) .replace(/([\d,]+)([m]?)[lL]\s/g, (_, num: string, mod: string) => { // 10l => 십리터 num = IntegerKorean.convertFromString(num); mod = LiterPrefix[mod as keyof typeof LiterPrefix]; return `${num} ${mod}리터 `; }) .replace(/([\d,]+)([mck]?)m\s/g, (_, num: string, mod: string) => { // 10m => 십미터 num = IntegerKorean.convertFromString(num); mod = MeterPrefix[mod as keyof typeof MeterPrefix]; return `${num} ${mod}미터 `; }) .replace( /([\d.]+)\s*([개살시평명])/g, (_, num: string, postfix: string) => { // 10명 => 열명 if (num.includes(".")) { return num + postfix; } const intNum = parseInt(num); if (CallingNumberKorean.canConvert(intNum)) { return CallingNumberKorean.convert(intNum) + postfix; } else { return IntegerKorean.convertFromString(num) + postfix; } }, ) .replace(/[\d,]+/g, (num: string) => { // 1,000 원 => 천원 if (!num.includes(",")) return num; return IntegerKorean.convertFromString(num); }) .replace( /(v?)([\d.]+)([ab]?)/g, (_, suffix: string, num: string, postfix: string) => { const dotCount = [...num.matchAll(/\./g)].length; const hasNoSuffix = suffix == ""; if (hasNoSuffix && dotCount == 0) { // 일반 숫자는 인트로 읽음 return IntegerKorean.convertFromString(num) + postfix; } else if (hasNoSuffix && dotCount == 1) { // 소수는 . 앞은 인트로, 뒤는 플로트로 읽음 const [intPart, floatPart] = num.split(/\./); return ( IntegerKorean.convertFromString(intPart ?? "") + "쩜" + FloatKorean.convert(floatPart ?? "") + postfix ); } else if ((suffix == "v" || postfix.length) && dotCount > 1) { // 버전표기는 버전을 붙여서 return ( "버전" + FloatKorean.convert(num) + (VersionPostfix[postfix as keyof typeof VersionPostfix] ?? "") ); } else { // 모든 경우에 속하지 않으면 영일이삼사 형태로 읽음 // (예: 111.111.111.111 ip address) return FloatKorean.convert(num) + postfix; } }, ) // Process symbol .replace( /[%^&*#@.\-+_=/\\♡$|:;><]/g, (t) => SymbolMap[t as keyof typeof SymbolMap], ) .replace(/([?!]+)/g, (_, content: string): string => content[0] ?? "") .replace(/[ \t\f\r]+/g, " ") // Process emoji .replace( UnicodeSymbolsRegex, (content: string) => UnicodeSymbols[content as keyof typeof UnicodeSymbols] ?? content, ) .replace(/\p{Extended_Pictographic}/gu, (content: string) => { return ( EmojiDescriptions[content as keyof typeof EmojiDescriptions] ?? content ); }) .replace(/\p{Emoji}/u, " 이모지 ") .trim() ); }