Skip to content

Commit 0c55d88

Browse files
committed
feat: TextRomanizer.analyze
1 parent 92d60f1 commit 0c55d88

4 files changed

Lines changed: 179 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## [next]
2+
3+
- `TextRomanizer.analyze`, which analyzes the input text and provides detailed information about detected languages and romanization results for each segment.
4+
15
## 0.0.2
26

37
- Japanese Kanji support added using [`kuromoji`](https://pub.dev/packages/kuromoji).

lib/romanize.dart

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
library;
22

33
import 'package:romanize/src/romanize_base.dart';
4+
import 'package:romanize/src/romanized_text.dart';
45
import 'package:romanize/src/romanizers/arabic.dart';
56
import 'package:romanize/src/romanizers/chinese.dart';
67
import 'package:romanize/src/romanizers/cyrillic.dart';
@@ -9,6 +10,7 @@ import 'package:romanize/src/romanizers/japanese.dart';
910
import 'package:romanize/src/romanizers/korean.dart';
1011

1112
export 'src/romanize_base.dart';
13+
export 'src/romanized_text.dart';
1214
export 'src/romanizers/arabic.dart';
1315
export 'src/romanizers/chinese.dart';
1416
export 'src/romanizers/cyrillic.dart';
@@ -46,10 +48,9 @@ class TextRomanizer {
4648
///
4749
/// The romanizers are checked in order when auto-detecting the language.
4850
static final Set<Romanizer> romanizers = <Romanizer>{
49-
HangulRomanizer(),
50-
51-
CyrillicRomanizer(),
5251
ArabicRomanizer(),
52+
CyrillicRomanizer(),
53+
HangulRomanizer(),
5354
HebrewRomanizer(),
5455

5556
// Note: Chinese is placed before Japanese. Pure Kanji (e.g., "東京") will
@@ -109,7 +110,10 @@ class TextRomanizer {
109110
return romanizers.where((romanizer) => romanizer.isValid(input)).toSet();
110111
}
111112

112-
static final _separatorPattern = RegExp(r'[\s\p{P}_()]+');
113+
static final _separatorPattern = RegExp(
114+
r'[^\p{L}\p{N}\p{M}]+',
115+
unicode: true,
116+
);
113117

114118
/// Romanizes the input text by processing each word separately.
115119
///
@@ -158,6 +162,61 @@ class TextRomanizer {
158162
);
159163
}
160164

165+
/// Analyzes the input text and returns a list of [RomanizedText] parts.
166+
///
167+
/// This method splits the input by spaces and punctuation, romanizing each
168+
/// word independently while preserving the original structure of the text.
169+
/// Each word is auto-detected and romanized according to its language.
170+
///
171+
/// Example:
172+
/// ```dart
173+
/// // Multi-language text
174+
/// final result = TextRomanizer.analyze('你好 Hello 안녕');
175+
/// print(result);
176+
/// // [
177+
/// // RomanizedText(rawText: '你好', language: 'japanese', romanizedText: 'ni hao'),
178+
/// // RomanizedText(rawText: 'Hello', language: '', romanizedText: 'Hello'),
179+
/// // RomanizedText(rawText: '안녕', language: 'korean', romanizedText: 'annyeong'),
180+
/// // ]
181+
/// ```
182+
///
183+
/// Uses a cache to avoid redundant language detection for repeated words.
184+
/// This improves performance for long texts.
185+
static List<RomanizedText> analyze(String input) {
186+
final parts = <RomanizedText>[];
187+
final wordCache = <String, RomanizedText>{};
188+
189+
input.splitMapJoin(
190+
_separatorPattern,
191+
onMatch: (Match match) {
192+
parts.add(
193+
RomanizedText(
194+
rawText: match[0]!,
195+
language: '',
196+
romanizedText: match[0]!,
197+
),
198+
);
199+
return match[0]!;
200+
},
201+
// Handle the content (words):
202+
onNonMatch: (String word) {
203+
if (word.isEmpty) return '';
204+
final romanizedPart = wordCache.putIfAbsent(word, () {
205+
final romanizer = detectLanguage(word);
206+
return RomanizedText(
207+
rawText: word,
208+
language: romanizer.language,
209+
romanizedText: romanizer.romanize(word),
210+
);
211+
});
212+
parts.add(romanizedPart);
213+
return word;
214+
},
215+
);
216+
217+
return parts;
218+
}
219+
161220
/// Returns a [Romanizer] for the specified language.
162221
///
163222
/// The language name is case-insensitive. For example, 'Korean', 'korean',
@@ -168,7 +227,7 @@ class TextRomanizer {
168227
/// Example:
169228
/// ```dart
170229
/// final romanizer = TextRomanizer.forLanguage('japanese');
171-
/// final result = romanizer.romanize('こんにちは');
230+
/// final result = romanizer.romanize('こんにちは'); // konnichiwa
172231
/// ```
173232
static Romanizer forLanguage(String language) {
174233
if (language.trim().isEmpty) {

lib/src/romanized_text.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// A class representing the romanized text along with its original form and the
2+
/// language used for romanization.
3+
///
4+
/// See also:
5+
///
6+
/// * [TextRomanizer.analyze], which returns a list of [RomanizedText] parts.
7+
class RomanizedText {
8+
/// The original raw text.
9+
final String rawText;
10+
11+
/// The romanizer used to convert the text.
12+
final String language;
13+
14+
/// The romanized text.
15+
final String romanizedText;
16+
17+
const RomanizedText({
18+
required this.rawText,
19+
required this.language,
20+
required this.romanizedText,
21+
});
22+
}

test/romanize_test.dart

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,95 @@ void main() {
383383
});
384384
});
385385

386+
group('analyze', () {
387+
test('should analyze multi-language text correctly', () {
388+
const input = '你好 Hello 안녕';
389+
final result = TextRomanizer.analyze(input);
390+
391+
// Expect: [Word(你好), Sep( ), Word(Hello), Sep( ), Word(안녕)]
392+
expect(result, hasLength(5));
393+
394+
// 1. Chinese/Japanese word
395+
expect(result[0].rawText, equals('你好'));
396+
// Note: Language detection for short strings like '你好' might be 'chinese' or 'japanese'
397+
expect(result[0].language, isIn(['chinese', 'japanese']));
398+
expect(result[0].romanizedText, isNotEmpty);
399+
400+
// 2. Separator (Space)
401+
expect(result[1].rawText, equals(' '));
402+
expect(
403+
result[1].language,
404+
isEmpty,
405+
); // Separators have empty string language
406+
expect(result[1].romanizedText, equals(' '));
407+
408+
// 3. English word (Unsupported/Empty)
409+
expect(result[2].rawText, equals('Hello'));
410+
expect(
411+
result[2].language,
412+
equals('empty'),
413+
); // detectLanguage returns 'empty' for unsupported
414+
expect(result[2].romanizedText, equals('Hello'));
415+
416+
// 4. Separator (Space)
417+
expect(result[3].rawText, equals(' '));
418+
419+
// 5. Korean word
420+
expect(result[4].rawText, equals('안녕'));
421+
expect(result[4].language, equals('korean'));
422+
expect(result[4].romanizedText, isNotEmpty);
423+
});
424+
425+
test('should preserve punctuation and sentence structure', () {
426+
const input = 'Hello, World!';
427+
final result = TextRomanizer.analyze(input);
428+
429+
final reconstructed = result.map((r) => r.rawText).join();
430+
expect(reconstructed, equals(input));
431+
});
432+
433+
test('should handle repeated words consistently (caching)', () {
434+
const input = '안녕 & 안녕';
435+
final result = TextRomanizer.analyze(input);
436+
437+
final firstWord = result.first;
438+
final lastWord = result.last;
439+
440+
expect(firstWord.rawText, equals('안녕'));
441+
expect(lastWord.rawText, equals('안녕'));
442+
443+
// The romanization should be identical
444+
expect(firstWord.romanizedText, equals(lastWord.romanizedText));
445+
expect(firstWord.language, equals(lastWord.language));
446+
});
447+
448+
test('should return empty list for empty input', () {
449+
final result = TextRomanizer.analyze('');
450+
expect(result, isEmpty);
451+
});
452+
453+
test('should treat whitespace-only input as separators', () {
454+
const input = ' ';
455+
final result = TextRomanizer.analyze(input);
456+
457+
expect(result, isNotEmpty);
458+
expect(result.first.rawText, equals(input));
459+
expect(result.first.language, isEmpty); // Separator
460+
});
461+
462+
test('should identify specific languages correctly in a sentence', () {
463+
// Korean + Punctuation + Japanese
464+
const input = '안녕하세요. こんにちは。';
465+
final result = TextRomanizer.analyze(input);
466+
467+
final koreanPart = result.firstWhere((r) => r.rawText == '안녕하세요');
468+
expect(koreanPart.language, equals('korean'));
469+
470+
final japanesePart = result.firstWhere((r) => r.rawText == 'こんにちは');
471+
expect(japanesePart.language, equals('japanese'));
472+
});
473+
});
474+
386475
group('supportedLanguages', () {
387476
test('should return list of supported languages', () {
388477
final languages = TextRomanizer.supportedLanguages;

0 commit comments

Comments
 (0)