Line data Source code
1 : // Copyright 2026 The Authors.
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // https://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : import 'backend.dart';
16 : import 'composite_backend.dart';
17 : import 'guess.dart';
18 : import 'script/script_filter.dart' as script_filter;
19 :
20 : /// Detects the language of a piece of text.
21 : ///
22 : /// Use [LanguageDetector.pureDart] for the zero-dependency default (a
23 : /// Unicode script pre-filter plus a character n-gram model, covering 58
24 : /// languages — see `CompositeBackend`), or supply a custom [backend] for
25 : /// testing or an alternative scoring strategy.
26 : ///
27 : /// Example:
28 : /// ```dart
29 : /// final detector = LanguageDetector.pureDart();
30 : /// switch (detector.detect('Bonjour le monde')) {
31 : /// case Detected(best: final guess):
32 : /// print('${guess.code} (${guess.confidence})'); // fr (...)
33 : /// case Undetermined():
34 : /// print('could not determine a language');
35 : /// }
36 : /// ```
37 : final class LanguageDetector {
38 : /// Creates a detector scoring with [backend].
39 : ///
40 : /// [restrictTo], when non-null, is applied as a post-hoc filter over
41 : /// [backend]'s results: guesses whose code is not in [restrictTo] are
42 : /// dropped before ranking. This works for *any* backend, but does not
43 : /// improve a custom backend's internal accuracy — only [pureDart] threads
44 : /// [restrictTo] into the scoring stage itself, which is what actually
45 : /// sharpens n-gram accuracy.
46 1 : LanguageDetector({
47 : required this.backend,
48 : this.minConfidence = 0.5,
49 : this.restrictTo,
50 : });
51 :
52 : /// The scoring strategy this detector delegates to.
53 : final LanguageDetectorBackend backend;
54 :
55 : /// The minimum confidence (in `[0.0, 1.0]`) a guess must reach for
56 : /// [detect] to return [Detected] rather than [Undetermined].
57 : final double minConfidence;
58 :
59 : /// If non-null, guesses whose code is not in this set are dropped before
60 : /// ranking. See the constructor doc for how this interacts with
61 : /// [pureDart]'s backend-level narrowing.
62 : final Set<String>? restrictTo;
63 :
64 : /// The zero-dependency default: a Unicode script pre-filter plus a
65 : /// character n-gram model, covering 58 languages (see `CompositeBackend`).
66 : ///
67 : /// [restrictTo], when supplied, is threaded into the n-gram stage so only
68 : /// the given languages' profiles are compared — the biggest practical
69 : /// accuracy lever for this detector.
70 1 : factory LanguageDetector.pureDart({
71 : double minConfidence = 0.5,
72 : Set<String>? restrictTo,
73 : }) {
74 1 : return LanguageDetector(
75 1 : backend: CompositeBackend(restrictTo: restrictTo),
76 : minConfidence: minConfidence,
77 : restrictTo: restrictTo,
78 : );
79 : }
80 :
81 : /// Full detection: scores [text] with [backend], applies [restrictTo] (if
82 : /// set) and [minConfidence], and ranks the result by descending
83 : /// confidence.
84 1 : DetectionResult detect(String text) {
85 2 : var guesses = backend.score(text);
86 1 : final allowed = restrictTo;
87 : if (allowed != null) {
88 1 : guesses = [
89 1 : for (final g in guesses)
90 3 : if (allowed.contains(g.code)) g,
91 : ];
92 : }
93 1 : guesses = [...guesses]
94 5 : ..sort((a, b) => b.confidence.compareTo(a.confidence));
95 :
96 2 : if (guesses.isEmpty) return Undetermined(guesses);
97 :
98 1 : final best = guesses.first;
99 3 : if (best.confidence >= minConfidence) {
100 1 : return Detected(best, guesses);
101 : }
102 1 : return Undetermined(guesses);
103 : }
104 :
105 : /// Cheap, script-only classification of [text] — does not run the n-gram
106 : /// stage.
107 : ///
108 : /// Returns a 4-letter ISO 15924 script code (e.g. `"Latn"`, `"Cyrl"`,
109 : /// `"Hani"`) for the most common script among the input's letter
110 : /// codepoints, or `null` if the input has no scripted letters.
111 : ///
112 : /// This always uses the built-in script table — it is not affected by a
113 : /// custom [backend], since script classification is a fixed, deterministic
114 : /// Unicode property lookup, not a pluggable strategy.
115 2 : String? dominantScript(String text) => script_filter.dominantScript(text);
116 : }
|