Насчёт чего у меня зачесались руки - что нужно разделить формулировку правил и транслитерацию текста. Потому что тут как бы два «специалиста» работают. «Филологу» интересно разрабатывать набор правил, а как они применяются к строке - неважно, технический вопрос. «Программисту», наоборот, конкретные правила не очень интересны. Loose coupling означает, что «программист» и «филолог» стараются минимально вмешиваться в работу друг друга. Они договорились, что набор правил будет объектом, определяющим правила преобразования конкретных литер. Ради гибкости такое правило может формулироваться в виде функции, преобразующей контекст. Использование функций в качестве входных данных обычно выводит гибкость на новый уровень.
Каждое нетривиальное правило реализовано в виде функции. Это всегда полезно, когда сущности предметной области соответствует сущность программы. Например, если есть проблема с транслитерацией буквы «о», мы сразу понимаем, что нужно дебажить, и не боимся, что фикс конкретного правила повлечёт неожиданные регресии. В качестве бонуса - можно одновременно экспериментировать с разными транслитами.
Такой подход, когда логика формулируется в виде некоего объекта, скармливаемого «движку», иногда называется data-driven programming.
Приведённая реализация не очень удобна для ди- и более графов. Для украинского это не очень важно, потому что нам нужно обработать только один диграф - ьо. Если её предполагается использовать для ситуаций, когда диграфов много, можно допилить «движок»: например, так, чтобы ключами объекта-преобразования допускались бы не только однолитерные строки.
/*
* В задаче есть две подзадачи.
* 1. Формулировка правил транслитерации.
* 2. Применение правил к транслитерации текста.
* Эти задачи достаточно независимы, и мы постараемся избегать tight coupling.
*
* Правило транслитерации может быть простым, когда транслитерируемой литере
* соответствует строка транслита, или специальным, когда литера
* транслитерируется в зависимости от контекста. Мы будем моделировать набор
* правил объектом, в котором однолитерным строкам ставятся в соответствие или
* строки, или функции, преобразующие контекст в строки. Под «контекстом» будем
* понимать тройку (currentIndex, text, currentChar). Такое представление
* правил не очень удобно для диграфов, но работает для данной задачи.
*/
/*
* Сначала напишем "движок", преобразующий набор правил в
* функцию-транслитератор текста.
*/
function createTranslit(rules) {
return text => {
const result = [];
const textLength = text.length;
for (let index = 0; index < textLength; ++index) {
const currentChar = text[index];
const translation = rules[currentChar];
if (typeof translation === 'string') {
// простое правило транслитерации
result.push(translation);
} else if (typeof translation === 'function') {
// специальное правило транслитерации: функция, преобразующая контекс
result.push(translation(index, text, currentChar));
} else {
// если правило не определено для литеры, литера не транслитерируется
result.push(currentChar);
}
}
return result.join('');
}
}
// Правила и транслитератор для украинской письменности
const LOWERCASE_CONSONANTS = 'бвгґджзйклмнпрстфхцчшщ';
const CONSONANTS = new Set(LOWERCASE_CONSONANTS + LOWERCASE_CONSONANTS.toUpperCase());
function isUaConsonant(ch) {
return CONSONANTS.has(ch);
}
// Специальное правило для йотированных букв.
function jotatedTranslation(jotated, softening) {
return (index, text) => {
const prevChar = text[index - 1];
// После согласной - смягчающий вариант, иначе - йотированный
return isUaConsonant(prevChar) ? softening : jotated;
}
}
// Транслитерация мягкого знака: «ьо» преобразуется в смягчающее «о», иначе
// возвращается softSignChar
function softSignTranslation(softSignChar, softeningOChar) {
return (index, text) => {
const nextChar = text[index + 1];
if (nextChar === 'о') return softeningOChar;
return softSignChar;
}
}
// Транслитерация «о»: преобразуется в oChar за исключением позиции после
// мягкого знака. В последнем случае буква является частью диграфа и не
// добавляется к транслитерируемому тексту
function oTranslation(oChar) {
return (index, text) => {
const prevChar = text[index - 1];
return prevChar === 'ь' ? '' : oChar;
}
}
const translitUa = createTranslit({
'я': jotatedTranslation('ja', 'ǎ'),
'Я': 'Ja',
'є': jotatedTranslation('je', 'ě'),
'Є': 'Je',
'ю': jotatedTranslation('ju', 'ǔ'),
'Ю': 'Ju',
'ь': softSignTranslation('í', 'ǒ'),
'Ь': 'Í',
'о': oTranslation('o'),
'О': 'O',
"'": '',
'’': '',
'а':'a',
'б':'b',
'в':'v',
'г':'h',
'ґ':'g',
'д':'d',
'е':'e',
'ж':'ž',
'з':'z',
'и':'y',
'і':'i',
'й':'j',
'к':'k',
'л':'l',
'м':'m',
'н':'n',
'п':'p',
'р':'r',
'с':'s',
'т':'t',
'у':'u',
'ф':'f',
'х':'x',
'ц':'c',
'ч':'č',
'ш':'š',
'щ':'ś',
'А':'A',
'Б':'B',
'В':'V',
'Г':'H',
'Ґ':'G',
'Д':'D',
'Е':'E',
'Ж':'Ž',
'З':'Z',
'И':'Y',
'І':'I',
'Й':'J',
'К':'K',
'Л':'L',
'М':'M',
'Н':'N',
'О':'O',
'П':'P',
'Р':'R',
'С':'S',
'Т':'T',
'У':'U',
'Ф':'F',
'Х':'X',
'Ц':'C',
'Ч':'Č',
'Ш':'Š',
'Щ':'Ś',
});
// translitUa('Сучасні USB на ноутбуках стали більш енергоощадливі, через що бувають казуси, як-от проблема зі зовнішнім CD-приводом у мене. Вирішення дійсно одне: додаткове живлення, якщо конструкція передбачує.')
// "Sučasni USB na noutbukax staly bilíš enerhoośadlyvi, čerez śo buvajutí kazusy, jak-ot problema zi zovnišnim CD-pryvodom u mene. Vyrišennǎ dijsno odne: dodatkove žyvlennǎ, jakśo konstrukcija peredbačuje.
// translitUa("льод м'яч")
// "lǒd mjač"