lingua_rs 0.3.0 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e301297292f2de86fd62416f6f06765c7ccd373c81020e2ad365e0c0151a55bf
4
- data.tar.gz: fe961910a3e44fa7df10580100b4aecef6f2552c08a7faba278a84eaf7ae89fc
3
+ metadata.gz: 10f60785757487a1928e8d35dff0a23a5b10143f69cd4c147fe4da13b7362e31
4
+ data.tar.gz: 587f8b61751bee91d89df4fa2c278163cdd48c601536292f56ed35f4807044fd
5
5
  SHA512:
6
- metadata.gz: 31e45f89765b5feb782a8b47f9574891c9ce826b5883f840b5f0d06c8715f26c9d9caa999a8e08349f475e307b2ccd28ecb303677ae8e51caa79920d94cc91fc
7
- data.tar.gz: 6ace3072e3b7e8e044c55712879ea398dc7f009475cb058444070cd22834bb7f4fa6525ab4a66bab95417d57b1f57b9599cda02dd159be80603280c0646b6414
6
+ metadata.gz: '03383e1fa0da8d3131f2e8d724b5818697bcddae9604d33a3cfafe7b73b48d32468f2817eaa5b1606f726b47a326b7f2c48f46ef5b3361a605b4f7a7c3b20cc2'
7
+ data.tar.gz: e347f5ef0485bda583d4df5cfa116699b496184554a2d367615ff3e538e8a8a76769aca20c74a2f67f7fe2caf870d6184d28733e1d040f84d2ca5be8c21ecaf6
data/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # LinguaRs
2
2
 
3
3
  [![CI](https://github.com/kochka/lingua_rs/actions/workflows/ci.yml/badge.svg)](https://github.com/kochka/lingua_rs/actions/workflows/ci.yml)
4
- [![Gem Version](https://badge.fury.io/rb/lingua_rs.svg)](https://badge.fury.io/rb/lingua_rs)
5
- [![Downloads](https://img.shields.io/gem/dt/lingua_rs)](https://rubygems.org/gems/lingua_rs)
6
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
+ [![Gem Version](https://img.shields.io/gem/v/lingua_rs?color=cc342d)](https://rubygems.org/gems/lingua_rs)
5
+ [![Downloads](https://img.shields.io/gem/dt/lingua_rs?color=0969da)](https://rubygems.org/gems/lingua_rs)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-6e7781.svg)](https://opensource.org/licenses/MIT)
7
7
 
8
8
  A Ruby gem wrapping the [Lingua](https://github.com/pemistahl/lingua-rs) Rust library for language detection.
9
9
 
@@ -42,19 +42,18 @@ export PATH=/usr/lib/llvm21/bin:$PATH
42
42
  require 'lingua'
43
43
 
44
44
  lang = Lingua.detect('Bonjour le monde')
45
+ lang.french? # => true
45
46
  lang.to_s # => 'French'
46
47
  lang.to_sym # => :french
47
48
  lang.to_iso # => 'fr'
48
- lang.to_iso6391 # => 'fr'
49
49
  lang.to_iso6393 # => 'fra'
50
- lang.inspect # => '#<Lingua::Language French>'
51
50
 
52
51
  Lingua.detect('') # => nil
53
52
  ```
54
53
 
55
54
  ### Restricting languages
56
55
 
57
- Pass a `languages` option to limit detection to a subset of languages. Values can be full names, ISO 639-1 codes, or ISO 639-3 codes, and can be mixed freely.
56
+ Pass a `languages` option to limit detection to a subset of languages. Values can be full names, ISO 639-1 codes, or ISO 639-3 codes, and can be mixed freely. Restricting the candidate set is also faster, since the detector has fewer languages to compare against.
58
57
 
59
58
  ```ruby
60
59
  # By full name
@@ -154,9 +153,11 @@ Lingua::Language['xxx'] # => nil
154
153
  | `name` | `String` | `'French'` |
155
154
  | `to_s` | `String` | `'French'` (alias for `name`) |
156
155
  | `to_sym` | `Symbol` | `:french` |
156
+ | `iso_code` | `String` | `'fr'` (alias for `to_iso6391`) |
157
157
  | `to_iso` | `String` | `'fr'` (alias for `to_iso6391`) |
158
158
  | `to_iso6391` | `String` | `'fr'` |
159
159
  | `to_iso6393` | `String` | `'fra'` |
160
+ | `french?` | `Boolean` | `true` (works with name, ISO 639-1 or ISO 639-3: `fr?`, `fra?`) |
160
161
  | `inspect` | `String` | `'#<Lingua::Language French>'` |
161
162
  | `==` | `Boolean` | Compare two languages |
162
163
  | `hash` | `Integer` | Hash value (usable as Hash key) |
@@ -194,6 +195,64 @@ Returned by `detect_multiple`.
194
195
  | `to_s` | `String` | `'French (0-22): Parlez-vous français? '` |
195
196
  | `inspect` | `String` | `'#<Lingua::Segment French (0-22) "Parlez-vous français? ">'` |
196
197
 
198
+ ### Error handling
199
+
200
+ `Lingua::UnknownLanguageError` (subclass of `ArgumentError`) is raised when an unrecognized language name or code is passed:
201
+
202
+ ```ruby
203
+ Lingua.detect('Hello', languages: %w[en zzzz])
204
+ # => Lingua::UnknownLanguageError: unknown language: "zzzz"
205
+
206
+ # Can also be rescued as ArgumentError
207
+ begin
208
+ Lingua.detect('Hello', languages: %w[zzzz])
209
+ rescue ArgumentError => e
210
+ puts e.message
211
+ end
212
+ ```
213
+
214
+ ## Optimization: selecting languages
215
+
216
+ By default, all 75 languages are compiled into the native extension (~278 MB). If you only need a subset, set the `LINGUA_LANGUAGES` environment variable before installing to reduce binary size and improve detection speed:
217
+
218
+ ```bash
219
+ LINGUA_LANGUAGES=core bundle install
220
+ ```
221
+
222
+ This compiles only the selected language models (~29 MB for the `core` bundle). You can use individual language names or predefined bundles, and you can mix both in the same build.
223
+
224
+ This also works with `gem install`, for example:
225
+
226
+ ```bash
227
+ LINGUA_LANGUAGES=french,english,german gem install lingua_rs
228
+ ```
229
+
230
+ Available bundles:
231
+
232
+ | Bundle | Languages |
233
+ | --- | --- |
234
+ | `core` | `english`, `french`, `german`, `spanish`, `italian`, `portuguese` |
235
+ | `europe_common` | `core` + `dutch`, `polish`, `russian`, `turkish` |
236
+ | `americas` | `english`, `spanish`, `portuguese`, `french` |
237
+ | `mena` | `arabic`, `turkish`, `persian`, `hebrew` |
238
+ | `south_asia` | `hindi`, `urdu`, `bengali`, `tamil`, `telugu`, `punjabi`, `marathi`, `gujarati` |
239
+ | `east_asia` | `chinese`, `japanese`, `korean`, `vietnamese`, `thai` |
240
+ | `africa_common` | `arabic`, `english`, `french`, `swahili`, `somali`, `yoruba`, `zulu` |
241
+
242
+ Examples:
243
+
244
+ ```bash
245
+ LINGUA_LANGUAGES=core bundle install
246
+ LINGUA_LANGUAGES=europe_common,polish bundle install
247
+ LINGUA_LANGUAGES=east_asia,english bundle install
248
+ ```
249
+
250
+ Language and bundle names must match the Cargo features defined by this gem (lowercase, e.g. `french`, `english`, `german`, `core`, `europe_common`).
251
+
252
+ Supported languages (from [Lingua Rust's official list](https://github.com/pemistahl/lingua-rs?tab=readme-ov-file#3-which-languages-are-supported)):
253
+
254
+ `afrikaans`, `albanian`, `arabic`, `armenian`, `azerbaijani`, `basque`, `belarusian`, `bengali`, `bokmal`, `bosnian`, `bulgarian`, `catalan`, `chinese`, `croatian`, `czech`, `danish`, `dutch`, `english`, `esperanto`, `estonian`, `finnish`, `french`, `ganda`, `georgian`, `german`, `greek`, `gujarati`, `hebrew`, `hindi`, `hungarian`, `icelandic`, `indonesian`, `irish`, `italian`, `japanese`, `kazakh`, `korean`, `latin`, `latvian`, `lithuanian`, `macedonian`, `malay`, `maori`, `marathi`, `mongolian`, `nynorsk`, `persian`, `polish`, `portuguese`, `punjabi`, `romanian`, `russian`, `serbian`, `shona`, `slovak`, `slovene`, `somali`, `sotho`, `spanish`, `swahili`, `swedish`, `tagalog`, `tamil`, `telugu`, `thai`, `tsonga`, `tswana`, `turkish`, `ukrainian`, `urdu`, `vietnamese`, `welsh`, `xhosa`, `yoruba`, `zulu`
255
+
197
256
  ## Development
198
257
 
199
258
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake compile` to build the native extension and `rake test` to run the tests.
@@ -11,4 +11,187 @@ crate-type = ["cdylib"]
11
11
 
12
12
  [dependencies]
13
13
  magnus = { version = "0.8.2" }
14
- lingua = "1.8.0"
14
+ lingua = { version = "1.8.0", default-features = false }
15
+
16
+ [features]
17
+ default = ["full"]
18
+ full = [
19
+ "europe_common",
20
+ "americas",
21
+ "mena",
22
+ "south_asia",
23
+ "east_asia",
24
+ "africa_common",
25
+ "afrikaans",
26
+ "albanian",
27
+ "armenian",
28
+ "azerbaijani",
29
+ "basque",
30
+ "belarusian",
31
+ "bokmal",
32
+ "bosnian",
33
+ "bulgarian",
34
+ "catalan",
35
+ "croatian",
36
+ "czech",
37
+ "danish",
38
+ "esperanto",
39
+ "estonian",
40
+ "finnish",
41
+ "ganda",
42
+ "georgian",
43
+ "greek",
44
+ "hungarian",
45
+ "icelandic",
46
+ "indonesian",
47
+ "irish",
48
+ "kazakh",
49
+ "latin",
50
+ "latvian",
51
+ "lithuanian",
52
+ "macedonian",
53
+ "malay",
54
+ "maori",
55
+ "mongolian",
56
+ "nynorsk",
57
+ "romanian",
58
+ "serbian",
59
+ "shona",
60
+ "slovak",
61
+ "slovene",
62
+ "sotho",
63
+ "swedish",
64
+ "tagalog",
65
+ "tsonga",
66
+ "tswana",
67
+ "welsh",
68
+ "xhosa",
69
+ ]
70
+ core = [
71
+ "english",
72
+ "french",
73
+ "german",
74
+ "spanish",
75
+ "italian",
76
+ "portuguese",
77
+ ]
78
+ europe_common = [
79
+ "core",
80
+ "dutch",
81
+ "polish",
82
+ "russian",
83
+ "turkish",
84
+ ]
85
+ americas = [
86
+ "english",
87
+ "spanish",
88
+ "portuguese",
89
+ "french",
90
+ ]
91
+ mena = [
92
+ "arabic",
93
+ "turkish",
94
+ "persian",
95
+ "hebrew",
96
+ ]
97
+ south_asia = [
98
+ "hindi",
99
+ "urdu",
100
+ "bengali",
101
+ "tamil",
102
+ "telugu",
103
+ "punjabi",
104
+ "marathi",
105
+ "gujarati",
106
+ ]
107
+ east_asia = [
108
+ "chinese",
109
+ "japanese",
110
+ "korean",
111
+ "vietnamese",
112
+ "thai",
113
+ ]
114
+ africa_common = [
115
+ "arabic",
116
+ "english",
117
+ "french",
118
+ "swahili",
119
+ "somali",
120
+ "yoruba",
121
+ "zulu",
122
+ ]
123
+ afrikaans = ["lingua/afrikaans"]
124
+ albanian = ["lingua/albanian"]
125
+ arabic = ["lingua/arabic"]
126
+ armenian = ["lingua/armenian"]
127
+ azerbaijani = ["lingua/azerbaijani"]
128
+ basque = ["lingua/basque"]
129
+ belarusian = ["lingua/belarusian"]
130
+ bengali = ["lingua/bengali"]
131
+ bokmal = ["lingua/bokmal"]
132
+ bosnian = ["lingua/bosnian"]
133
+ bulgarian = ["lingua/bulgarian"]
134
+ catalan = ["lingua/catalan"]
135
+ chinese = ["lingua/chinese"]
136
+ croatian = ["lingua/croatian"]
137
+ czech = ["lingua/czech"]
138
+ danish = ["lingua/danish"]
139
+ dutch = ["lingua/dutch"]
140
+ english = ["lingua/english"]
141
+ esperanto = ["lingua/esperanto"]
142
+ estonian = ["lingua/estonian"]
143
+ finnish = ["lingua/finnish"]
144
+ french = ["lingua/french"]
145
+ ganda = ["lingua/ganda"]
146
+ georgian = ["lingua/georgian"]
147
+ german = ["lingua/german"]
148
+ greek = ["lingua/greek"]
149
+ gujarati = ["lingua/gujarati"]
150
+ hebrew = ["lingua/hebrew"]
151
+ hindi = ["lingua/hindi"]
152
+ hungarian = ["lingua/hungarian"]
153
+ icelandic = ["lingua/icelandic"]
154
+ indonesian = ["lingua/indonesian"]
155
+ irish = ["lingua/irish"]
156
+ italian = ["lingua/italian"]
157
+ japanese = ["lingua/japanese"]
158
+ kazakh = ["lingua/kazakh"]
159
+ korean = ["lingua/korean"]
160
+ latin = ["lingua/latin"]
161
+ latvian = ["lingua/latvian"]
162
+ lithuanian = ["lingua/lithuanian"]
163
+ macedonian = ["lingua/macedonian"]
164
+ malay = ["lingua/malay"]
165
+ maori = ["lingua/maori"]
166
+ marathi = ["lingua/marathi"]
167
+ mongolian = ["lingua/mongolian"]
168
+ nynorsk = ["lingua/nynorsk"]
169
+ persian = ["lingua/persian"]
170
+ polish = ["lingua/polish"]
171
+ portuguese = ["lingua/portuguese"]
172
+ punjabi = ["lingua/punjabi"]
173
+ romanian = ["lingua/romanian"]
174
+ russian = ["lingua/russian"]
175
+ serbian = ["lingua/serbian"]
176
+ shona = ["lingua/shona"]
177
+ slovak = ["lingua/slovak"]
178
+ slovene = ["lingua/slovene"]
179
+ somali = ["lingua/somali"]
180
+ sotho = ["lingua/sotho"]
181
+ spanish = ["lingua/spanish"]
182
+ swahili = ["lingua/swahili"]
183
+ swedish = ["lingua/swedish"]
184
+ tagalog = ["lingua/tagalog"]
185
+ tamil = ["lingua/tamil"]
186
+ telugu = ["lingua/telugu"]
187
+ thai = ["lingua/thai"]
188
+ tsonga = ["lingua/tsonga"]
189
+ tswana = ["lingua/tswana"]
190
+ turkish = ["lingua/turkish"]
191
+ ukrainian = ["lingua/ukrainian"]
192
+ urdu = ["lingua/urdu"]
193
+ vietnamese = ["lingua/vietnamese"]
194
+ welsh = ["lingua/welsh"]
195
+ xhosa = ["lingua/xhosa"]
196
+ yoruba = ["lingua/yoruba"]
197
+ zulu = ["lingua/zulu"]
@@ -3,4 +3,13 @@
3
3
  require 'mkmf'
4
4
  require 'rb_sys/mkmf'
5
5
 
6
- create_rust_makefile('lingua/lingua')
6
+ # Allow users to select specific languages to reduce binary size.
7
+ # Example: LINGUA_LANGUAGES=french,english,german bundle install
8
+ languages = ENV.fetch('LINGUA_LANGUAGES', '').split(',').map { |lang| lang.strip.downcase }
9
+
10
+ create_rust_makefile('lingua/lingua') do |r|
11
+ unless languages.empty?
12
+ r.extra_cargo_args << '--no-default-features'
13
+ r.features = languages
14
+ end
15
+ end
@@ -3,7 +3,7 @@ use magnus::{Error, RArray, RHash, RModule, Ruby, function, method, prelude::*};
3
3
 
4
4
  use crate::confidence_result::ConfidenceResult;
5
5
  use crate::segment::Segment;
6
- use crate::helpers::{fetch_option, parse_language, value_to_string};
6
+ use crate::helpers::{fetch_option, parse_language, unknown_language_error, value_to_string};
7
7
  use crate::language::WrappedLanguage;
8
8
 
9
9
  pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
@@ -25,7 +25,7 @@ pub fn compute_confidence(
25
25
  let language_str = value_to_string(language)?;
26
26
  let lang = parse_language(&language_str).ok_or_else(|| {
27
27
  Error::new(
28
- ruby.exception_arg_error(),
28
+ unknown_language_error(&ruby),
29
29
  format!("unknown language: \"{language_str}\""),
30
30
  )
31
31
  })?;
@@ -116,12 +116,18 @@ pub fn build_detector_from_options(
116
116
  for l in &raw_languages {
117
117
  let lang = parse_language(l).ok_or_else(|| {
118
118
  Error::new(
119
- ruby.exception_arg_error(),
119
+ unknown_language_error(ruby),
120
120
  format!("unknown language: \"{l}\""),
121
121
  )
122
122
  })?;
123
123
  languages.push(lang);
124
124
  }
125
+ if languages.is_empty() {
126
+ return Err(magnus::Error::new(
127
+ ruby.exception_arg_error(),
128
+ "languages must contain at least 1 language",
129
+ ));
130
+ }
125
131
  LanguageDetectorBuilder::from_languages(&languages)
126
132
  } else {
127
133
  LanguageDetectorBuilder::from_all_languages()
@@ -129,6 +135,12 @@ pub fn build_detector_from_options(
129
135
 
130
136
  if let Some(opts) = options {
131
137
  if let Some(dist) = fetch_option::<f64>(ruby, opts, "minimum_relative_distance") {
138
+ if !(0.0..=0.99).contains(&dist) {
139
+ return Err(magnus::Error::new(
140
+ ruby.exception_arg_error(),
141
+ "minimum_relative_distance must be between 0.0 and 0.99",
142
+ ));
143
+ }
132
144
  builder.with_minimum_relative_distance(dist);
133
145
  }
134
146
  if fetch_option::<bool>(ruby, opts, "is_every_language_model_preloaded").unwrap_or(false) {
@@ -1,7 +1,23 @@
1
1
  use std::str::FromStr;
2
+ use std::cell::Cell;
2
3
 
3
4
  use lingua::{IsoCode639_1, IsoCode639_3, Language};
4
- use magnus::{Error, Ruby, Symbol, prelude::*};
5
+ use magnus::{ExceptionClass, Error, RModule, Ruby, Symbol, prelude::*};
6
+
7
+ thread_local! {
8
+ static UNKNOWN_LANGUAGE_ERROR: Cell<Option<ExceptionClass>> = const { Cell::new(None) };
9
+ }
10
+
11
+ pub fn define_errors(module: &RModule) -> Result<(), Error> {
12
+ let ruby = Ruby::get().unwrap();
13
+ let class = module.define_error("UnknownLanguageError", ruby.exception_arg_error())?;
14
+ UNKNOWN_LANGUAGE_ERROR.set(Some(class));
15
+ Ok(())
16
+ }
17
+
18
+ pub fn unknown_language_error(ruby: &Ruby) -> ExceptionClass {
19
+ UNKNOWN_LANGUAGE_ERROR.get().unwrap_or_else(|| ruby.exception_arg_error())
20
+ }
5
21
 
6
22
  pub fn parse_language(input: &str) -> Option<Language> {
7
23
  Language::from_str(input)
@@ -10,6 +10,7 @@ pub fn define(ruby: &Ruby, module: &RModule) -> Result<(), Error> {
10
10
  class.define_method("to_s", method!(WrappedLanguage::name, 0))?;
11
11
  class.define_method("to_iso6391", method!(WrappedLanguage::to_iso6391, 0))?;
12
12
  class.define_method("to_iso", method!(WrappedLanguage::to_iso6391, 0))?;
13
+ class.define_method("iso_code", method!(WrappedLanguage::to_iso6391, 0))?;
13
14
  class.define_method("to_iso6393", method!(WrappedLanguage::to_iso6393, 0))?;
14
15
  class.define_method("to_sym", method!(WrappedLanguage::to_sym, 0))?;
15
16
  class.define_method("inspect", method!(WrappedLanguage::inspect, 0))?;
@@ -45,6 +45,7 @@ fn detect_multiple(ruby: &Ruby, arguments: RArray) -> Result<RArray, Error> {
45
45
  fn init(ruby: &Ruby) -> Result<(), Error> {
46
46
  let module = ruby.define_module("Lingua")?;
47
47
 
48
+ helpers::define_errors(&module)?;
48
49
  language::define(ruby, &module)?;
49
50
  confidence_result::define(ruby, &module)?;
50
51
  segment::define(ruby, &module)?;
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lingua
4
+ # Represents a language returned by Lingua detection and lookup APIs.
5
+ class Language
6
+ private
7
+
8
+ def respond_to_missing?(method_name, include_private = false)
9
+ method_name.end_with?('?') || super
10
+ end
11
+
12
+ def method_missing(method_name, *args)
13
+ if method_name.end_with?('?') && args.empty?
14
+ match = Lingua::Language[method_name.name.delete_suffix('?')]
15
+ !match.nil? && self == match
16
+ else
17
+ super
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lingua
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.2'
5
5
  end
data/lib/lingua.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative 'lingua/version'
4
4
  require_relative 'lingua/lingua'
5
+ require_relative 'lingua/language'
5
6
 
6
7
  module Lingua
7
8
  class Error < StandardError; end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lingua_rs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sébastien Vrillaud
@@ -37,17 +37,16 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: 1.3.0
40
- description: Native Ruby bindings for the Lingua Rust library. Detects languages with
41
- confidence scores, ISO 639-1/639-3 support, and configurable accuracy modes.
40
+ description: Native Ruby bindings for the Lingua Rust library, bringing highly accurate
41
+ language detection for short text and mixed-language text to Ruby, with confidence
42
+ scores and ISO 639-1/639-3 language codes.
42
43
  email:
43
44
  - kochka@gmail.com
44
45
  executables: []
45
46
  extensions:
46
- - ext/lingua/Cargo.toml
47
+ - ext/lingua/extconf.rb
47
48
  extra_rdoc_files: []
48
49
  files:
49
- - Cargo.lock
50
- - Cargo.toml
51
50
  - LICENSE.txt
52
51
  - README.md
53
52
  - Rakefile
@@ -61,6 +60,7 @@ files:
61
60
  - ext/lingua/src/lib.rs
62
61
  - ext/lingua/src/segment.rs
63
62
  - lib/lingua.rb
63
+ - lib/lingua/language.rb
64
64
  - lib/lingua/version.rb
65
65
  - sig/lingua.rbs
66
66
  homepage: https://github.com/kochka/lingua_rs
@@ -87,5 +87,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  requirements: []
88
88
  rubygems_version: 3.6.9
89
89
  specification_version: 4
90
- summary: Fast language detection for Ruby, powered by Lingua (Rust).
90
+ summary: High-accuracy language detection for Ruby, powered by Lingua (Rust).
91
91
  test_files: []