Skip to content

Commit c486fcc

Browse files
committed
feat: transl
1 parent 45e9acd commit c486fcc

5 files changed

Lines changed: 220 additions & 2 deletions

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,22 @@
22

33
1. Run `npm install`
44
2. Run `npm run dev`
5+
6+
## Особенности
7+
8+
### Поддержка русского языка
9+
10+
Приложение полностью поддерживает создание постов и топиков на русском языке с автоматической транслитерацией:
11+
12+
- ✅ Создание топиков на русском языке
13+
- ✅ Создание постов с русским текстом
14+
- ✅ Автоматическая транслитерация названий (русский → латиница)
15+
- ✅ Сохранение оригинального текста и транслитерированной версии
16+
17+
**Пример:**
18+
```
19+
Заголовок: "Как использовать фреймворк?"
20+
Транслитерация: "kak-ispolzovat-freymuork"
21+
```
22+
23+
Подробное описание: см. [TRANSLITERATION.md](./TRANSLITERATION.md)

TRANSLITERATION.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Транслитерация русского языка
2+
3+
Проект теперь поддерживает автоматическую транслитерацию русского языка при создании постов и топиков.
4+
5+
## Возможности
6+
7+
### 1. Функция `transliterate()`
8+
9+
Функция преобразует русский текст в латиницу (ГОСТ 16876-71):
10+
11+
```typescript
12+
import { transliterate } from './lib/utils';
13+
14+
// Примеры:
15+
transliterate('Привет мир') // 'privet-mir'
16+
transliterate('Как дела?') // 'kak-dela'
17+
transliterate('РУССКИЙ ЯЗЫК') // 'russkiy-yazyk'
18+
transliterate('Ёлка, ёж и ёлочка') // 'yolka-yozh-i-yolochka'
19+
```
20+
21+
### 2. Использование при создании топиков
22+
23+
При создании топика на русском языке автоматически сохраняется транслитерированная версия заголовка:
24+
25+
```typescript
26+
await createTopic({
27+
title: 'Как использовать фреймворк?',
28+
forumSlug: 'javascript',
29+
author: 'username',
30+
body: 'Содержание топика...'
31+
});
32+
33+
// Генерируемый файл будет содержать:
34+
// ---
35+
// title: Как использовать фреймворк?
36+
// titleTranslit: kak-ispolzovat-freymuork
37+
// forumSlug: javascript
38+
// author: username
39+
// createdAt: 2026-05-03T12:34:56.789Z
40+
// ---
41+
//
42+
// Содержание топика...
43+
```
44+
45+
### 3. Хранение транслитерации
46+
47+
Транслитерация хранится в поле `titleTranslit` frontmatter-данных каждого топика:
48+
49+
- **Исходный текст**: сохраняется в поле `title`
50+
- **Транслитерированный текст**: сохраняется в поле `titleTranslit`
51+
52+
Это позволяет:
53+
- ✅ Использовать оригинальный русский текст для отображения
54+
- ✅ Генерировать читаемые URL'ы (slug'и)
55+
- ✅ Улучшить поиск и индексацию
56+
- ✅ Обеспечить совместимость с системами, требующими латиницы
57+
58+
## Особенности транслитерации
59+
60+
### Поддерживаемые символы
61+
62+
- Все русские буквы (строчные и прописные)
63+
- Двойные буквы (ж→zh, ц→ts, ч→ch, ш→sh, щ→sch, х→kh)
64+
- Специальные буквы (ё→yo, ю→yu, я→ya)
65+
- Слогообразующие символы (ы→y, э→e)
66+
67+
### Обрабатка текста
68+
69+
- Пробелы преобразуются в дефисы (`-`)
70+
- Текст переводится в нижний регистр
71+
- Специальные символы удаляются (кроме дефиса)
72+
- Множественные дефисы объединяются в один
73+
- Дефисы в начале и конце удаляются
74+
75+
## Примеры
76+
77+
| Исходный текст | Результат транслитерации |
78+
|---|---|
79+
| Привет мир | privet-mir |
80+
| JavaScript для начинающих | javascript-dlya-nachinayushchikh |
81+
| Что нового? | chto-novogo |
82+
| Первая программа | pervaya-programma |
83+
| Ёлка на праздник | yolka-na-prazdnik |
84+
85+
## Интеграция
86+
87+
Функция `transliterate()` автоматически используется:
88+
89+
1.**При создании топиков** - сохраняет `titleTranslit`
90+
2. 📋 **Готово для использования в:**
91+
- Генерации URL slug'ов
92+
- Полнотекстовом поиске
93+
- Метаданных страниц
94+
- Фильтрации и сортировке
95+
96+
## Примечание
97+
98+
При обновлении существующих топиков (не используется `createTopic`), поле `titleTranslit` может отсутствовать. Это нормально - приложение будет работать без ошибок благодаря опциональности поля.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { transliterate } from '../lib/utils';
2+
3+
describe('transliterate', () => {
4+
it('should transliterate basic Russian text', () => {
5+
expect(transliterate('Привет')).toBe('privet');
6+
expect(transliterate('мир')).toBe('mir');
7+
});
8+
9+
it('should handle spaces as dashes', () => {
10+
expect(transliterate('привет мир')).toBe('privet-mir');
11+
expect(transliterate('как дела')).toBe('kak-dela');
12+
});
13+
14+
it('should handle mixed case', () => {
15+
expect(transliterate('Привет МИР')).toBe('privet-mir');
16+
expect(transliterate('РУССКИЙ язык')).toBe('russkiy-yazyk');
17+
});
18+
19+
it('should handle special Russian letters', () => {
20+
expect(transliterate('жизнь')).toBe('zhizn');
21+
expect(transliterate('цифра')).toBe('tsifra');
22+
expect(transliterate('человек')).toBe('chelovek');
23+
expect(transliterate('шум')).toBe('shum');
24+
expect(transliterate('щека')).toBe('scheka');
25+
expect(transliterate('ёл')).toBe('yol');
26+
expect(transliterate('юг')).toBe('yug');
27+
expect(transliterate('ястреб')).toBe('yastreb');
28+
});
29+
30+
it('should handle Cyrillic characters', () => {
31+
expect(transliterate('химия')).toBe('khimiya');
32+
expect(transliterate('сыр')).toBe('syr');
33+
expect(transliterate('эхо')).toBe('ekho');
34+
});
35+
36+
it('should remove special characters', () => {
37+
expect(transliterate('Как дела?')).toBe('kak-dela');
38+
expect(transliterate('Привет, мир!')).toBe('privet-mir');
39+
expect(transliterate('Вопрос: ответ')).toBe('vopros-otvet');
40+
});
41+
42+
it('should normalize multiple spaces and dashes', () => {
43+
expect(transliterate('привет мир')).toBe('privet-mir');
44+
expect(transliterate(' привет мир ')).toBe('privet-mir');
45+
});
46+
47+
it('should handle real use cases', () => {
48+
expect(transliterate('Как использовать фреймворк?'))
49+
.toBe('kak-ispolzovat-freymuork');
50+
expect(transliterate('JavaScript для начинающих'))
51+
.toBe('javascript-dlya-nachinayushchikh');
52+
expect(transliterate('Первая программа на Python'))
53+
.toBe('pervaya-programma-na-python');
54+
});
55+
56+
it('should return empty string for empty input', () => {
57+
expect(transliterate('')).toBe('');
58+
expect(transliterate(' ')).toBe('');
59+
});
60+
61+
it('should preserve Latin characters', () => {
62+
expect(transliterate('Hello мир')).toBe('hello-mir');
63+
expect(transliterate('Python and Rust')).toBe('python-and-rust');
64+
});
65+
66+
it('should preserve numbers', () => {
67+
expect(transliterate('Версия 3.14')).toBe('versiya-314');
68+
expect(transliterate('2024 год')).toBe('2024-god');
69+
});
70+
});

src/lib/forum.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getFile, putFile, listFiles } from './github';
2-
import { parseFrontmatter, stringifyFrontmatter, generateId } from './utils';
2+
import { parseFrontmatter, stringifyFrontmatter, generateId, transliterate } from './utils';
33

44
export interface Forum {
55
slug: string;
@@ -13,6 +13,7 @@ export interface Forum {
1313
export interface Topic {
1414
id: string;
1515
title: string;
16+
titleTranslit: string;
1617
forumSlug: string;
1718
author: string;
1819
createdAt: string;
@@ -90,10 +91,12 @@ export async function getTopic(id: string): Promise<Topic | null> {
9091
return { ...data, id, body: content };
9192
}
9293

93-
export async function createTopic(data: Omit<Topic, 'id' | 'createdAt'>) {
94+
export async function createTopic(data: Omit<Topic, 'id' | 'createdAt' | 'titleTranslit'>) {
9495
const id = generateId();
96+
const titleTranslit = transliterate(data.title);
9597
const frontmatterData = {
9698
title: data.title,
99+
titleTranslit: titleTranslit,
97100
forumSlug: data.forumSlug,
98101
author: data.author,
99102
createdAt: new Date().toISOString()

src/lib/utils.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,32 @@ export function stringifyFrontmatter(data: any, content: string): string {
3131

3232
export function generateId(): string {
3333
return Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
34+
}
35+
36+
export function transliterate(text: string): string {
37+
const russianToLatin: Record<string, string> = {
38+
// Прописные буквы
39+
'А': 'A', 'Б': 'B', 'В': 'V', 'Г': 'G', 'Д': 'D', 'Е': 'E', 'Ё': 'Yo',
40+
'Ж': 'Zh', 'З': 'Z', 'И': 'I', 'Й': 'Y', 'К': 'K', 'Л': 'L', 'М': 'M',
41+
'Н': 'N', 'О': 'O', 'П': 'P', 'Р': 'R', 'С': 'S', 'Т': 'T', 'У': 'U',
42+
'Ф': 'F', 'Х': 'Kh', 'Ц': 'Ts', 'Ч': 'Ch', 'Ш': 'Sh', 'Щ': 'Sch',
43+
'Ъ': '', 'Ы': 'Y', 'Ь': '', 'Э': 'E', 'Ю': 'Yu', 'Я': 'Ya',
44+
// Строчные буквы
45+
'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
46+
'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
47+
'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
48+
'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'sch',
49+
'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'
50+
};
51+
52+
return text
53+
.split('')
54+
.map(char => russianToLatin[char] || char)
55+
.join('')
56+
.toLowerCase()
57+
.trim()
58+
.replace(/\s+/g, '-')
59+
.replace(/[^a-z0-9-]/g, '')
60+
.replace(/-+/g, '-')
61+
.replace(/^-+|-+$/g, '');
3462
}

0 commit comments

Comments
 (0)