Repository: zaemyung/sentsplit (language: python)

Issue:
Empty list for the last trailing new line
```python
from sentsplit.segment import SentSplit
sentsplit = SentSplit('en')

input_string = "string\n"
sentsplit.segment(input_string)
# ['string\n', []]
```


Discussion / hints:

Fix patch:
diff --git a/README.md b/README.md
index 6b45746..fa46579 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@ All in all, the library aims to benefit from the best of both worlds: data-drive
 You can try out the library [here](https://share.streamlit.io/zaemyung/sentsplit/main).
 
 ## Installation
-Supports Python 3.6+
+Supports Python 3.7+
 
 ```bash
 # stable
@@ -52,7 +52,7 @@ sentences = sent_splitter.segment([lines])
 ## Features
 The behavior of segmentation can be adjusted by the following arguments:
 - `mincut`: a line is not segmented if its character-level length is smaller than `mincut`, preventing too short sentences.
-- `maxcut`: a line is segmented if its character-level length is greater or equal to `maxcut`, preventing too long sentences.
+- `maxcut`: a line is ["heuristically"](https://github.com/zaemyung/sentsplit/blob/cce34e1ed372b6a79c739f42334c775581fc0de8/sentsplit/segment.py#L271) segmented if its character-level length is greater or equal to `maxcut`, preventing too long sentences.
 - `strip_spaces`: trim any white spaces in front and end of a sentence; does not guarantee exact reconstruction of original passages.
 - `handle_multiple_spaces`: substitute multiple spaces with a single space, perform segmentation, and recover the original spaces.
 - `segment_regexes`: segment at either `start` or `end` index of the matched group defined by the regex patterns.
diff --git a/pyproject.toml b/pyproject.toml
index 374b58c..8388756 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,3 +4,10 @@ requires = [
     "wheel"
 ]
 build-backend = "setuptools.build_meta"
+
+[tool.black]
+line-length = 120
+
+[tool.isort]
+profile = "black"
+line_length = 120
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index fb930e0..0848ef4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,3 +2,4 @@ python-crfsuite>=0.9.7
 tqdm>=4.59.0
 loguru>=0.5.3
 regex>=2021.10.23
+typing-extensions
\ No newline at end of file
diff --git a/sentsplit/__init__.py b/sentsplit/__init__.py
index ad94235..017a99f 100644
--- a/sentsplit/__init__.py
+++ b/sentsplit/__init__.py
@@ -8,60 +8,145 @@
 
 
 def str_to_bool(v):
-  return str(v).lower() in ('yes', 'true', 't', '1')
+    return str(v).lower() in ("yes", "true", "t", "1")
 
 
 def main(*args: str) -> None:
     parser = argparse.ArgumentParser(description=sentsplit.meta_data.description)
-    parser.add_argument('-v', '--version', action='version', version=f'%(prog)s {sentsplit.meta_data.version}')
+    parser.add_argument(
+        "-v",
+        "--version",
+        action="version",
+        version=f"%(prog)s {sentsplit.meta_data.version}",
+    )
     parser.set_defaults(func=lambda x: parser.print_usage())
     subparsers = parser.add_subparsers()
 
     # train a new CRF model
-    subparser_train = subparsers.add_parser('train', help='train a new CRF model')
-    subparser_train.add_argument('-l', '--lang', required=True, help='ISO language code, e.g. "ko" for Korean, "en" for English')
-    subparser_train.add_argument('-c', '--corpus', required=True, help='path to input corpus file, where one line is one sentence')
-    subparser_train.add_argument('-n', '--ngram', type=int, default=5, help='maximum ngram features for CRF model')
-    subparser_train.add_argument('-o', '--output', type=str, help='path to output CRF model')
-    subparser_train.add_argument('--sample_min_length', type=int, default=450, help='minimum number of characters of an input sample for CRF model')
-    subparser_train.add_argument('--crf_max_iteration', type=int, default=50, help='maximum number of CRF iteration for training')
-    subparser_train.add_argument('--depunctuation_ratio', type=float, default=0.0,
-                                 help='ratio of training samples with no punctuation; only appropriate for certain languages '\
-                                      '(e.g. "ko", "ja") that have specific endings for sentences; '\
-                                      'top-`num_depunctuation_endings` most common endings are computed from `corpus`; '\
-                                      '1.0 means 100%% of the samples are depunctuated')
-    subparser_train.add_argument('--num_depunctuation_endings', type=int, default=100, help='number of sentence endings to extract and use')
-    subparser_train.add_argument('--ending_length', type=int, default=3, help='length of a sentence ending counted from reverse, excluding any punctuation')
-    subparser_train.add_argument('--despace_ratio', type=float, default=0.0,
-                                 help='ratio of training samples without whitespaces inbetween the sentences; '\
-                                      '1.0 means 100%% of the samples are despaced')
+    subparser_train = subparsers.add_parser("train", help="train a new CRF model")
+    subparser_train.add_argument(
+        "-l",
+        "--lang",
+        required=True,
+        help='ISO language code, e.g. "ko" for Korean, "en" for English',
+    )
+    subparser_train.add_argument(
+        "-c",
+        "--corpus",
+        required=True,
+        help="path to input corpus file, where one line is one sentence",
+    )
+    subparser_train.add_argument(
+        "-n",
+        "--ngram",
+        type=int,
+        default=5,
+        help="maximum ngram features for CRF model",
+    )
+    subparser_train.add_argument("-o", "--output", type=str, help="path to output CRF model")
+    subparser_train.add_argument(
+        "--sample_min_length",
+        type=int,
+        default=450,
+        help="minimum number of characters of an input sample for CRF model",
+    )
+    subparser_train.add_argument(
+        "--crf_max_iteration",
+        type=int,
+        default=50,
+        help="maximum number of CRF iteration for training",
+    )
+    subparser_train.add_argument(
+        "--depunctuation_ratio",
+        type=float,
+        default=0.0,
+        help="ratio of training samples with no punctuation; only appropriate for certain languages "
+        '(e.g. "ko", "ja") that have specific endings for sentences; '
+        "top-`num_depunctuation_endings` most common endings are computed from `corpus`; "
+        "1.0 means 100%% of the samples are depunctuated",
+    )
+    subparser_train.add_argument(
+        "--num_depunctuation_endings",
+        type=int,
+        default=100,
+        help="number of sentence endings to extract and use",
+    )
+    subparser_train.add_argument(
+        "--ending_length",
+        type=int,
+        default=3,
+        help="length of a sentence ending counted from reverse, excluding any punctuation",
+    )
+    subparser_train.add_argument(
+        "--despace_ratio",
+        type=float,
+        default=0.0,
+        help="ratio of training samples without whitespaces inbetween the sentences; "
+        "1.0 means 100%% of the samples are despaced",
+    )
     subparser_train.set_defaults(func=sentsplit.cli.sentsplit_train)
 
     # segment a given text file
-    subparser_segment = subparsers.add_parser('segment', aliases=['split'],
-                                              help='segment an input file into sentences;'\
-                                                   'optional arguments can override the default values defined in the `config.py`')
-    subparser_segment.add_argument('-l', '--lang', required=True, help='ISO language code, e.g. "ko", "en"')
-    subparser_segment.add_argument('-i', '--input', required=True, help='path to input file to segment')
-    subparser_segment.add_argument('-o', '--output', help='path to output file, default is `{input}.split`')
-    subparser_segment.add_argument('-m', '--model', help='path to the CRF model')
-    subparser_segment.add_argument('--ngram', type=int, help='maximum ngram for both training and segmentation')
-    subparser_segment.add_argument('--mincut', type=int, help='does not segment a line if its character length is shorter than the mincut')
-    subparser_segment.add_argument('--maxcut', type=int, help='segment a line if its character length exceeds the maxcut')
-    subparser_segment.add_argument('--strip_spaces', type=str_to_bool, default=False,
-                                   help='trim (if any) whitespaces of segmented sentences at the head and tail')
-    subparser_segment.add_argument('--handle_multiple_spaces', type=str_to_bool, default=True,
-                                   help='substitute multiple spaces with a single space, segment, and recover the original spaces')
-    subparser_segment.add_argument('--prevent_word_split', type=str_to_bool, default=True,
-                                   help='prevent from splitting a word where word boundary is denoted by white spaces; '\
-                                        'ignored if the splitting position is at a punctuation')
-    subparser_segment.add_argument('--segment_regexes', nargs='?', type=json.loads,
-                                   help='segment at either start or end indices of the matched regex patterns defined by `segment_regexes` in JSON;'\
-                                        'e.g. \'[{"name": "custom", "regex": "\\\\w+봐유~+", "at": "end"}]\'')
-    subparser_segment.add_argument('--prevent_regexes', nargs='?', type=json.loads,
-                                   help='do not segment at characters that are matched by `prevent_regexes` regex(es) in JSON;'\
-                                        'e.g. \'[{"name": "custom", "regex": "\\\\.["\\\\\'”] "}]\'')
-    subparser_segment.add_argument('--cores', type=int, default=1, help='number of CPU cores to use; default is 1 (single core)')
+    subparser_segment = subparsers.add_parser(
+        "segment",
+        aliases=["split"],
+        help="segment an input file into sentences;"
+        "optional arguments can override the default values defined in the `config.py`",
+    )
+    subparser_segment.add_argument("-l", "--lang", required=True, help='ISO language code, e.g. "ko", "en"')
+    subparser_segment.add_argument("-i", "--input", required=True, help="path to input file to segment")
+    subparser_segment.add_argument("-o", "--output", help="path to output file, default is `{input}.split`")
+    subparser_segment.add_argument("-m", "--model", help="path to the CRF model")
+    subparser_segment.add_argument("--ngram", type=int, help="maximum ngram for both training and segmentation")
+    subparser_segment.add_argument(
+        "--mincut",
+        type=int,
+        help="does not segment a line if its character length is shorter than the mincut",
+    )
+    subparser_segment.add_argument(
+        "--maxcut",
+        type=int,
+        help="segment a line if its character length exceeds the maxcut",
+    )
+    subparser_segment.add_argument(
+        "--strip_spaces",
+        type=str_to_bool,
+        default=False,
+        help="trim (if any) whitespaces of segmented sentences at the head and tail",
+    )
+    subparser_segment.add_argument(
+        "--handle_multiple_spaces",
+        type=str_to_bool,
+        default=True,
+        help="substitute multiple spaces with a single space, segment, and recover the original spaces",
+    )
+    subparser_segment.add_argument(
+        "--prevent_word_split",
+        type=str_to_bool,
+        default=True,
+        help="prevent from splitting a word where word boundary is denoted by white spaces; "
+        "ignored if the splitting position is at a punctuation",
+    )
+    subparser_segment.add_argument(
+        "--segment_regexes",
+        nargs="?",
+        type=json.loads,
+        help="segment at either start or end indices of the matched regex patterns defined by `segment_regexes` in JSON;"
+        'e.g. \'[{"name": "custom", "regex": "\\\\w+봐유~+", "at": "end"}]\'',
+    )
+    subparser_segment.add_argument(
+        "--prevent_regexes",
+        nargs="?",
+        type=json.loads,
+        help="do not segment at characters that are matched by `prevent_regexes` regex(es) in JSON;"
+        'e.g. \'[{"name": "custom", "regex": "\\\\.["\\\\\'”] "}]\'',
+    )
+    subparser_segment.add_argument(
+        "--cores",
+        type=int,
+        default=1,
+        help="number of CPU cores to use; default is 1 (single core)",
+    )
     subparser_segment.set_defaults(func=sentsplit.cli.sentsplit_segment)
 
     args = parser.parse_args()
diff --git a/sentsplit/cli.py b/sentsplit/cli.py
index 2809772..2c868f3 100644
--- a/sentsplit/cli.py
+++ b/sentsplit/cli.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import os
 import pprint
 import subprocess
@@ -6,7 +8,6 @@
 from copy import deepcopy
 from datetime import datetime
 from multiprocessing import Pool
-from typing import List
 
 from loguru import logger
 from tqdm import tqdm
@@ -34,14 +35,24 @@ def sentsplit_train(args: Namespace) -> None:
 
     if output_path is None:
         corpus_basename = os.path.basename(corpus_path)
-        today_str = datetime.today().strftime('%d%m%Y')
-        output_path = f'./{corpus_basename}.{lang}-{ngram}-gram-{today_str}.model'
+        today_str = datetime.today().strftime("%d%m%Y")
+        output_path = f"./{corpus_basename}.{lang}-{ngram}-gram-{today_str}.model"
 
-    train_crf_model(corpus_path, ngram, output_path, sample_min_length, crf_max_iteration, depunctuation_ratio, num_depunctuation_endings, ending_length, despace_ratio)
+    train_crf_model(
+        corpus_path,
+        ngram,
+        output_path,
+        sample_min_length,
+        crf_max_iteration,
+        depunctuation_ratio,
+        num_depunctuation_endings,
+        ending_length,
+        despace_ratio,
+    )
 
 
-def call_sentsplit_batch(line: str) -> List[str]:
-    return sentsplit.segment(line.rstrip('\n'))
+def call_sentsplit_batch(line: str) -> list[str]:
+    return sentsplit.segment(line.rstrip("\n"))
 
 
 def sentsplit_segment(args: Namespace) -> None:
@@ -50,13 +61,13 @@ def sentsplit_segment(args: Namespace) -> None:
     output_file = args.output
     assert os.path.isfile(input_file)
     if output_file is None:
-        output_file = f'{input_file}.segment'
+        output_file = f"{input_file}.segment"
 
     override_options = vars(args)
-    cores = override_options['cores']
-    del override_options['cores']
+    cores = override_options["cores"]
+    del override_options["cores"]
     try:
-        default_config = deepcopy(getattr(config, '{}_config'.format(lang)))
+        default_config = deepcopy(getattr(config, "{}_config".format(lang)))
     except AttributeError:
         logger.critical(f"Unsupported language: {lang.upper()}")
         sys.exit(1)
@@ -68,12 +79,12 @@ def sentsplit_segment(args: Namespace) -> None:
     global sentsplit
     sentsplit = SentSplit(lang, **default_config)
 
-    num_lines = int(subprocess.check_output(['wc', '-l', input_file]).decode('utf8').split()[0])
-    with open(input_file, 'r') as inf, open(output_file, 'w') as outf:
+    num_lines = int(subprocess.check_output(["wc", "-l", input_file]).decode("utf8").split()[0])
+    with open(input_file, "r") as inf, open(output_file, "w") as outf:
         cnt = 0
         if cores <= 1:
             for line in tqdm(inf, total=num_lines):
-                line = line.rstrip('\n')
+                line = line.rstrip("\n")
                 segments = sentsplit.segment(line)
                 for segment in segments:
                     outf.write(f"{segment}\n")
diff --git a/sentsplit/config.py b/sentsplit/config.py
index ee8d77c..88d74f5 100644
--- a/sentsplit/config.py
+++ b/sentsplit/config.py
@@ -1,60 +1,84 @@
+from __future__ import annotations
+
 from copy import deepcopy
 
-base_config = {
-    'ngram': 5,
-    'mincut': 7,
-    'maxcut': 500,
-    'strip_spaces': False,
-    'segment_regexes': [
-        {'name': 'after_semicolon'},
-        {'name': 'ellipsis'},
-        {'name': 'newline'},
+from typing_extensions import NotRequired, TypedDict
+
+from sentsplit.regexes import Regex
+
+
+class Config(TypedDict):
+    model: NotRequired[str]
+
+    ngram: int
+
+    mincut: int
+    maxcut: int
+
+    strip_spaces: bool
+    handle_multiple_spaces: bool
+
+    segment_regexes: list[Regex]
+
+    prevent_regexes: list[Regex]
+    prevent_word_split: bool
+
+
+base_config = Config(
+    ngram=5,
+    mincut=7,
+    maxcut=500,
+    strip_spaces=False,
+    segment_regexes=[
+        Regex(name="after_semicolon"),
+        Regex(name="ellipsis"),
+        Regex(name="newline"),
     ],
-    'prevent_regexes': [
-        {'name': 'liberal_url'},
-        {'name': 'period_followed_by_lowercase'},
+    prevent_regexes=[
+        Regex(name="liberal_url"),
+        Regex(name="period_followed_by_lowercase"),
     ],
-    'handle_multiple_spaces': True,
-    'prevent_word_split': True,
-}
+    handle_multiple_spaces=True,
+    prevent_word_split=True,
+)
 
 de_config = deepcopy(base_config)
-de_config['model'] = 'crf_models/de-default-25032021.model'
+de_config["model"] = "crf_models/de-default-25032021.model"
 
 en_config = deepcopy(base_config)
-en_config['model'] = 'crf_models/en-default-25032021.model'
+en_config["model"] = "crf_models/en-default-25032021.model"
 
 fr_config = deepcopy(base_config)
-fr_config['model'] = 'crf_models/fr-default-25032021.model'
+fr_config["model"] = "crf_models/fr-default-25032021.model"
 
 it_config = deepcopy(base_config)
-it_config['model'] = 'crf_models/it-default-25032021.model'
+it_config["model"] = "crf_models/it-default-25032021.model"
 
 ja_config = deepcopy(base_config)
-ja_config['model'] = 'crf_models/ja-default-05042021.model'
-ja_config['prevent_word_split'] = False
-ja_config['mincut'] = 5
+ja_config["model"] = "crf_models/ja-default-05042021.model"
+ja_config["mincut"] = 5
+ja_config["prevent_word_split"] = False
 
 ko_config = deepcopy(base_config)
-ko_config['model'] = 'crf_models/ko-default-05042021.model'
-ko_config['mincut'] = 5
+ko_config["model"] = "crf_models/ko-default-05042021.model"
+ko_config["mincut"] = 5
 
 lt_config = deepcopy(base_config)
-lt_config['model'] = 'crf_models/lt-default-25032021.model'
+lt_config["model"] = "crf_models/lt-default-25032021.model"
 
 pl_config = deepcopy(base_config)
-pl_config['model'] = 'crf_models/pl-default-25032021.model'
+pl_config["model"] = "crf_models/pl-default-25032021.model"
 
 pt_config = deepcopy(base_config)
-pt_config['model'] = 'crf_models/pt-default-25032021.model'
+pt_config["model"] = "crf_models/pt-default-25032021.model"
 
 ru_config = deepcopy(base_config)
-ru_config['model'] = 'crf_models/ru-default-25032021.model'
+ru_config["model"] = "crf_models/ru-default-25032021.model"
 
 tr_config = deepcopy(base_config)
-tr_config['model'] = 'crf_models/tr-default-25032021.model'
+tr_config["model"] = "crf_models/tr-default-25032021.model"
 
 zh_config = deepcopy(base_config)
-zh_config['model'] = 'crf_models/zh-default-05042021.model'
-zh_config['prevent_word_split'] = False
-zh_config['mincut'] = 5
+zh_config["model"] = "crf_models/zh-default-05042021.model"
+zh_config["mincut"] = 5
+zh_config["prevent_word_split"] = False
diff --git a/sentsplit/meta_data.py b/sentsplit/meta_data.py
index 9aa6240..721c78a 100644
--- a/sentsplit/meta_data.py
+++ b/sentsplit/meta_data.py
@@ -1,5 +1,5 @@
-version = '1.0.7'
-description = 'A flexible sentence segmentation library using CRF model and regex rules'
-author = 'Zae Myung Kim'
-author_email = 'zaemyung@gmail.com'
-url = 'https://github.com/zaemyung/sentsplit'
+version = "1.0.8"
+description = "A flexible sentence segmentation library using CRF model and regex rules"
+author = "Zae Myung Kim"
+author_email = "zaemyung@gmail.com"
+url = "https://github.com/zaemyung/sentsplit"
diff --git a/sentsplit/regexes.py b/sentsplit/regexes.py
index 1c3260f..31a2b15 100644
--- a/sentsplit/regexes.py
+++ b/sentsplit/regexes.py
@@ -1,35 +1,26 @@
+from typing_extensions import TypedDict
+
+
+class Regex(TypedDict, total=False):
+    name: str
+    regex: str
+    at: str
+
+
 """
 `segment_regexes`: make sure a string is segmented at either the start or end of the matching group(s)
 """
-newline = {
-    'name': 'newline',
-    'regex': r'\n',
-    'at': 'end'
-}
-
-ellipsis = {
-    'name': 'ellipsis',
-    'regex': r'…(?![\!\?\.．？！])',
-    'at': 'end'
-}
-
-after_semicolon = {
-    'name': 'after_semicolon',
-    'regex': r' *;',
-    'at': 'end'
-}
+newline = Regex(name="newline", regex=r"\n", at="end")
+ellipsis = Regex(name="ellipsis", regex=r"…(?![\!\?\.．？！])", at="end")
+after_semicolon = Regex(name="after_semicolon", regex=r" *;", at="end")
 
 
 """
 `prevent_regexes`: make sure a string is not segmented at characters that fall within the matching group(s)
 """
-liberal_url = {
+liberal_url = Regex(
+    name="liberal_url",
     # ref. https://gist.github.com/gruber/249502#gistcomment-1328838
-    'name': 'liberal_url',
-    'regex': r'\b((?:[a-z][\w\-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))'
-}
-
-period_followed_by_lowercase = {
-    'name': 'period_followed_by_lowercase',
-    'regex': r'\.(?= *[a-z])'
-}
+    regex=r'\b((?:[a-z][\w\-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:\'".,<>?«»“”‘’]))',
+)
+period_followed_by_lowercase = Regex(name="period_followed_by_lowercase", regex=r"\.(?= *[a-z])")
diff --git a/sentsplit/segment.py b/sentsplit/segment.py
index 4278153..564d6de 100644
--- a/sentsplit/segment.py
+++ b/sentsplit/segment.py
@@ -1,29 +1,31 @@
+from __future__ import annotations
+
 import math
 import os
 import pprint
 from copy import deepcopy
-from typing import Any, Dict, List, Tuple, Union
+from typing import Any, Optional, Union
 
 import pkg_resources
 import pycrfsuite
 import regex as re
 from loguru import logger
 
-from . import config, regexes
-from .train import _PUNCTUATIONS, _sample_to_features
-from .utils import split_keep_multiple_separators
-
+from sentsplit import config, regexes
+from sentsplit.regexes import Regex
+from sentsplit.train import _PUNCTUATIONS, _sample_to_features
+from sentsplit.utils import split_keep_multiple_separators
 
-class SentSplit():
 
+class SentSplit:
     def __init__(self, lang: str, **kwargs: Any) -> None:
         self.lang = lang
         try:
-            default_config = deepcopy(getattr(config, f'{self.lang}_config'))
+            default_config = deepcopy(getattr(config, f"{self.lang}_config"))
         except AttributeError:
             logger.critical(f"Unsupported language: {self.lang.upper()}")
             logger.info(f"Falling back to the `base_config`. Need to provide `model` path.")
-            default_config = deepcopy(getattr(config, 'base_config'))
+            default_config = deepcopy(getattr(config, "base_config"))
 
         # override config arguments
         for k, v in kwargs.items():
@@ -35,9 +37,9 @@ def __init__(self, lang: str, **kwargs: Any) -> None:
         self.config = default_config
 
         # load tagger
-        model_path = self.config['model']
+        model_path = self.config["model"]
         if not os.path.isfile(model_path):
-            model_path = pkg_resources.resource_filename('sentsplit', model_path)
+            model_path = pkg_resources.resource_filename("sentsplit", model_path)
         self.tagger = SentSplit._load_model(model_path)
 
         # fill regexes
@@ -59,19 +61,19 @@ def _load_model(model_path: str) -> pycrfsuite.Tagger:
         return tagger
 
     def _fill_regexes(self) -> None:
-        '''
+        """
         Retrieve actual regexes from `regexes.py` by their `name`s if not given
-        '''
-        for rgx_index, rgx in enumerate(self.config['segment_regexes']):
-            if 'regex' not in rgx:
-                self.config['segment_regexes'][rgx_index] = getattr(regexes, rgx['name'])
-        for rgx_index, rgx in enumerate(self.config['prevent_regexes']):
-            if 'regex' not in rgx:
-                self.config['prevent_regexes'][rgx_index] = getattr(regexes, rgx['name'])
-
-    def segment(self, string: Union[str, List], strip_spaces: Union[None, bool] = None) -> List[str]:
+        """
+        for rgx_index, rgx in enumerate(self.config["segment_regexes"]):
+            if "regex" not in rgx:
+                self.config["segment_regexes"][rgx_index] = getattr(regexes, rgx["name"])
+        for rgx_index, rgx in enumerate(self.config["prevent_regexes"]):
+            if "regex" not in rgx:
+                self.config["prevent_regexes"][rgx_index] = getattr(regexes, rgx["name"])
+
+    def segment(self, string: Union[str, list[str]], strip_spaces: Optional[bool] = None) -> list[str]:
         if strip_spaces is None:
-            strip_spaces = self.config['strip_spaces']
+            strip_spaces = self.config["strip_spaces"]
         else:
             assert isinstance(strip_spaces, bool), "`strip_spaces` must be a boolean value"
 
@@ -84,10 +86,10 @@ def segment(self, string: Union[str, List], strip_spaces: Union[None, bool] = No
             result = [self._segment(t, strip_spaces) for t in string]
         return result
 
-    def _segment(self, original_string: str, strip_spaces: bool) -> List[str]:
-        '''This method deals with a single string'''
+    def _segment(self, original_string: str, strip_spaces: bool) -> list[str]:
+        """This method deals with a single string"""
         # initially segment by line feeds
-        strings = split_keep_multiple_separators(original_string, ['\n'])
+        strings = split_keep_multiple_separators(original_string, ["\n"])
 
         # list of original characters per string
         chars_strings = []
@@ -99,45 +101,63 @@ def _segment(self, original_string: str, strip_spaces: bool) -> List[str]:
         for string in strings:
             # keep the original characters per string
             chars_strings.append([c for c in string])
-            if self.config['handle_multiple_spaces']:
+            if self.config["handle_multiple_spaces"]:
                 # replace multiple spaces with a single space for better segmentation by CRF model
-                preprocessed_string, multiple_spaces_positions = SentSplit._substitute_multiple_spaces(string)
+                (
+                    preprocessed_string,
+                    multiple_spaces_positions,
+                ) = SentSplit._substitute_multiple_spaces(string)
                 multiple_spaces_positions_strings.append(multiple_spaces_positions)
                 # convert the string into character n-gram features
-                features_strings.append(_sample_to_features([c for c in preprocessed_string], self.config['ngram']))
+                features_strings.append(_sample_to_features([c for c in preprocessed_string], self.config["ngram"]))
             else:
                 # convert the string into character n-gram features
-                features_strings.append(_sample_to_features([c for c in string], self.config['ngram']))
+                features_strings.append(_sample_to_features([c for c in string], self.config["ngram"]))
 
         # tag strings
         y_tags_strings = [self.tagger.tag(f_s) for f_s in features_strings]
 
-        if self.config['handle_multiple_spaces']:
+        if self.config["handle_multiple_spaces"]:
             # adjust y_tags_strings to account for the removed multiple spaces
-            y_tags_strings = SentSplit._adjust_tags_for_multiple_spaces(y_tags_strings, multiple_spaces_positions_strings)
-
-        y_tags_strings = SentSplit._tag_segment_regexes(y_tags_strings, strings, self.config['segment_regexes'])
-        y_tags_strings = SentSplit._tag_prevent_regexes(y_tags_strings, strings, self.config['prevent_regexes'],
-                                                        self.config['maxcut'], self.config['prevent_word_split'])
-        results = SentSplit._segment_by_char_tag(chars_strings, y_tags_strings, strip_spaces, self.config['maxcut'], self.config['mincut'])
+            y_tags_strings = SentSplit._adjust_tags_for_multiple_spaces(
+                y_tags_strings, multiple_spaces_positions_strings
+            )
+
+        y_tags_strings = SentSplit._tag_segment_regexes(y_tags_strings, strings, self.config["segment_regexes"])
+        y_tags_strings = SentSplit._tag_prevent_regexes(
+            y_tags_strings,
+            strings,
+            self.config["prevent_regexes"],
+            self.config["maxcut"],
+            self.config["prevent_word_split"],
+        )
+        results = SentSplit._segment_by_char_tag(
+            chars_strings,
+            y_tags_strings,
+            strip_spaces,
+            self.config["maxcut"],
+            self.config["mincut"],
+        )
         return results
 
     @staticmethod
-    def _substitute_multiple_spaces(line: str) -> Tuple[str, List[Tuple[int, int]]]:
-        '''
+    def _substitute_multiple_spaces(line: str) -> tuple[str, list[tuple[int, int]]]:
+        """
         Substitute multiple spaces with a single space and record their indices so that they can be restored later
         multiple_spaces_positions: [(start_ind, end_ind), ..]
-        '''
-        rgx_multiple_spaces = r'(\s{2,})'
+        """
+        rgx_multiple_spaces = r"(\s{2,})"
         multiple_spaces_positions = [(m.start(0), m.end(0)) for m in re.finditer(rgx_multiple_spaces, line)]
         if len(multiple_spaces_positions) > 0:
-            line = re.sub(rgx_multiple_spaces, ' ', line)
+            line = re.sub(rgx_multiple_spaces, " ", line)
         return line, multiple_spaces_positions
 
     @staticmethod
-    def _adjust_tags_for_multiple_spaces(y_tags_strings: List[List[str]],
-                                         multiple_spaces_positions_strings: List[List[Tuple[int, int]]]) -> List[List[str]]:
-        '''
+    def _adjust_tags_for_multiple_spaces(
+        y_tags_strings: list[list[str]],
+        multiple_spaces_positions_strings: list[list[tuple[int, int]]],
+    ) -> list[list[str]]:
+        """
         So far y_tags_strings contains labels (tags) for the multiple-space-substituted strings
         This method adds back (`len_matched_spaces` - 1) 'O' tags after the substituted single space to restore original strings
         Note that `start_char_ind` and `end_char_ind` are character-level indices of the original strings
@@ -149,62 +169,70 @@ def _adjust_tags_for_multiple_spaces(y_tags_strings: List[List[str]],
             Returns:
                 (Restores the three spaces after '~ good.'
                 y_tags_string: ['O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'O', 'EOS', 'O', 'O', 'O', 'O','O']
-        '''
+        """
         assert len(y_tags_strings) == len(multiple_spaces_positions_strings)
         for string_index, multiple_spaces_positions in enumerate(multiple_spaces_positions_strings):
             y_tags = y_tags_strings[string_index]
             for start_char_ind, end_char_ind in multiple_spaces_positions:
                 len_matched_spaces = end_char_ind - start_char_ind
                 # y_tags already contains an 'O' for one space, hence subtract 1
-                o_tags_for_matched_spaces = ['O'] * (len_matched_spaces - 1)
+                o_tags_for_matched_spaces = ["O"] * (len_matched_spaces - 1)
                 # insert 'O's after the index of the substituted single space
-                y_tags = y_tags[:start_char_ind + 1] + o_tags_for_matched_spaces + y_tags[start_char_ind + 1:]
+                y_tags = y_tags[: start_char_ind + 1] + o_tags_for_matched_spaces + y_tags[start_char_ind + 1 :]
             y_tags_strings[string_index] = y_tags
         return y_tags_strings
 
     @staticmethod
-    def _tag_segment_regexes(y_tags_strings: List[List[str]], strings: List[str], segment_regexes: List[Dict]) -> List[List[str]]:
-        '''
+    def _tag_segment_regexes(
+        y_tags_strings: list[list[str]], strings: list[str], segment_regexes: list[Regex]
+    ) -> list[list[str]]:
+        """
         Label either the start or end indices of the matched regex patterns with 'EOS'
         @param segment_regexes: [{'regex': '<pattern>', 'at': <'end' or 'start'>}, ..]
-        '''
+        """
         for string_index, string in enumerate(strings):
             assert len(string) == len(y_tags_strings[string_index])
             for rgx in segment_regexes:
-                for matched_position in re.finditer(rgx['regex'], string):
+                for matched_position in re.finditer(rgx["regex"], string):
                     start = matched_position.start(0)
                     end = matched_position.end(0) - 1
-                    if rgx['at'] == 'start':
-                        y_tags_strings[string_index][start] = 'EOS'
+                    if rgx["at"] == "start":
+                        y_tags_strings[string_index][start] = "EOS"
                     else:
-                        y_tags_strings[string_index][end] = 'EOS'
+                        y_tags_strings[string_index][end] = "EOS"
         return y_tags_strings
 
     @staticmethod
-    def _tag_prevent_regexes(y_tags_strings: List[List[str]], strings: List[str], prevent_regexes: List[Dict],
-                             maxcut: int, prevent_word_split: bool) -> List[List[str]]:
-        '''
+    def _tag_prevent_regexes(
+        y_tags_strings: list[list[str]],
+        strings: list[str],
+        prevent_regexes: list[Regex],
+        maxcut: int,
+        prevent_word_split: bool,
+    ) -> list[list[str]]:
+        """
         Remove 'EOS' label for characters that are matched by prevent_regexes
         and prevent these characters from being cut due to maxcut
         @param prevent_regexes: [{'regex': '<pattern>'}, ..]
-        '''
-        def _tag_prevent_word_split(y_tags: List[str], curr_string: str) -> List[str]:
-            '''Prevent segmentation occurring in the middle of a word'''
+        """
+
+        def _tag_prevent_word_split(y_tags: list[str], curr_string: str) -> list[str]:
+            """Prevent segmentation occurring in the middle of a word"""
             assert isinstance(curr_string, str)
             for i, tag in enumerate(y_tags[:-1]):
-                if tag != 'EOS':
+                if tag != "EOS":
                     continue
                 char = curr_string[i]
                 if char not in _PUNCTUATIONS and not char.isspace() and not curr_string[i + 1].isspace():
-                    y_tags[i] = 'O'
+                    y_tags[i] = "O"
             return y_tags
 
-        def _get_last_segmented_index(labels: List[str], pivot: int) -> int:
-            '''Return the closest previously cut index to pivot'''
+        def _get_last_segmented_index(labels: list[str], pivot: int) -> int:
+            """Return the closest previously cut index to pivot"""
             if pivot < 1:
                 return -1
             for index in range(pivot - 1, -1, -1):
-                if labels[index] == 'EOS':
+                if labels[index] == "EOS":
                     return index
             return -1
 
@@ -214,12 +242,12 @@ def _get_last_segmented_index(labels: List[str], pivot: int) -> int:
             if prevent_word_split:
                 y_tags_string = _tag_prevent_word_split(y_tags_string, string)
             for rgx in prevent_regexes:
-                for matched_position in re.finditer(rgx['regex'], string):
+                for matched_position in re.finditer(rgx["regex"], string):
                     start = matched_position.start(0)
                     end = matched_position.end(0) - 1
                     # add 'O' at matched positions
                     for pos in range(start, end + 1):
-                        y_tags_string[pos] = 'O'
+                        y_tags_string[pos] = "O"
 
                     # make sure this pattern is not maxcut later by segmenting before the pattern
                     if end < maxcut:
@@ -235,23 +263,30 @@ def _get_last_segmented_index(labels: List[str], pivot: int) -> int:
                     next_maxcut_pos = last_segmented_plus_one + maxcutted_length
                     # cut before the pattern if the next_maxcut_pos falls within the pattern
                     if start <= next_maxcut_pos < end and start - 1 > 0:
-                        y_tags_string[start - 1] = 'EOS'
+                        y_tags_string[start - 1] = "EOS"
 
             y_tags_strings[string_index] = y_tags_string
         return y_tags_strings
 
     @staticmethod
-    def _segment_by_char_tag(chars_strings: List[List[str]], y_tags_strings: List[List[str]], strip_spaces: bool, maxcut: int, mincut: int) -> List[str]:
-        '''
+    def _segment_by_char_tag(
+        chars_strings: list[list[str]],
+        y_tags_strings: list[list[str]],
+        strip_spaces: bool,
+        maxcut: int,
+        mincut: int,
+    ) -> list[str]:
+        """
         Loop through character-level tags and segment when 'EOS' and other conditions are met
         :param chars_strings: list of lines where each line consists of characters
         :param y_tags_strings: list of lines where each line consists of tags which are either 'O' or 'EOS'
-        '''
+        """
+
         def _check_and_add_sentence(sent: str, str_len: int, cur_ind: int, is_leftover: bool = False) -> bool:
-            '''
+            """
             Check if the given `sent` can be added to `results`.
             If so, add to `results` and return `True`; otherwise, return `False`
-            '''
+            """
             if len(sent) <= mincut:
                 if not is_leftover:
                     return False
@@ -269,49 +304,57 @@ def _check_and_add_sentence(sent: str, str_len: int, cur_ind: int, is_leftover:
             return True
 
         def _segment_maxcut_string(sent: str) -> str:
-            '''
+            """
             Heuristically segment a maxcut string into two, add the first half to `results`, and return the remaining half.
             A list of heuristic regexes are applied to `sent` in decreasing order of importance.
             As soon as a matching is found, the sentence is segmented.
-            '''
+            """
             rgx_punctuation_and_closing = r'(?<=[\.。︀?？!！…])[\'"’”❜❞›»❯」』)）\]］】〟]'
             rgx_closing_and_punctuation = r'(?<=[\'"’”❜❞›»❯」』)）\]］】〟])[\.。︀?？!！…]'
-            rgx_space_and_dash = r'(?<=\s)\p{Pd}'
-            rgx_colon = r'[:﹕：;﹔；؛⁏]'
+            rgx_space_and_dash = r"(?<=\s)\p{Pd}"
+            rgx_colon = r"[:﹕：;﹔；؛⁏]"
             rgx_not_space_and_closing_and_space = r'(?<=[^\s])[\'"’”❜❞›»❯」』)）\]］】〟](?=\s)'
-            rgx_comma = r'[,，﹐]'
-            rgx_closing = r'[’”❜❞›»❯」』)）\]］】〟]'
-            rgx_whitespace = r'\s'
-            heuristics = [rgx_punctuation_and_closing, rgx_closing_and_punctuation, rgx_space_and_dash,
-                          rgx_colon, rgx_not_space_and_closing_and_space, rgx_comma, rgx_closing, rgx_whitespace]
+            rgx_comma = r"[,，﹐]"
+            rgx_closing = r"[’”❜❞›»❯」』)）\]］】〟]"
+            rgx_whitespace = r"\s"
+            heuristics = [
+                rgx_punctuation_and_closing,
+                rgx_closing_and_punctuation,
+                rgx_space_and_dash,
+                rgx_colon,
+                rgx_not_space_and_closing_and_space,
+                rgx_comma,
+                rgx_closing,
+                rgx_whitespace,
+            ]
             for heu in heuristics:
                 for match in re.finditer(heu, sent):
                     assert match.end() - match.start() == 1
-                    first_half = sent[:match.end()]
+                    first_half = sent[: match.end()]
                     if _check_and_add_sentence(first_half, len(sent), len(first_half)):
-                        return sent[match.end():]
+                        return sent[match.end() :]
             if _check_and_add_sentence(sent, string_length, current_index, is_leftover=True):
-                return ''
+                return ""
             raise RuntimeError(f"Cannot segment maxcut string: {sent}")
 
         results = []
         for char_string, tags in zip(chars_strings, y_tags_strings):
-            sentence = ''
+            sentence = ""
             string_length = len(char_string)
-            if string_length < 1:
+            if string_length < 1 and len(char_string) > 0:
                 results.append(char_string)
                 continue
             for current_index, (current_character, tag) in enumerate(zip(char_string, tags)):
                 if len(sentence) >= maxcut:
                     sentence = _segment_maxcut_string(sentence)
-                if tag == 'EOS':
+                if tag == "EOS":
                     sentence += current_character
                     if _check_and_add_sentence(sentence, string_length, current_index):
-                        sentence = ''
+                        sentence = ""
                 else:
                     sentence += current_character
             if _check_and_add_sentence(sentence, string_length, current_index, is_leftover=True):
-                sentence = ''
+                sentence = ""
         return results
 
     def close(self):
diff --git a/sentsplit/train.py b/sentsplit/train.py
index 0fb97de..3bfbf75 100644
--- a/sentsplit/train.py
+++ b/sentsplit/train.py
@@ -1,24 +1,43 @@
+from __future__ import annotations
+
 import random
 from collections import Counter
-from typing import List, Set, Tuple
 
 import pycrfsuite
 from loguru import logger
 from tqdm import tqdm
 
-from .utils import read_lines
-
-_PUNCTUATIONS = {'.', '?', '!', '\"', '\'', '”', '．', '？', '！', '。', '…'}
-
-
-def train_crf_model(corpus_path: str, ngram: int, output_path: str, sample_min_length: int, crf_max_iteration: int,
-                    depunctuation_ratio: float, num_depunctuation_endings: int, ending_length: int, despace_ratio: float) -> None:
-    train_samples = _preprocess(corpus_path, sample_min_length, depunctuation_ratio, num_depunctuation_endings, ending_length, despace_ratio)
+from sentsplit.utils import read_lines
+
+_PUNCTUATIONS = {".", "?", "!", '"', "'", "”", "．", "？", "！", "。", "…"}
+
+
+def train_crf_model(
+    corpus_path: str,
+    ngram: int,
+    output_path: str,
+    sample_min_length: int,
+    crf_max_iteration: int,
+    depunctuation_ratio: float,
+    num_depunctuation_endings: int,
+    ending_length: int,
+    despace_ratio: float,
+) -> None:
+    train_samples = _preprocess(
+        corpus_path,
+        sample_min_length,
+        depunctuation_ratio,
+        num_depunctuation_endings,
+        ending_length,
+        despace_ratio,
+    )
     X_train, y_train = _create_features(train_samples, ngram)
     _fit_model(X_train, y_train, output_path, crf_max_iteration)
 
 
-def _compute_top_k_depunctuated_endings(lines: List[str], num_depunctuation_endings: int, ending_length: int) -> Set[str]:
+def _compute_top_k_depunctuated_endings(
+    lines: list[str], num_depunctuation_endings: int, ending_length: int
+) -> set[str]:
     """
     Computes the most common endings of sentences.
     Applicable only to languages that utilize specific endings
@@ -30,7 +49,7 @@ def _compute_top_k_depunctuated_endings(lines: List[str], num_depunctuation_endi
         line = line.strip()
         if len(line) - 1 < ending_length or line[-1] not in _PUNCTUATIONS:
             continue
-        ending = line[-ending_length - 1:-1]
+        ending = line[-ending_length - 1 : -1]
         all_endings.append(ending)
     ending_counter = Counter(all_endings)
     top_endings = ending_counter.most_common(num_depunctuation_endings)
@@ -38,8 +57,14 @@ def _compute_top_k_depunctuated_endings(lines: List[str], num_depunctuation_endi
     return set(ending for ending, _ in top_endings)
 
 
-def _preprocess(corpus_path: str, sample_min_length: int, depunctuation_ratio:float,
-                num_depunctuation_endings: int, ending_length: int, despace_ratio: float) -> List[List[Tuple[str, str]]]:
+def _preprocess(
+    corpus_path: str,
+    sample_min_length: int,
+    depunctuation_ratio: float,
+    num_depunctuation_endings: int,
+    ending_length: int,
+    despace_ratio: float,
+) -> list[list[tuple[str, str]]]:
     logger.info("Preprocessing sentences..")
     assert 0.0 <= depunctuation_ratio <= 1.0
     assert 0.0 <= despace_ratio <= 1.0
@@ -57,25 +82,28 @@ def _preprocess(corpus_path: str, sample_min_length: int, depunctuation_ratio:fl
     single_sample = []
     for line in tqdm(raw_lines):
         chars = [c for c in line.strip()]
-        labels = ['O' for _ in chars[:-1]] + ['EOS']
+        labels = ["O" for _ in chars[:-1]] + ["EOS"]
         # Ex. 'Hello!' -> [('H', 'O'), ('e', 'O'), ('l', 'O'), ('l', 'O'), ('o', 'O'), ('!', 'EOS')]
         char_and_labels = [(char, label) for char, label in zip(chars, labels)]
 
         if len(single_sample) < 1:
             single_sample = char_and_labels
         else:
-            if num_depunctuation_remainings > 0 and single_sample[-1][0] in _PUNCTUATIONS and \
-               len(single_sample) - 1 > ending_length and \
-               ''.join(char for char, _ in single_sample[-ending_length - 1:-1]) in depunctuated_endings:
+            if (
+                num_depunctuation_remainings > 0
+                and single_sample[-1][0] in _PUNCTUATIONS
+                and len(single_sample) - 1 > ending_length
+                and "".join(char for char, _ in single_sample[-ending_length - 1 : -1]) in depunctuated_endings
+            ):
                 single_sample = single_sample[:-1]
-                new_eos = (single_sample[-1][0], 'EOS')
+                new_eos = (single_sample[-1][0], "EOS")
                 single_sample[-1] = new_eos
                 num_depunctuation_remainings -= 1
             if num_despace_remainings > 0 and single_sample[-1][0] in _PUNCTUATIONS:
                 single_sample += char_and_labels
                 num_despace_remainings -= 1
             else:
-                single_sample += [(' ', 'O')] + char_and_labels
+                single_sample += [(" ", "O")] + char_and_labels
 
         if len(single_sample) >= sample_min_length:
             train_samples.append(single_sample)
@@ -83,7 +111,7 @@ def _preprocess(corpus_path: str, sample_min_length: int, depunctuation_ratio:fl
     return train_samples
 
 
-def _create_features(samples: List[List[Tuple[str, str]]], ngram: int) -> Tuple[List[List[List[str]]], List[List[str]]]:
+def _create_features(samples: list[list[tuple[str, str]]], ngram: int) -> tuple[list[list[list[str]]], list[list[str]]]:
     logger.info("Creating features..")
     X = []
     y = []
@@ -93,21 +121,21 @@ def _create_features(samples: List[List[Tuple[str, str]]], ngram: int) -> Tuple[
     return X, y
 
 
-def _sample_to_features(sample: List[Tuple[str, str]], ngram: int) -> List[List[str]]:
-    def _char_to_features(i: int) -> List[str]:
+def _sample_to_features(sample: list[tuple[str, str]], ngram: int) -> list[list[str]]:
+    def _char_to_features(i: int) -> list[str]:
         char = sample[i][0]
         feats = [
-            'bias',
-            f'char={char}',
-            f'char.isdigit={char.isdigit()}',
-            f'char.isupper={char.isupper()}',
+            "bias",
+            f"char={char}",
+            f"char.isdigit={char.isdigit()}",
+            f"char.isupper={char.isupper()}",
         ]
         for j in range(1, min(i, ngram) + 1):
             char_j = sample[i - j][0]
-            feats.append(f'-{j}:char={char_j}')
+            feats.append(f"-{j}:char={char_j}")
         for j in range(1, min(len(sample) - 1 - i, ngram) + 1):
             char_j = sample[i + j][0]
-            feats.append(f'+{j}:char={char_j}')
+            feats.append(f"+{j}:char={char_j}")
         return feats
 
     features = []
@@ -116,22 +144,29 @@ def _char_to_features(i: int) -> List[str]:
     return features
 
 
-def _sample_to_labels(sample: List[Tuple[str, str]]) -> List[str]:
+def _sample_to_labels(sample: list[tuple[str, str]]) -> list[str]:
     return [label for _, label in sample]
 
 
-def _fit_model(X_train: List[List[List[str]]], y_train: List[List[str]], output_path: str, crf_max_iteration: int) -> None:
+def _fit_model(
+    X_train: list[list[list[str]]],
+    y_train: list[list[str]],
+    output_path: str,
+    crf_max_iteration: int,
+) -> None:
     logger.info("Fitting CRF model..")
     trainer = pycrfsuite.Trainer(verbose=True)
     for xseq, yseq in zip(X_train, y_train):
         trainer.append(xseq, yseq)
-    trainer.set_params({
-        'c1': 1.0,   # coefficient for L1 penalty
-        'c2': 1e-3,  # coefficient for L2 penalty
-        'epsilon': 1e-4,
-        'max_iterations': crf_max_iteration,  # stop earlier
-        # include transitions that are possible, but not observed
-        'feature.possible_transitions': True
-    })
+    trainer.set_params(
+        {
+            "c1": 1.0,  # coefficient for L1 penalty
+            "c2": 1e-3,  # coefficient for L2 penalty
+            "epsilon": 1e-4,
+            "max_iterations": crf_max_iteration,  # stop earlier
+            # include transitions that are possible, but not observed
+            "feature.possible_transitions": True,
+        }
+    )
     trainer.train(output_path)
     logger.info(f"Done! Model saved at {output_path}")
diff --git a/sentsplit/utils.py b/sentsplit/utils.py
index 6c85b25..ce9a829 100644
--- a/sentsplit/utils.py
+++ b/sentsplit/utils.py
@@ -1,29 +1,34 @@
+from __future__ import annotations
+
 from functools import reduce
-from typing import List
 
 import regex as re
 
 
-def read_lines(file_path: str) -> List[str]:
+def read_lines(file_path: str) -> list[str]:
     lines = []
-    with open(file_path, 'r') as inf:
+    with open(file_path, "r") as inf:
         for line in inf:
-            lines.append(line.rstrip('\n'))
+            lines.append(line.rstrip("\n"))
     return lines
 
 
-def write_lines(lines: List[str], file_path: str) -> None:
-    with open(file_path, 'w') as outf:
+def write_lines(lines: list[str], file_path: str) -> None:
+    with open(file_path, "w") as outf:
         for line in lines:
-            outf.write(f'{line}\n')
+            outf.write(f"{line}\n")
 
 
-def split_keep_multiple_separators(string: str, separators: List[str]) -> List[str]:
-    '''
+def split_keep_multiple_separators(string: str, separators: list[str]) -> list[str]:
+    """
     Split `string` on separator(s) but also keep the separator(s)
     Modified from `http://programmaticallyspeaking.com/split-on-separator-but-keep-the-separator-in-python.html`
     to extend to multiple separators.
-    '''
-    rgx_multiple_separators = '|'.join([re.escape(sep) for sep in separators])
-    rgx_multiple_separators = '(' + rgx_multiple_separators + ')'
-    return reduce(lambda acc, elem: acc[:-1] + [acc[-1] + elem] if (elem in separators) else acc + [elem], re.split(rgx_multiple_separators, string), [])
+    """
+    rgx_multiple_separators = "|".join([re.escape(sep) for sep in separators])
+    rgx_multiple_separators = "(" + rgx_multiple_separators + ")"
+    return reduce(
+        lambda acc, elem: acc[:-1] + [acc[-1] + elem] if (elem in separators) else acc + [elem],
+        re.split(rgx_multiple_separators, string),
+        [],
+    )
diff --git a/setup.py b/setup.py
index 066f455..73f7122 100644
--- a/setup.py
+++ b/setup.py
@@ -5,18 +5,18 @@
 
 # read meta-data of the project
 meta = {}
-meta_path = convert_path('sentsplit/meta_data.py')
+meta_path = convert_path("sentsplit/meta_data.py")
 with open(meta_path) as meta_file:
     exec(meta_file.read(), meta)
 
 # read the contents README.md file
 this_directory = os.path.abspath(os.path.dirname(__file__))
-with open(os.path.join(this_directory, 'README.md')) as f:
+with open(os.path.join(this_directory, "README.md")) as f:
     long_description = f.read()
 
 
 def requirements():
-    reqfile = 'requirements.txt'
+    reqfile = "requirements.txt"
     with open(os.path.join(os.path.dirname(__file__), reqfile)) as f:
         ret = []
         for eachline in f.read().splitlines():
@@ -26,19 +26,20 @@ def requirements():
         return ret
 
 
-setup(name='sentsplit',
-      version=meta['version'],
-      description=meta['description'],
-      long_description=long_description,
-      long_description_content_type='text/markdown',
-      author=meta['author'],
-      author_email=meta['author_email'],
-      url=meta['url'],
-      packages=find_packages(),
-      entry_points={
-          'console_scripts': ['sentsplit=sentsplit:main'],
-      },
-      include_package_data=True,
-      install_requires=requirements(),
-      python_requires='>=3.6',
+setup(
+    name="sentsplit",
+    version=meta["version"],
+    description=meta["description"],
+    long_description=long_description,
+    long_description_content_type="text/markdown",
+    author=meta["author"],
+    author_email=meta["author_email"],
+    url=meta["url"],
+    packages=find_packages(),
+    entry_points={
+        "console_scripts": ["sentsplit=sentsplit:main"],
+    },
+    include_package_data=True,
+    install_requires=requirements(),
+    python_requires=">=3.7",
 )
diff --git a/streamlit_app.py b/streamlit_app.py
index b921189..f59e43a 100644
--- a/streamlit_app.py
+++ b/streamlit_app.py
@@ -2,89 +2,108 @@
 from copy import deepcopy
 from typing import Iterable
 
-import sentsplit.config
 import streamlit as st
-from sentsplit.segment import SentSplit
 
+import sentsplit.config
+from sentsplit.segment import SentSplit
 
 supported_languages = {
-    'en': {'name': 'English',
-           'sample': 'Tell people you’re learning English. They will usually be understanding. You might be surprised at how patient people are when they know you’re still learning English. Ask people to repeat things you don’t understand. Don’t be embarrassed to admit you didn’t understand something. Ask people to correct you. Work is a perfect place to improve your English. Ask to be corrected and you will be learning all day. Speak up. Sometimes when people are not sure of what they’re saying, they mumble or speak quietly. This just makes them harder to understand! Don’t worry about being “right,” just “understood.” It doesn’t matter if your sentence didn’t use perfect grammar, as long as the person you were speaking to understood what you meant. And if you’ve asked them to correct your English, you can try to get it right next time. Be confident and be yourself. You’ll do fine! Below are many simple English sentences you can use in different situations at work. Take a deep breath… and you’re ready for work! “Hi [name], nice to meet you.” Say this to someone you just met for the first time. Repeating their name will help you remember it. “I’m still learning English, so please speak slowly.” This is a polite way to ask someone to slow down when they’re speaking English. “I just started working here. I’m the new [name of your job].” You can tell people you’re new with this sentence. “I’m working in the [name] department. What do you do here?” Many jobs have different departments, which are sections of the jobs that concentrate on one part of the job. For example, the IT (Information Technology) Department works with setting up and fixing the company’s computers. When you introduce yourself, tell the person which department you work for, and ask them for theirs. “See you later/tomorrow.” This is an informal but polite way of saying goodbye to someone. “Good night.” If you are leaving in the evening or late afternoon, you can say “good night” as a way of saying goodbye. Small talk is light conversation. It can be about the weather, food, anything that isn’t too serious. If you’re in the same room as someone, in an elevator together or just standing near each other and you aren’t working, making small talk can open the conversation and form friendships and connections. It also saves you from uncomfortable silences! The weather is always a safe topic for small talk. You can use this line for almost any kind of weather. If it’s a beautiful day you can say “I can’t believe how nice it is today.” Don’t be afraid to ask this question. It’s better to understand everything completely than to think you understand, and then waste time doing something the wrong way. Say this sentence, then repeat what you just heard. This is a great way to show that you were paying attention, and to make sure you understood everything correctly. See how easy these sentences are? Use these simple sentences so you don’t have to worry about how to speak, and you can save your energy for impressing your boss. Every job has its own rules and ways of doing things. Find out what they are so that you can follow them.'},
-    'fr': {'name': 'French',
-           'sample': 'En effet, alors que 67 départements sont toujours en vigilance orange, les températures les plus élevées doivent être atteintes lundi ou mardi selon les régions, d’après Météo France. La journée du dimanche 5 août a encore été très chaude sur la majeure partie du pays, particulièrement la moitié sud avec 36 °C à Bordeaux, Lyon et Marseille relevés vers 16 heures, ou encore 37 °C à Béziers ou Perpignan vers 15 heures. La moitié nord a un peu moins souffert, avec 30 °C à Paris ou 32 °C à Strasbourg. À peine un répit, avant « une nouvelle remontée dès lundi » des températures, a prévenu l’institut météorologique. D’après Météo France, « le pic de canicule devrait se situer lundi sur les régions du Sud-Ouest, mardi sur celles du Nord-Est, du Centre-Est et du bassin parisien ». Mardi, 36 °C sont ainsi attendus à Paris ou à Lyon. Pour ceux qui ont du mal à dormir ou rafraîchir leur logement, ça ne devrait pas s’arranger. « Les nuits suivantes resteront très chaudes, particulièrement en milieu urbain », indique Météo France. La journée de samedi avait été « la plus chaude en moyenne sur la France depuis la canicule de 2003 », selon François Jobard, prévisionniste à Météo France. Cet épisode sera quoi qu’il arrive dans le « top 3 » des canicules les plus sévères mesurées en France. À compter de la fin d’après-midi mardi, la France devrait mieux respirer. « L’arrivée d’une masse d’air plus maritime, venue de l’Atlantique, jouera un rôle rafraîchissant », a précisé le prévisionniste. On devrait retrouver à partir de jeudi des températures conformes voire légèrement inférieures à la normale saisonnière. La ministre de la Santé Agnès Buzyn a estimé dimanche que cette canicule devrait probablement se traduire par une « surmortalité chez les personnes âgées ». Mais les chiffres ne devraient pas être connus avant plusieurs semaines. Elle s’est cependant félicitée du nombre « relativement faible » de consultations dans les services d’urgence, 3 à 4 % du total seulement étant dues à la canicule. Pour éviter tout problème, il est recommandé de boire régulièrement de l’eau, de manger suffisamment, de se mouiller et de se ventiler, de passer du temps dans des endroits frais, et de prendre des nouvelles de ses proches. La chaleur et le trafic routier des vacanciers ont provoqué une pollution à l’ozone dans de nombreuses régions. En Ile-de-France elle devait se prolonger jusqu’à lundi au moins. L’Est, la vallée du Rhône, les Alpes et la Gironde sont aussi touchés. En Haute-Savoie, le préfet et le maire de Saint-Gervais ont recommandé aux alpinistes de différer leur ascension du Mont-Blanc par le couloir du Goûter en raison du risque important de chutes de pierres dû à la canicule.'},
-    'de': {'name': 'German',
-           'sample': 'Er hoffe, dass es in der kommenden Woche Klarheit darüber geben werde, ob diese Abkommen klappen. Doch nach einer Viertelstunde warf der Trainer hilflos beide Arme in die Luft und brüllte allerhand wütende Anweisungen aufs Feld.Mit der neuen Funktion zur Selbstkontrolle reagiert Facebook auf öffentliche Kritik und eine Debatte, die seit Monaten tobt. Hervorgegangen war sie aus der 1837 gegründeten „Leipziger Allgemeinen Zeitung“, die von Heinrich Brockhaus verlegt wurde. Dessen Sohn Eduard Brockhaus verlegte das Blatt nicht nur, sondern redigierte es von 1857 bis 1883 auch selbst. Dessen politische Ausrichtung prägte auch die Zeitung: Obwohl der Sozialdemokrat Wilhelm Liebknecht zur Gründungsredaktion gehörte, entwickelte die Zeitung bald ein nationalliberales bis konservatives Profil. Die Gesellschafter ab 1872 gehörten zum Hamburger Freundeskreis um den Reichskanzler Otto von Bismarck, der von Albertus Ohlendorff und seinem Bruder Heinrich angeführt wurde. In der Anfangsphase war die Norddeutsche Bank ein weiterer maßgeblicher Gesellschafter, um den benötigten Kaufpreis in Höhe von 800.000 Mark bereitzustellen.[1] Die Zeitung war immer regierungsnah („Bismarcks Hauspostille“), und wurde zeitweise von einem Reptilienfond des Auswärtigen Amtes direkt finanziert.[2] Bis 1872 führte der bisherige Eigentümer August Braß (1818–1876) die Redaktion, der dann ausschied und die Zeitung verkaufte, da er sich dagegen sträubte, zu „Bismarcks Schreiberling“ degradiert zu werden und jeden Leitartikel der Regierung vorzulegen. Die Auflage der Zeitung unter seinem Nachfolger Emil Pindter (1836–1897) brach um die Hälfte auf 5000 ein. Das Blatt ging hauptsächlich an höhere Beamte und andere Zeitungsredaktionen. 1894/95 übernahm Martin Griesemann die Chefredaktion, der wegen Krankheit oft von Verlagsdirektor Graf Rudolph von Westarp vertreten werden musste. Nach Griesemanns Tod 1897 übernahm Wilhelm Lauser das Ruder, gefolgt von Otto Runge, der das Blatt bis 1917 leitete.1917 erwarb der Verleger Reimar Hobbing das bis dahin „regierungsoffiziöse“ Blatt und sollte ihm im Kaiserreich kurz vor dessen Zusammenbruch ein demokratisches Profil geben mit dem Ziel, eine deutsche „Times“ zu schaffen. Liberale Intellektuelle wie Otto Flake, der kurzzeitig das Feuilleton leitete, konnten für die Mitarbeit gewonnen werden. Daneben arbeiteten bedeutende Historiker wie der damals zweiundzwanzigjährige Egmont Zechlin und Friedrich Meinecke (nach dem Zweiten Weltkrieg Gründungsrektor der FU Berlin) für das Blatt.Nach Ende des Ersten Weltkrieges wurde die „Norddeutsche Allgemeine“ am 12. November 1918 als „DAZ“ in Berlin wiedergegründet, nachdem der Arbeiter- und Soldatenrat unter Felix Stössinger in den ersten zwei Revolutionstagen die Räume der Zeitung besetzt hatte und die Zeitung unter dem Kopf „Internationale“ erschien. Unterstützung bei der Umgestaltung zur „DAZ“ erhielt der neue Eigentümer Reimar Hobbing von Otto Karl Stollberg. In der Zeit des Kaiserreiches und der Weimarer Republik gehörte die DAZ neben dem Berliner Tageblatt, der Vossischen Zeitung und der Frankfurter Zeitung zu den international bekanntesten und renommiertesten Blättern. Deswegen konnte die defizitäre Zeitung überleben. Sie war von der Ausrichtung her konservativ-staatstragender als die drei anderen Zeitungen. Immanuel Kant kritisierte die herkömmlichen Vorstellungen von Glück, da diese bedeuteten, dass jeder den unvorhersehbaren Schwankungen seiner eigenen wechselhaften Triebe, Bedürfnisse, Gewohnheiten und Vorlieben ausgeliefert sei. Auch der Freiheit, der Unsterblichkeit und Gott könne man sich im Wege der Vernunft nicht nähern. Man könne sie allein postulieren. Kant forderte stattdessen, dass sich der Mensch freiwillig den Gesetzen der Moral (kategorischer Imperativ) unterwirft. Dadurch könne ein selbstbestimmtes (autonomes), vernünftiges Leben geführt werden, in dem sich immerhin Zufriedenheit erreichen lasse. „Um der Sinnhaftigkeit der Sittlichkeit willen und um der Sinnhaftigkeit der Welt willen müssen wir Gott und Unsterblichkeit postulieren: Die ethische Bestimmung des Menschen fordert seine Weiterdauer.“Der modernen analytischen Sprachphilosophie, wie sie sich auf Wittgenstein beziehen lässt, schien unklar, welchen Status Aussagen über das, was man tun oder nicht tun soll, haben können. „Die Lösung des Problems des Lebens merkt man am Verschwinden dieses Problems. (Ist nicht dies der Grund, warum Menschen, denen der Sinn des Lebens nach langen Zweifeln klar wurde, warum diese dann nicht sagen können, worin der Sinn bestand?)“ (TLP 6.521)[36] Er konzedierte aber, dass der Mensch im religiösen Glauben einen Sinn finden kann: „An einen Gott glauben, heißt sehen, daß es mit den Tatsachen der Welt noch nicht abgetan ist. An Gott glauben, heißt sehen, daß das Leben einen Sinn hat.“[37] Vor allem Bertrand Russell betonte, dass man dem objektiven Naturprozess keinen Sinn entnehmen könne.[38] Damit geriet die Sinnfrage in einen Raum individueller Entscheidung außerhalb eines strenger zu fassenden philosophischen Projekts. Sie trennt aus dieser Perspektive daher den Bereich des Lebens von den Bereichen, in denen die Philosophie aus ihrer Sicht Antwort zu geben vermag.'},
-    'it': {'name': 'Italian',
-           'sample': 'Qui dove tutto è cominciato un sabato mattina di sei mesi fa, dove l\'Italia ha conosciuto e si è consegnata al fantasma sovranista dell\'Uomo Bianco, il tempo si è fermato. Niente fiori, niente targhe a ricordare le vittime della mattanza del "lupo". Piuttosto, ancora i segni lasciati dal gesso della scientifica in via dei Velini e i fori di proiettile nel bar di Casette Verdini. Un infarto insieme psicologico e materiale. CERVIA - Sembra una leggenda, la storia raccontata al bagno Paola di Pinarella. "Sì, io l\'ho visto, il venditore di cocco. Diceva "cocco, cocco bello" ma sottovoce". "Guardi, 25 giorni che sono qui e di coccobelli o altri abusivi non ne ho visto uno". Nove chilometri di spiaggia, da Milano Marittima a Tagliata, passando da Cervia e Pinarella. Sembra incredibile: nessun venditore abusivo. Questa mattina i carabinieri di Moncalieri hanno identificato gli autori dell\'aggressione, avvenuta nella notte tra il 29 e il 30 luglio, nei confronti dell\'atleta Daisy Osakue, colpita a un occhio da un uovo lanciato da un\'auto in corsa. Si tratta di tre ragazzi italiani residenti a Vinovo, La Loggia e Moncalieri, che hanno utilizzato una Fiat Doblo\' intestata al padre di uno di essi. Ai carabinieri hanno raccontato di aver lanciato le uova per "goliardia". Sono stati denunciati per lesioni e omissioni di soccorso. Le indagini, coordinate dalla Procura di Torino, erano state avviate subito dopo le prime dichiarazioni fornite dalla ragazza, che rischia di saltare gli  Europei di Berlino. Il veicolo era stato segnalato precedentemente da una donna residente a Moncalieri, che qualche giorno prima era stata vittima di un analogo lancio di uova, per fortuna senza conseguenze. I carabinieri hanno acquisito i filmati delle videocamere di sorveglianza dei vari negozi e di quelle posizionate nelle rotatorie, che hanno permesso di risalire al numero di targa del veicolo. Questa mattina i militari hanno raggiunto l\'abitazione del proprietario dell\'auto a Vinovo. La vettura era parcheggiata sotto casa, con evidenti striature di residui di uova sulla fiancata destra. Il proprietario è stato accompagnato in caserma, dove ha spiegato che la vettura è saltuariamente utilizzata dal figlio 19enne. Quest\'ultimo, alla presenza del suo avvocato, ha ammesso di aver lanciato le di uova, almeno sette in due mesi, fornendo anche i nomi di due amici che in caserma hanno ammesso le loro responsabilità. "L\'informazione "ufficiale" ha occupato pagine di giornali e ore di telegiornali per denunciare l\'emergenza Razzismo, alimentata da quel cattivone di Salvini. E invece... Grazie alle indagini dei Carabinieri, a cui vanno i miei complimenti, sono stati presi i cretini lanciatori di uova di Moncalieri, che erano mossi non da razzismo ma da semplice stupidità". Così il ministro dell\'Interno Matteo Salvini, su Facebook. "Vi aggiungo un dettaglio che ancora non troverete in giro: pare che uno dei \'lanciatori\' sia il figlio di un consigliere comunale del Pd", sostiene il titolare del Viminale. "Avete capito bene. Scommettiamo che i tigì faranno fatica a dare la notizia? Per fortuna c\'è la rete, e per fortuna gli italiani non sono scemi".'},
-    'ja': {'name': 'Japanese',
-           'sample': '１月２３日夕方、東京電力では難しい検討が続いていました。前日からの記録的な大雪の影響で、翌２４日のピーク時の供給余力が、最低限必要とされる３％を大きく割り込み、わずか１％にまで低下するという予測が出たからです。これは政府が「ひっ迫 警報」を発令する基準に相当します。余力が１％しかないと、仮に発電所が１か所停止するだけで、需要が供給を上回る可能性があ ります。そうなると最悪、発電所などでトラブルが起きて、予期せぬ大規模停電が発生しかねず、これを避けるためには、市民生活 への影響が極めて大きい計画停電を迫られるのです。一般的に東京電力のピーク需要は猛暑の夏になります。東日本大震災以降、電 力の不足をきたさないよう、政府と電力各社は夏と冬の需給を事前に予測。冬については１０年に１度の厳しい寒さを想定して、こ の冬、東京電力は最大需要４９１０万ｋＷ、供給力５５３０万ｋＷと、１２．６％の余力が確保できると見込んでいました。しかし 、この冬の寒さは想定を大きく超えて、暖房需要が急増。首都圏などで雪が降り始めた１月２２日にピーク需要が５１０１万ｋＷま で増加するなど、この週だけで、最大需要が５１００万ｋＷを超えた日が３日間もありました。電力を確保せよ！今回の事態につい て、経済産業省は今後、検証することにしています。論点の１つは事前の想定です。冬だけでなく、夏の猛暑など、気候変動の影響 が指摘されている天候の想定をより厳しくすべきではないかという点です。 また、太陽光や風力発電についても、普及を進める一方で、天候に左右されやすいという弱点をどうカバーするかも重要な課題になりそうです。 小売自由化で供給力に不安がないかも課題になりそうです。新たに参入した、いわゆる新電力会社の多くは自前の発電所を持たず、電力会社などが売りに出す電力市場で調達 しています。 しかし、今回は市場価格も高騰。１月２３日に東京電力管内の電力を取り引きする市場では、翌日分の１ｋＷｈ当たりの価格が、通常の３倍に上昇。その後も、売りに出る電力が減ったことで価格が高止まりし、新電力会社の中には、市場では調達で きないというところも出てきたと言うことです。こうした中で重要になるのが、今回も有効だった電力融通ですが、改めて課題が浮 き彫りになっています。特に、東日本と西日本を結ぶ、東京電力と中部電力の間の送電線の容量が１２０万ｋＷに限られていること です。実は、この問題は震災直後から指摘されてきました。経済産業省は、この送電線の容量を２０２０年度に２１０万ｋＷに増強 するなどとしていますが、計画がこのままでよいのかという指摘も出てきそうです。震災と原発事故をきっかけに、小売りの全面自 由化など電力を巡ってはさまざまな制度の見直しが進みました。しかし、震災からまもなく７年を迎えるこの冬の寒さは、安定供給 が確保されているかを、改めて問う形になっています。 電力を安く供給することも重要ですが、そのために各社が安定供給に必要なコストまで削ってはいないのかや、供給責任の在り方などを、今一度、点検すべき時期に来ているのかもしれません。寒さや大雪が 続く今月からは、国のエネルギー政策の方針「エネルギー基本計画」の見直し作業が本格化しますが、この中でも、安定供給という 観点が改めて重要になると思います。'},
-    'ko': {'name': 'Korean',
-           'sample': '지금 라이브예요? 그냥 시작하면 되죠? 안녕하세요. PUNCH입니다. 사일렌토랑 같이 한 거 아시죠? 많이 사랑해주셔서 감사합니다. 그리고 드디어 댓글이 들어오네요. 감사합니다. 어딘지 아시겠어요? 지금 온 곳은 제가 저번에 그때 여러분들이 사랑해주셔서 좋았어요. 그때 커버영상 justin bieber의 \'What do you mean\'을 그래서 지금 녹음실인데 한 번 불러드릴게요. 좋죠? 이거 어떻게 돌려? 감사합니다. 요즘 중학교라서 많이 크는 것 같아요. 그래서 열심히 밥 먹고  살도 좀 찌고 열심히 하겠습니다. 엄마가 해준 밥 먹었어요. 애교 뭐가 있지? 부끄럽다. 요즘 살 찌는 게 너무 힘들어요. 됐어. 좋아. 많이 들으셨겠죠? 여러분들, 그거 많이 들어주세요. 만약에 주변에 아는 분들  있으면 많이 얘기해주세요. 이렇게요? 와이파이가 안 되나요? 아닌가? 안녕하세요. 인도네시아, 안녕하세요. 터키! 재미있다. 처음 해보는 건데 재미있네. 브라질 한 번 가보고 싶다. 되게 궁금하네. 미국말로 어떻게 말하지? 댓글 너무 빨리 올라간다. 여기 구경 시켜줘도 돼요? 여러분 저희 연습실 구경시켜 드릴게요. 여자 화장실이니까 저는 남자니까 못 들어가죠. 그때 언제였죠? 커버 리메이크해서 찍었던 곳입니다. 그런데 재활용 쓰레기를 그렇게 신경 안 쓰죠. 그런데 뭐가 다른지 모르겠어요. 여긴 대박이죠. 여러분도 혹시  피규어 좋아하세요? 피규어. 피규어 좋아하세요? 뒷모습이네. 이게 아이언맨이에요. 캡틴 아메리카. 그리고 지금 밖에 비오고 있어요. 우산 못 가져왔다. 어두워. 머리 자르고 싶다. 그래서 제가 V app을 하는 이유는 여러분들과 오랜만에 V app으로 소통을 하고 싶어서 하게 됐습니다. 그리고 오늘 깜짝 방송이라 아쉽게도 이 정도로  마무리를 해야 될 것 같아요. Brave Girls 누나들 V app  많이 시청해주세요. 저도 시청해주세요. 그러면 마지막으로  댓글을 몇 개 읽을까요? 뽑아서 읽겠습니다. 처음에 시작할 때  짧게 불러보겠습니다. 하나, 둘, 하나, 둘. 녹음실! 노래 만족하셨나요? 그리고 댓글을  네 개만 읽겠습니다. Brave 사운드 너무 좋아. 죄송합니다. 그러면 마지막 딱 하나만 읽겠습니다. 모자 하나 쓰고 시계도 찼어요. 신발. 특히 반지. 그러면 여기까지 하겠고요. 다음에 여러분들과  또 했으면 좋겠습니다. 지금까지 PUNCH였습니다. 안녕. 내가 앞장설게. 여기야. 이거 가라고? 앞으로. 미안한데 여기.. 유겸이 없어. 유겸 없다고. 잠깐만. 들어왔어요. 들어왔어. 게임을 시작하지. 유겸이형. 유겸. 너 진짜. 재미있다. 뭔가 SAW 의 Jigsaw가 된 느낌. 살려주세요. 나의 말을 들으면 죽지는 않을 거야. 진짜 한 대 때리고 싶다. 미션을 시작하지. 미션 합시다. 알파벳 숫자를 찾아서 나열해봐. 다음 제시어는 GOT7이야. T, T, T, T. 찾았어. 아닌가? 벽에 틀이 있는데 거기에 붙이면 돼. 어디? 벽에. 벽에 틀이 있는데 거기 안에 유겸아 목소리 바꿀꺼면 처음부터 끝까지 바꿔. 싫은데? 너 오늘 나가면 끝이야. 빨리 빨리 좀 해. 여기 여기. O, T, 7 어디갔어? 만들었어! 손 잡아 다시. 열어줘! 야 만들었대. 이제 두 번째 게임을 시작하지. 김유겸 너 죽었다! 어떤 단어를 말하면 그걸 표현해 보면 돼. 나 모르겠어 이거. 임재범. 턱 내밀어. 미안해 형. 나도 턱 내밀고 있었어. 성공! 이거 마지막 미션 아니었어? 야 마크형 어디 갔어? 나와서 나 때리지 마라 진짜. 때릴거야. 문 열어주세요. 내가 열어줄 수 있어. 애교 한 번 보여줘. 유겸 오빠. 사랑해요. 뭔데 여기? 나 이따 맞아 죽겠다. 게임을 시작하지. 오케이. 진짜 시끄러워. 마이크로 하니까 진짜 귀 터질 것 같아. 그래. 고릴라 표정을 지어볼게. 진짜 잘한다. 혹시 엽기 사진 찍으시려는 거면 진작에 포기하시는게 좋을 것 같아요. 이미 잘 나왔어요. 그럼 이번에는 영재, Mark 갈게요. 두 명은 특별히 귀여운 거로 할게요. 코코를 따라해 볼게요. 좀 치사하다. 3, 2, 1. 안 귀여워요. 짜증나. 그냥 웃긴거 해야겠다. 좋아 좋아. 뭐야 끝이야. 뭐야? 끝이야? 대답을 해봐. 진짜 웃겨. 꿈에 나올까 봐 무섭다. 이건 악어였어요. 진짜 우리 멍청해 보인다. 감사합니다. 가장 수고 많았던 유겸에게 다시 한 번 박수. 얘한테 뭘 박수를 쳐요? 난 갈게. 고생하셨습니다. 잠시 후 계속됩니다. 야 핑크 후드! 아 힘들어. 지금 BamBam인가 저거? 봤어 봤어. 흔들어 흔들어! 와 힘들다. 오늘은 Mark 형의 Hard Carry를 하러 왔습니다. Mark 형 뭐 썼죠? 나는 수상스포츠. 나는 스킨 스쿠버. 저는 제트 스키. 어떻게 보면 똑같아요. 그래서 Han River에서 웨이크보드 타러 왔어요. 여기 스노우보드 스케이트보드 다 좋아하는 멤버들이에요. 근데 웨이크보드는 또 처음 타네. 누가 제일 잘할 것 같아요? Mark 형. Mark 형 이런 거 잘해요. 난 근데 의외로 니가 잘할 것 같아. 왜 난 안 골라? 생각만. BamBam은 오늘의 구멍. 나 스케이트보드 잘 탔어. 그랬어? 찾았어! 앞에 있어! 안녕하세요~ 아니야, 발레 해야지. 저와 똑같이 하시면 됩니다. 30번? 한 번, 두 번, 세 번! 열 아홉 번! 스무 번! 잠시만! 이거 진짜 어려워. 이거 진짜 이걸 30번 해야 되는 거 맞지? 진짜 알려 주시면 안 돼요? 아니면 우리가 신발 안 신고 돌았잖아. 아니 30번 도는 게 말이 안 돼. 나 이걸로 돈 적이 없어. 너무해! 다른 분은 다 알려 줬죠? 그러면 우리가 말하고 맞는 거면 \'응\' 하시면 돼요. 잠시 카메라를 꺼 볼까요? 좋은 생각이지 않을까요? 안 알려 줄 거예요? 안 알려 줄 거예요? 한 번만 도는 거예요? 저희 같이 얘기를 나눠볼까요? 그렇게 나쁜 사람 아닐 거라고 믿습니다. 안 알려 주시면 너무해요. 완전 똑같이 따라하면 되는 거죠? 안녕하십니까.'},
-    'lt': {'name': 'Lithuanian',
-           'sample': None},
-    'pl': {'name': 'Polish',
-           'sample': None},
-    'pt': {'name': 'Portuguese',
-           'sample': None},
-    'ru': {'name': 'Russian',
-           'sample': 'Мосгорсуд изменил приговор актеру Павлу Устинову, осужденному на 3,5 года колонии общего режима по обвинению в применении насилия к сотруднику Росгвардии на протестной акции в Москве 3 августа, передает корреспондент Би-би-си из зала суда. Реальный срок лишения свободы ему заменили на условный. Согласно пересмотренному вердикту, Устинов приговорен к одному году условно. В ходе рассмотрения апелляции суд согласился изменить категорию преступления, вменяемого обвиняемому, с тяжкого на средней тяжести. При этом жалобы адвокатов, которые просили отменить приговор в связи с невиновностью их подзащитного, суд отклонил - актер все равно признан виновным. Защита осталась недовольна таким исходом и пообещала обжаловать и новое решение суда. С таким же заявлением выступил и сам Устинов. Он сказал, что нормально относится к росгвардии, но считает действия ее сотрудников нарушением и будет добиваться своего оправдания. Заседание началось с просмотра видеозаписей задержания Устинова, которые Мосгорсуд приобщил к делу. В ходе предыдущих процессов судья отказывался изучать видео с места задержания. Согласно выводам экспертизы, подтверждений захвата Устиновым плеча сотрудника Росгвардии найдено не было, сообщила судья на заседании в понедельник. Сотрудник Росгвардии Александр Лягин, проводивший задержание актера, заявил, что Устинов оказывал ему сопротивление, но при этом выступил против того, чтобы наказывать актера реальным тюремным сроком. Адвокат Устинова Анатолий Кучерена подчеркивал, что Устинов не интересуется политикой и имеет безупречную репутацию. "Так сложилось, сложились обстоятельства, что он оказался там, где проходили митинги", - сказал он. "Там не было экстремальной ситуации. Человек не совершил убийство, не наносил телесных повреждений, нож не доставал. Он не совершал действий, которые свидетельствовали о том, что его надо скрутить", - добавил адвокат. Сам Устинов заявил, что уважает действия сотрудников Росгвардии и сам ранее служил там. Он в очередной раз подчеркнул, что не оказывал сопротивления правоохранителям. "В данном случае нелепое стечение обстоятельств - я оказался там, где был какой-то митинг. В моей природе нет насилия. Я ничего не кричал. Как можно напасть на человека вот так?" - сказал Устинов. Он также призвал не наказывать сотрудников Росгвардии за превышение полномочий, отметив, что "не хочет крови". В своем последнем слове актер попросил вынести ему оправдательный приговор и отменить решение суда первой инстанции. 16 сентября Тверской суд Москвы приговорил Устинова к трем годам и шести месяцам лишения свободы по статье о причинении вреда здоровью представителя власти, опасного для его жизни (часть 2 статьи 318 УК РФ). По версии следствия, молодой человек оказывал активное сопротивление задерживающему его омоновцу и вывихнул тому плечо. Сам Устинов вину не признавал и настаивал на том, что не участвовал в акции протеста и просто стоял в стороне с телефоном в руке, когда на накинулась группа росгвардейцев. Адвокаты Устинова утверждали, что на видеозаписях с места задержания отчетливо видно, что он не оказывает сопротивления стражам порядка. Приговор Устинову вызвал широкой общестенный резонанс. На оглашение приговора актеру пришел художественный руководитель театра "Сатирикон" Константин Райкин, десятки звезд российского кино присоединились к флешмобу в соцсетях с требованием освободить Устинова. С осуждением приговора Устинову выступил и секретарь генерального совета "Единой России" Андрей Турчак. 20 сентября Мосгорсуд удовлетворил ходатайство Генпрокуратуры и изменил меру пресечения Устинову с ареста на подписку о невыезде. Приговор Устинову вызвал широкой общестенный резонанс. На оглашение приговора актеру пришел художественный руководитель театра "Сатирикон" Константин Райкин, десятки звезд российского кино присоединились к флешмобу в соцсетях с требованием освободить Устинова. С осуждением приговора Устинову выступил и секретарь генерального совета "Единой России" Андрей Турчак. 20 сентября Мосгорсуд удовлетворил ходатайство Генпрокуратуры и изменил меру пресечения Устинову с ареста на подписку о невыезде. Усталость от прежних конфликтов, нежелание рисковать и добавлять еще один неразрешенный кризис в копилку к имеющимся - главные причины, побудившие Европу и США оставаться до определенного времени в стороне. Расчет оказался неверен, размышляет дипломат: европейцы, помня о муджахеддинах в Афганистане, не торопились помогать умеренной оппозиции, тогда как более радикальные группы поддерживались Катаром, Турцией и Саудовской Аравией. Башару Асаду, с самого начала говорившему о борьбе с террористами, а не оппозиционерами режиму, постепенная радикализация умеренных сил была только на руку. "Война в Сирии началась в неподходящее для Запада время, - объясняет дипломат в интервью Би-би-си. - Ни Европа, ни США не хотели повторения Афганистана, нового Ирака или новой Ливии. Ввязываться в новый конфликт не было никакого желания". Именно эта "усталость от войны" не позволила в августе-сентябре 2013 года администрации Барака Обамы поддержать вооруженную сирийскую оппозицию, пишет Дюкло. Этим, в свою очередь, спустя несколько лет сумел воспользоваться Путин, продолжает дипломат. Поняв, что Европа не будет препятствовать, в 2015 году Россия вмешалась в конфликт на стороне Асада. Путин увидел в Сирии "возможность заработать очки против Запада и отомстить за Ливию", пишет в книге французский посол. В октябре 2011 года Россия не применила право вето при голосовании в Совбезе ООН о начале западной военной операции в Ливии, закончившейся свержением ливийского лидера Муаммара Каддафи. В книге "Долгая сирийская ночь" Дюкло задается вопросом, когда и при каких обстоятельствах вмешательство сторонних сил в чужой конфликт можно считать легитимным?'},
-    'zh': {'name': 'Simplified Chinese',
-           'sample': '临近春节，陕西延川文安驿镇梁家河村的道路两旁挂满了红灯笼。村里的秧歌队、锣鼓队正在抓紧排练，到处是过节的喜庆气氛。67岁的村民刘金莲在窑洞内的灶头架上了油锅，随着锅内的油嗞嗞作响，炸得金黄的年糕不一会儿就堆满了案板。 三年前，也是春节前，刘金莲家来了一位特殊的客人——当年的插队知青、阔别梁家河40年的习近平总书记。总书记带着自己出钱采办的“年货”——饺子粉、 大米、食用油、肉制品，还有春联、年画，来给父老乡亲们拜年。对于梁家河的一草一木、一沟一岔、一峁一梁，习近平总书记都饱含 深情、常常牵挂。这一行，他不仅带去党中央对革命老区人民脱贫致富的关心，更带去带领全国人民一道奔小康的决心。三年过去，梁 家河年年有新貌。当年习近平总书记关心的那片果园，如今山地苹果种植已成为村里的主要产业。随着乡村旅游的兴旺发展，2017年全 村人均纯收入达到20800元，比三年前翻了一番。——在兰州市城关区“虚拟养老院”，习近平总书记细致地询问饭菜价格，端起一个盛满饭菜的餐盘，为杨林太老人送上。——在边境一线阿尔山市，他顶风踏雪登上海拔1000多米的三角山哨所，对年轻战士张伟说：“今天，我和你们一起执勤站岗。”——在革命老区井冈山，家家都忙着准备年货，看到一家正在打糍粑，习近平总书记饶有兴致地加入其中，和村民一同拿起木槌、打起糍粑来。 心里装着人民、讲话贴近人民、奋斗为了人民，人民更将他视如亲人。一次次走村入户，他进门叫一声“老 阿姨”，就像自己家里人一样亲切；盘腿上炕拉着手，就像亲戚串门一样自在；抱起年幼的孩童，眉目间尽是慈爱。一次次关怀慰问，他开米缸、察地窖、摸火墙，关心着百姓生活的点点滴滴；孩子上学远不远、看病有没有保障、上厕所方不方便……他惦念着群众日常的方 方面面。一次次亲切交谈，他和村民一起“算收支账”，为农家小院规划支招，希望乡亲们同党中央一起“撸起袖子加油干，让好日子芝麻开花节节高”。五年间，他的足迹遍布东西南北，把实现人民的幸福感、获得感当作义不容辞的责任，把最暖的牵挂、最实的关怀带到老少边穷地区的村村寨寨。去偏远、贫困地区看望慰问困难群众。新华社《学习进行时》原创品牌栏目“讲习所”今天推出文章，带您回顾 这几年春节前夕总书记都去了哪儿，见了谁。'},
-    'tr': {'name': 'Turkish',
-           'sample': None}}
+    "en": {
+        "name": "English",
+        "sample": "Tell people you’re learning English. They will usually be understanding. You might be surprised at how patient people are when they know you’re still learning English. Ask people to repeat things you don’t understand. Don’t be embarrassed to admit you didn’t understand something. Ask people to correct you. Work is a perfect place to improve your English. Ask to be corrected and you will be learning all day. Speak up. Sometimes when people are not sure of what they’re saying, they mumble or speak quietly. This just makes them harder to understand! Don’t worry about being “right,” just “understood.” It doesn’t matter if your sentence didn’t use perfect grammar, as long as the person you were speaking to understood what you meant. And if you’ve asked them to correct your English, you can try to get it right next time. Be confident and be yourself. You’ll do fine! Below are many simple English sentences you can use in different situations at work. Take a deep breath… and you’re ready for work! “Hi [name], nice to meet you.” Say this to someone you just met for the first time. Repeating their name will help you remember it. “I’m still learning English, so please speak slowly.” This is a polite way to ask someone to slow down when they’re speaking English. “I just started working here. I’m the new [name of your job].” You can tell people you’re new with this sentence. “I’m working in the [name] department. What do you do here?” Many jobs have different departments, which are sections of the jobs that concentrate on one part of the job. For example, the IT (Information Technology) Department works with setting up and fixing the company’s computers. When you introduce yourself, tell the person which department you work for, and ask them for theirs. “See you later/tomorrow.” This is an informal but polite way of saying goodbye to someone. “Good night.” If you are leaving in the evening or late afternoon, you can say “good night” as a way of saying goodbye. Small talk is light conversation. It can be about the weather, food, anything that isn’t too serious. If you’re in the same room as someone, in an elevator together or just standing near each other and you aren’t working, making small talk can open the conversation and form friendships and connections. It also saves you from uncomfortable silences! The weather is always a safe topic for small talk. You can use this line for almost any kind of weather. If it’s a beautiful day you can say “I can’t believe how nice it is today.” Don’t be afraid to ask this question. It’s better to understand everything completely than to think you understand, and then waste time doing something the wrong way. Say this sentence, then repeat what you just heard. This is a great way to show that you were paying attention, and to make sure you understood everything correctly. See how easy these sentences are? Use these simple sentences so you don’t have to worry about how to speak, and you can save your energy for impressing your boss. Every job has its own rules and ways of doing things. Find out what they are so that you can follow them.",
+    },
+    "fr": {
+        "name": "French",
+        "sample": "En effet, alors que 67 départements sont toujours en vigilance orange, les températures les plus élevées doivent être atteintes lundi ou mardi selon les régions, d’après Météo France. La journée du dimanche 5 août a encore été très chaude sur la majeure partie du pays, particulièrement la moitié sud avec 36 °C à Bordeaux, Lyon et Marseille relevés vers 16 heures, ou encore 37 °C à Béziers ou Perpignan vers 15 heures. La moitié nord a un peu moins souffert, avec 30 °C à Paris ou 32 °C à Strasbourg. À peine un répit, avant « une nouvelle remontée dès lundi » des températures, a prévenu l’institut météorologique. D’après Météo France, « le pic de canicule devrait se situer lundi sur les régions du Sud-Ouest, mardi sur celles du Nord-Est, du Centre-Est et du bassin parisien ». Mardi, 36 °C sont ainsi attendus à Paris ou à Lyon. Pour ceux qui ont du mal à dormir ou rafraîchir leur logement, ça ne devrait pas s’arranger. « Les nuits suivantes resteront très chaudes, particulièrement en milieu urbain », indique Météo France. La journée de samedi avait été « la plus chaude en moyenne sur la France depuis la canicule de 2003 », selon François Jobard, prévisionniste à Météo France. Cet épisode sera quoi qu’il arrive dans le « top 3 » des canicules les plus sévères mesurées en France. À compter de la fin d’après-midi mardi, la France devrait mieux respirer. « L’arrivée d’une masse d’air plus maritime, venue de l’Atlantique, jouera un rôle rafraîchissant », a précisé le prévisionniste. On devrait retrouver à partir de jeudi des températures conformes voire légèrement inférieures à la normale saisonnière. La ministre de la Santé Agnès Buzyn a estimé dimanche que cette canicule devrait probablement se traduire par une « surmortalité chez les personnes âgées ». Mais les chiffres ne devraient pas être connus avant plusieurs semaines. Elle s’est cependant félicitée du nombre « relativement faible » de consultations dans les services d’urgence, 3 à 4 % du total seulement étant dues à la canicule. Pour éviter tout problème, il est recommandé de boire régulièrement de l’eau, de manger suffisamment, de se mouiller et de se ventiler, de passer du temps dans des endroits frais, et de prendre des nouvelles de ses proches. La chaleur et le trafic routier des vacanciers ont provoqué une pollution à l’ozone dans de nombreuses régions. En Ile-de-France elle devait se prolonger jusqu’à lundi au moins. L’Est, la vallée du Rhône, les Alpes et la Gironde sont aussi touchés. En Haute-Savoie, le préfet et le maire de Saint-Gervais ont recommandé aux alpinistes de différer leur ascension du Mont-Blanc par le couloir du Goûter en raison du risque important de chutes de pierres dû à la canicule.",
+    },
+    "de": {
+        "name": "German",
+        "sample": "Er hoffe, dass es in der kommenden Woche Klarheit darüber geben werde, ob diese Abkommen klappen. Doch nach einer Viertelstunde warf der Trainer hilflos beide Arme in die Luft und brüllte allerhand wütende Anweisungen aufs Feld.Mit der neuen Funktion zur Selbstkontrolle reagiert Facebook auf öffentliche Kritik und eine Debatte, die seit Monaten tobt. Hervorgegangen war sie aus der 1837 gegründeten „Leipziger Allgemeinen Zeitung“, die von Heinrich Brockhaus verlegt wurde. Dessen Sohn Eduard Brockhaus verlegte das Blatt nicht nur, sondern redigierte es von 1857 bis 1883 auch selbst. Dessen politische Ausrichtung prägte auch die Zeitung: Obwohl der Sozialdemokrat Wilhelm Liebknecht zur Gründungsredaktion gehörte, entwickelte die Zeitung bald ein nationalliberales bis konservatives Profil. Die Gesellschafter ab 1872 gehörten zum Hamburger Freundeskreis um den Reichskanzler Otto von Bismarck, der von Albertus Ohlendorff und seinem Bruder Heinrich angeführt wurde. In der Anfangsphase war die Norddeutsche Bank ein weiterer maßgeblicher Gesellschafter, um den benötigten Kaufpreis in Höhe von 800.000 Mark bereitzustellen.[1] Die Zeitung war immer regierungsnah („Bismarcks Hauspostille“), und wurde zeitweise von einem Reptilienfond des Auswärtigen Amtes direkt finanziert.[2] Bis 1872 führte der bisherige Eigentümer August Braß (1818–1876) die Redaktion, der dann ausschied und die Zeitung verkaufte, da er sich dagegen sträubte, zu „Bismarcks Schreiberling“ degradiert zu werden und jeden Leitartikel der Regierung vorzulegen. Die Auflage der Zeitung unter seinem Nachfolger Emil Pindter (1836–1897) brach um die Hälfte auf 5000 ein. Das Blatt ging hauptsächlich an höhere Beamte und andere Zeitungsredaktionen. 1894/95 übernahm Martin Griesemann die Chefredaktion, der wegen Krankheit oft von Verlagsdirektor Graf Rudolph von Westarp vertreten werden musste. Nach Griesemanns Tod 1897 übernahm Wilhelm Lauser das Ruder, gefolgt von Otto Runge, der das Blatt bis 1917 leitete.1917 erwarb der Verleger Reimar Hobbing das bis dahin „regierungsoffiziöse“ Blatt und sollte ihm im Kaiserreich kurz vor dessen Zusammenbruch ein demokratisches Profil geben mit dem Ziel, eine deutsche „Times“ zu schaffen. Liberale Intellektuelle wie Otto Flake, der kurzzeitig das Feuilleton leitete, konnten für die Mitarbeit gewonnen werden. Daneben arbeiteten bedeutende Historiker wie der damals zweiundzwanzigjährige Egmont Zechlin und Friedrich Meinecke (nach dem Zweiten Weltkrieg Gründungsrektor der FU Berlin) für das Blatt.Nach Ende des Ersten Weltkrieges wurde die „Norddeutsche Allgemeine“ am 12. November 1918 als „DAZ“ in Berlin wiedergegründet, nachdem der Arbeiter- und Soldatenrat unter Felix Stössinger in den ersten zwei Revolutionstagen die Räume der Zeitung besetzt hatte und die Zeitung unter dem Kopf „Internationale“ erschien. Unterstützung bei der Umgestaltung zur „DAZ“ erhielt der neue Eigentümer Reimar Hobbing von Otto Karl Stollberg. In der Zeit des Kaiserreiches und der Weimarer Republik gehörte die DAZ neben dem Berliner Tageblatt, der Vossischen Zeitung und der Frankfurter Zeitung zu den international bekanntesten und renommiertesten Blättern. Deswegen konnte die defizitäre Zeitung überleben. Sie war von der Ausrichtung her konservativ-staatstragender als die drei anderen Zeitungen. Immanuel Kant kritisierte die herkömmlichen Vorstellungen von Glück, da diese bedeuteten, dass jeder den unvorhersehbaren Schwankungen seiner eigenen wechselhaften Triebe, Bedürfnisse, Gewohnheiten und Vorlieben ausgeliefert sei. Auch der Freiheit, der Unsterblichkeit und Gott könne man sich im Wege der Vernunft nicht nähern. Man könne sie allein postulieren. Kant forderte stattdessen, dass sich der Mensch freiwillig den Gesetzen der Moral (kategorischer Imperativ) unterwirft. Dadurch könne ein selbstbestimmtes (autonomes), vernünftiges Leben geführt werden, in dem sich immerhin Zufriedenheit erreichen lasse. „Um der Sinnhaftigkeit der Sittlichkeit willen und um der Sinnhaftigkeit der Welt willen müssen wir Gott und Unsterblichkeit postulieren: Die ethische Bestimmung des Menschen fordert seine Weiterdauer.“Der modernen analytischen Sprachphilosophie, wie sie sich auf Wittgenstein beziehen lässt, schien unklar, welchen Status Aussagen über das, was man tun oder nicht tun soll, haben können. „Die Lösung des Problems des Lebens merkt man am Verschwinden dieses Problems. (Ist nicht dies der Grund, warum Menschen, denen der Sinn des Lebens nach langen Zweifeln klar wurde, warum diese dann nicht sagen können, worin der Sinn bestand?)“ (TLP 6.521)[36] Er konzedierte aber, dass der Mensch im religiösen Glauben einen Sinn finden kann: „An einen Gott glauben, heißt sehen, daß es mit den Tatsachen der Welt noch nicht abgetan ist. An Gott glauben, heißt sehen, daß das Leben einen Sinn hat.“[37] Vor allem Bertrand Russell betonte, dass man dem objektiven Naturprozess keinen Sinn entnehmen könne.[38] Damit geriet die Sinnfrage in einen Raum individueller Entscheidung außerhalb eines strenger zu fassenden philosophischen Projekts. Sie trennt aus dieser Perspektive daher den Bereich des Lebens von den Bereichen, in denen die Philosophie aus ihrer Sicht Antwort zu geben vermag.",
+    },
+    "it": {
+        "name": "Italian",
+        "sample": 'Qui dove tutto è cominciato un sabato mattina di sei mesi fa, dove l\'Italia ha conosciuto e si è consegnata al fantasma sovranista dell\'Uomo Bianco, il tempo si è fermato. Niente fiori, niente targhe a ricordare le vittime della mattanza del "lupo". Piuttosto, ancora i segni lasciati dal gesso della scientifica in via dei Velini e i fori di proiettile nel bar di Casette Verdini. Un infarto insieme psicologico e materiale. CERVIA - Sembra una leggenda, la storia raccontata al bagno Paola di Pinarella. "Sì, io l\'ho visto, il venditore di cocco. Diceva "cocco, cocco bello" ma sottovoce". "Guardi, 25 giorni che sono qui e di coccobelli o altri abusivi non ne ho visto uno". Nove chilometri di spiaggia, da Milano Marittima a Tagliata, passando da Cervia e Pinarella. Sembra incredibile: nessun venditore abusivo. Questa mattina i carabinieri di Moncalieri hanno identificato gli autori dell\'aggressione, avvenuta nella notte tra il 29 e il 30 luglio, nei confronti dell\'atleta Daisy Osakue, colpita a un occhio da un uovo lanciato da un\'auto in corsa. Si tratta di tre ragazzi italiani residenti a Vinovo, La Loggia e Moncalieri, che hanno utilizzato una Fiat Doblo\' intestata al padre di uno di essi. Ai carabinieri hanno raccontato di aver lanciato le uova per "goliardia". Sono stati denunciati per lesioni e omissioni di soccorso. Le indagini, coordinate dalla Procura di Torino, erano state avviate subito dopo le prime dichiarazioni fornite dalla ragazza, che rischia di saltare gli  Europei di Berlino. Il veicolo era stato segnalato precedentemente da una donna residente a Moncalieri, che qualche giorno prima era stata vittima di un analogo lancio di uova, per fortuna senza conseguenze. I carabinieri hanno acquisito i filmati delle videocamere di sorveglianza dei vari negozi e di quelle posizionate nelle rotatorie, che hanno permesso di risalire al numero di targa del veicolo. Questa mattina i militari hanno raggiunto l\'abitazione del proprietario dell\'auto a Vinovo. La vettura era parcheggiata sotto casa, con evidenti striature di residui di uova sulla fiancata destra. Il proprietario è stato accompagnato in caserma, dove ha spiegato che la vettura è saltuariamente utilizzata dal figlio 19enne. Quest\'ultimo, alla presenza del suo avvocato, ha ammesso di aver lanciato le di uova, almeno sette in due mesi, fornendo anche i nomi di due amici che in caserma hanno ammesso le loro responsabilità. "L\'informazione "ufficiale" ha occupato pagine di giornali e ore di telegiornali per denunciare l\'emergenza Razzismo, alimentata da quel cattivone di Salvini. E invece... Grazie alle indagini dei Carabinieri, a cui vanno i miei complimenti, sono stati presi i cretini lanciatori di uova di Moncalieri, che erano mossi non da razzismo ma da semplice stupidità". Così il ministro dell\'Interno Matteo Salvini, su Facebook. "Vi aggiungo un dettaglio che ancora non troverete in giro: pare che uno dei \'lanciatori\' sia il figlio di un consigliere comunale del Pd", sostiene il titolare del Viminale. "Avete capito bene. Scommettiamo che i tigì faranno fatica a dare la notizia? Per fortuna c\'è la rete, e per fortuna gli italiani non sono scemi".',
+    },
+    "ja": {
+        "name": "Japanese",
+        "sample": "１月２３日夕方、東京電力では難しい検討が続いていました。前日からの記録的な大雪の影響で、翌２４日のピーク時の供給余力が、最低限必要とされる３％を大きく割り込み、わずか１％にまで低下するという予測が出たからです。これは政府が「ひっ迫 警報」を発令する基準に相当します。余力が１％しかないと、仮に発電所が１か所停止するだけで、需要が供給を上回る可能性があ ります。そうなると最悪、発電所などでトラブルが起きて、予期せぬ大規模停電が発生しかねず、これを避けるためには、市民生活 への影響が極めて大きい計画停電を迫られるのです。一般的に東京電力のピーク需要は猛暑の夏になります。東日本大震災以降、電 力の不足をきたさないよう、政府と電力各社は夏と冬の需給を事前に予測。冬については１０年に１度の厳しい寒さを想定して、こ の冬、東京電力は最大需要４９１０万ｋＷ、供給力５５３０万ｋＷと、１２．６％の余力が確保できると見込んでいました。しかし 、この冬の寒さは想定を大きく超えて、暖房需要が急増。首都圏などで雪が降り始めた１月２２日にピーク需要が５１０１万ｋＷま で増加するなど、この週だけで、最大需要が５１００万ｋＷを超えた日が３日間もありました。電力を確保せよ！今回の事態につい て、経済産業省は今後、検証することにしています。論点の１つは事前の想定です。冬だけでなく、夏の猛暑など、気候変動の影響 が指摘されている天候の想定をより厳しくすべきではないかという点です。 また、太陽光や風力発電についても、普及を進める一方で、天候に左右されやすいという弱点をどうカバーするかも重要な課題になりそうです。 小売自由化で供給力に不安がないかも課題になりそうです。新たに参入した、いわゆる新電力会社の多くは自前の発電所を持たず、電力会社などが売りに出す電力市場で調達 しています。 しかし、今回は市場価格も高騰。１月２３日に東京電力管内の電力を取り引きする市場では、翌日分の１ｋＷｈ当たりの価格が、通常の３倍に上昇。その後も、売りに出る電力が減ったことで価格が高止まりし、新電力会社の中には、市場では調達で きないというところも出てきたと言うことです。こうした中で重要になるのが、今回も有効だった電力融通ですが、改めて課題が浮 き彫りになっています。特に、東日本と西日本を結ぶ、東京電力と中部電力の間の送電線の容量が１２０万ｋＷに限られていること です。実は、この問題は震災直後から指摘されてきました。経済産業省は、この送電線の容量を２０２０年度に２１０万ｋＷに増強 するなどとしていますが、計画がこのままでよいのかという指摘も出てきそうです。震災と原発事故をきっかけに、小売りの全面自 由化など電力を巡ってはさまざまな制度の見直しが進みました。しかし、震災からまもなく７年を迎えるこの冬の寒さは、安定供給 が確保されているかを、改めて問う形になっています。 電力を安く供給することも重要ですが、そのために各社が安定供給に必要なコストまで削ってはいないのかや、供給責任の在り方などを、今一度、点検すべき時期に来ているのかもしれません。寒さや大雪が 続く今月からは、国のエネルギー政策の方針「エネルギー基本計画」の見直し作業が本格化しますが、この中でも、安定供給という 観点が改めて重要になると思います。",
+    },
+    "ko": {
+        "name": "Korean",
+        "sample": "지금 라이브예요? 그냥 시작하면 되죠? 안녕하세요. PUNCH입니다. 사일렌토랑 같이 한 거 아시죠? 많이 사랑해주셔서 감사합니다. 그리고 드디어 댓글이 들어오네요. 감사합니다. 어딘지 아시겠어요? 지금 온 곳은 제가 저번에 그때 여러분들이 사랑해주셔서 좋았어요. 그때 커버영상 justin bieber의 'What do you mean'을 그래서 지금 녹음실인데 한 번 불러드릴게요. 좋죠? 이거 어떻게 돌려? 감사합니다. 요즘 중학교라서 많이 크는 것 같아요. 그래서 열심히 밥 먹고  살도 좀 찌고 열심히 하겠습니다. 엄마가 해준 밥 먹었어요. 애교 뭐가 있지? 부끄럽다. 요즘 살 찌는 게 너무 힘들어요. 됐어. 좋아. 많이 들으셨겠죠? 여러분들, 그거 많이 들어주세요. 만약에 주변에 아는 분들  있으면 많이 얘기해주세요. 이렇게요? 와이파이가 안 되나요? 아닌가? 안녕하세요. 인도네시아, 안녕하세요. 터키! 재미있다. 처음 해보는 건데 재미있네. 브라질 한 번 가보고 싶다. 되게 궁금하네. 미국말로 어떻게 말하지? 댓글 너무 빨리 올라간다. 여기 구경 시켜줘도 돼요? 여러분 저희 연습실 구경시켜 드릴게요. 여자 화장실이니까 저는 남자니까 못 들어가죠. 그때 언제였죠? 커버 리메이크해서 찍었던 곳입니다. 그런데 재활용 쓰레기를 그렇게 신경 안 쓰죠. 그런데 뭐가 다른지 모르겠어요. 여긴 대박이죠. 여러분도 혹시  피규어 좋아하세요? 피규어. 피규어 좋아하세요? 뒷모습이네. 이게 아이언맨이에요. 캡틴 아메리카. 그리고 지금 밖에 비오고 있어요. 우산 못 가져왔다. 어두워. 머리 자르고 싶다. 그래서 제가 V app을 하는 이유는 여러분들과 오랜만에 V app으로 소통을 하고 싶어서 하게 됐습니다. 그리고 오늘 깜짝 방송이라 아쉽게도 이 정도로  마무리를 해야 될 것 같아요. Brave Girls 누나들 V app  많이 시청해주세요. 저도 시청해주세요. 그러면 마지막으로  댓글을 몇 개 읽을까요? 뽑아서 읽겠습니다. 처음에 시작할 때  짧게 불러보겠습니다. 하나, 둘, 하나, 둘. 녹음실! 노래 만족하셨나요? 그리고 댓글을  네 개만 읽겠습니다. Brave 사운드 너무 좋아. 죄송합니다. 그러면 마지막 딱 하나만 읽겠습니다. 모자 하나 쓰고 시계도 찼어요. 신발. 특히 반지. 그러면 여기까지 하겠고요. 다음에 여러분들과  또 했으면 좋겠습니다. 지금까지 PUNCH였습니다. 안녕. 내가 앞장설게. 여기야. 이거 가라고? 앞으로. 미안한데 여기.. 유겸이 없어. 유겸 없다고. 잠깐만. 들어왔어요. 들어왔어. 게임을 시작하지. 유겸이형. 유겸. 너 진짜. 재미있다. 뭔가 SAW 의 Jigsaw가 된 느낌. 살려주세요. 나의 말을 들으면 죽지는 않을 거야. 진짜 한 대 때리고 싶다. 미션을 시작하지. 미션 합시다. 알파벳 숫자를 찾아서 나열해봐. 다음 제시어는 GOT7이야. T, T, T, T. 찾았어. 아닌가? 벽에 틀이 있는데 거기에 붙이면 돼. 어디? 벽에. 벽에 틀이 있는데 거기 안에 유겸아 목소리 바꿀꺼면 처음부터 끝까지 바꿔. 싫은데? 너 오늘 나가면 끝이야. 빨리 빨리 좀 해. 여기 여기. O, T, 7 어디갔어? 만들었어! 손 잡아 다시. 열어줘! 야 만들었대. 이제 두 번째 게임을 시작하지. 김유겸 너 죽었다! 어떤 단어를 말하면 그걸 표현해 보면 돼. 나 모르겠어 이거. 임재범. 턱 내밀어. 미안해 형. 나도 턱 내밀고 있었어. 성공! 이거 마지막 미션 아니었어? 야 마크형 어디 갔어? 나와서 나 때리지 마라 진짜. 때릴거야. 문 열어주세요. 내가 열어줄 수 있어. 애교 한 번 보여줘. 유겸 오빠. 사랑해요. 뭔데 여기? 나 이따 맞아 죽겠다. 게임을 시작하지. 오케이. 진짜 시끄러워. 마이크로 하니까 진짜 귀 터질 것 같아. 그래. 고릴라 표정을 지어볼게. 진짜 잘한다. 혹시 엽기 사진 찍으시려는 거면 진작에 포기하시는게 좋을 것 같아요. 이미 잘 나왔어요. 그럼 이번에는 영재, Mark 갈게요. 두 명은 특별히 귀여운 거로 할게요. 코코를 따라해 볼게요. 좀 치사하다. 3, 2, 1. 안 귀여워요. 짜증나. 그냥 웃긴거 해야겠다. 좋아 좋아. 뭐야 끝이야. 뭐야? 끝이야? 대답을 해봐. 진짜 웃겨. 꿈에 나올까 봐 무섭다. 이건 악어였어요. 진짜 우리 멍청해 보인다. 감사합니다. 가장 수고 많았던 유겸에게 다시 한 번 박수. 얘한테 뭘 박수를 쳐요? 난 갈게. 고생하셨습니다. 잠시 후 계속됩니다. 야 핑크 후드! 아 힘들어. 지금 BamBam인가 저거? 봤어 봤어. 흔들어 흔들어! 와 힘들다. 오늘은 Mark 형의 Hard Carry를 하러 왔습니다. Mark 형 뭐 썼죠? 나는 수상스포츠. 나는 스킨 스쿠버. 저는 제트 스키. 어떻게 보면 똑같아요. 그래서 Han River에서 웨이크보드 타러 왔어요. 여기 스노우보드 스케이트보드 다 좋아하는 멤버들이에요. 근데 웨이크보드는 또 처음 타네. 누가 제일 잘할 것 같아요? Mark 형. Mark 형 이런 거 잘해요. 난 근데 의외로 니가 잘할 것 같아. 왜 난 안 골라? 생각만. BamBam은 오늘의 구멍. 나 스케이트보드 잘 탔어. 그랬어? 찾았어! 앞에 있어! 안녕하세요~ 아니야, 발레 해야지. 저와 똑같이 하시면 됩니다. 30번? 한 번, 두 번, 세 번! 열 아홉 번! 스무 번! 잠시만! 이거 진짜 어려워. 이거 진짜 이걸 30번 해야 되는 거 맞지? 진짜 알려 주시면 안 돼요? 아니면 우리가 신발 안 신고 돌았잖아. 아니 30번 도는 게 말이 안 돼. 나 이걸로 돈 적이 없어. 너무해! 다른 분은 다 알려 줬죠? 그러면 우리가 말하고 맞는 거면 '응' 하시면 돼요. 잠시 카메라를 꺼 볼까요? 좋은 생각이지 않을까요? 안 알려 줄 거예요? 안 알려 줄 거예요? 한 번만 도는 거예요? 저희 같이 얘기를 나눠볼까요? 그렇게 나쁜 사람 아닐 거라고 믿습니다. 안 알려 주시면 너무해요. 완전 똑같이 따라하면 되는 거죠? 안녕하십니까.",
+    },
+    "lt": {"name": "Lithuanian", "sample": None},
+    "pl": {"name": "Polish", "sample": None},
+    "pt": {"name": "Portuguese", "sample": None},
+    "ru": {
+        "name": "Russian",
+        "sample": 'Мосгорсуд изменил приговор актеру Павлу Устинову, осужденному на 3,5 года колонии общего режима по обвинению в применении насилия к сотруднику Росгвардии на протестной акции в Москве 3 августа, передает корреспондент Би-би-си из зала суда. Реальный срок лишения свободы ему заменили на условный. Согласно пересмотренному вердикту, Устинов приговорен к одному году условно. В ходе рассмотрения апелляции суд согласился изменить категорию преступления, вменяемого обвиняемому, с тяжкого на средней тяжести. При этом жалобы адвокатов, которые просили отменить приговор в связи с невиновностью их подзащитного, суд отклонил - актер все равно признан виновным. Защита осталась недовольна таким исходом и пообещала обжаловать и новое решение суда. С таким же заявлением выступил и сам Устинов. Он сказал, что нормально относится к росгвардии, но считает действия ее сотрудников нарушением и будет добиваться своего оправдания. Заседание началось с просмотра видеозаписей задержания Устинова, которые Мосгорсуд приобщил к делу. В ходе предыдущих процессов судья отказывался изучать видео с места задержания. Согласно выводам экспертизы, подтверждений захвата Устиновым плеча сотрудника Росгвардии найдено не было, сообщила судья на заседании в понедельник. Сотрудник Росгвардии Александр Лягин, проводивший задержание актера, заявил, что Устинов оказывал ему сопротивление, но при этом выступил против того, чтобы наказывать актера реальным тюремным сроком. Адвокат Устинова Анатолий Кучерена подчеркивал, что Устинов не интересуется политикой и имеет безупречную репутацию. "Так сложилось, сложились обстоятельства, что он оказался там, где проходили митинги", - сказал он. "Там не было экстремальной ситуации. Человек не совершил убийство, не наносил телесных повреждений, нож не доставал. Он не совершал действий, которые свидетельствовали о том, что его надо скрутить", - добавил адвокат. Сам Устинов заявил, что уважает действия сотрудников Росгвардии и сам ранее служил там. Он в очередной раз подчеркнул, что не оказывал сопротивления правоохранителям. "В данном случае нелепое стечение обстоятельств - я оказался там, где был какой-то митинг. В моей природе нет насилия. Я ничего не кричал. Как можно напасть на человека вот так?" - сказал Устинов. Он также призвал не наказывать сотрудников Росгвардии за превышение полномочий, отметив, что "не хочет крови". В своем последнем слове актер попросил вынести ему оправдательный приговор и отменить решение суда первой инстанции. 16 сентября Тверской суд Москвы приговорил Устинова к трем годам и шести месяцам лишения свободы по статье о причинении вреда здоровью представителя власти, опасного для его жизни (часть 2 статьи 318 УК РФ). По версии следствия, молодой человек оказывал активное сопротивление задерживающему его омоновцу и вывихнул тому плечо. Сам Устинов вину не признавал и настаивал на том, что не участвовал в акции протеста и просто стоял в стороне с телефоном в руке, когда на накинулась группа росгвардейцев. Адвокаты Устинова утверждали, что на видеозаписях с места задержания отчетливо видно, что он не оказывает сопротивления стражам порядка. Приговор Устинову вызвал широкой общестенный резонанс. На оглашение приговора актеру пришел художественный руководитель театра "Сатирикон" Константин Райкин, десятки звезд российского кино присоединились к флешмобу в соцсетях с требованием освободить Устинова. С осуждением приговора Устинову выступил и секретарь генерального совета "Единой России" Андрей Турчак. 20 сентября Мосгорсуд удовлетворил ходатайство Генпрокуратуры и изменил меру пресечения Устинову с ареста на подписку о невыезде. Приговор Устинову вызвал широкой общестенный резонанс. На оглашение приговора актеру пришел художественный руководитель театра "Сатирикон" Константин Райкин, десятки звезд российского кино присоединились к флешмобу в соцсетях с требованием освободить Устинова. С осуждением приговора Устинову выступил и секретарь генерального совета "Единой России" Андрей Турчак. 20 сентября Мосгорсуд удовлетворил ходатайство Генпрокуратуры и изменил меру пресечения Устинову с ареста на подписку о невыезде. Усталость от прежних конфликтов, нежелание рисковать и добавлять еще один неразрешенный кризис в копилку к имеющимся - главные причины, побудившие Европу и США оставаться до определенного времени в стороне. Расчет оказался неверен, размышляет дипломат: европейцы, помня о муджахеддинах в Афганистане, не торопились помогать умеренной оппозиции, тогда как более радикальные группы поддерживались Катаром, Турцией и Саудовской Аравией. Башару Асаду, с самого начала говорившему о борьбе с террористами, а не оппозиционерами режиму, постепенная радикализация умеренных сил была только на руку. "Война в Сирии началась в неподходящее для Запада время, - объясняет дипломат в интервью Би-би-си. - Ни Европа, ни США не хотели повторения Афганистана, нового Ирака или новой Ливии. Ввязываться в новый конфликт не было никакого желания". Именно эта "усталость от войны" не позволила в августе-сентябре 2013 года администрации Барака Обамы поддержать вооруженную сирийскую оппозицию, пишет Дюкло. Этим, в свою очередь, спустя несколько лет сумел воспользоваться Путин, продолжает дипломат. Поняв, что Европа не будет препятствовать, в 2015 году Россия вмешалась в конфликт на стороне Асада. Путин увидел в Сирии "возможность заработать очки против Запада и отомстить за Ливию", пишет в книге французский посол. В октябре 2011 года Россия не применила право вето при голосовании в Совбезе ООН о начале западной военной операции в Ливии, закончившейся свержением ливийского лидера Муаммара Каддафи. В книге "Долгая сирийская ночь" Дюкло задается вопросом, когда и при каких обстоятельствах вмешательство сторонних сил в чужой конфликт можно считать легитимным?',
+    },
+    "zh": {
+        "name": "Simplified Chinese",
+        "sample": "临近春节，陕西延川文安驿镇梁家河村的道路两旁挂满了红灯笼。村里的秧歌队、锣鼓队正在抓紧排练，到处是过节的喜庆气氛。67岁的村民刘金莲在窑洞内的灶头架上了油锅，随着锅内的油嗞嗞作响，炸得金黄的年糕不一会儿就堆满了案板。 三年前，也是春节前，刘金莲家来了一位特殊的客人——当年的插队知青、阔别梁家河40年的习近平总书记。总书记带着自己出钱采办的“年货”——饺子粉、 大米、食用油、肉制品，还有春联、年画，来给父老乡亲们拜年。对于梁家河的一草一木、一沟一岔、一峁一梁，习近平总书记都饱含 深情、常常牵挂。这一行，他不仅带去党中央对革命老区人民脱贫致富的关心，更带去带领全国人民一道奔小康的决心。三年过去，梁 家河年年有新貌。当年习近平总书记关心的那片果园，如今山地苹果种植已成为村里的主要产业。随着乡村旅游的兴旺发展，2017年全 村人均纯收入达到20800元，比三年前翻了一番。——在兰州市城关区“虚拟养老院”，习近平总书记细致地询问饭菜价格，端起一个盛满饭菜的餐盘，为杨林太老人送上。——在边境一线阿尔山市，他顶风踏雪登上海拔1000多米的三角山哨所，对年轻战士张伟说：“今天，我和你们一起执勤站岗。”——在革命老区井冈山，家家都忙着准备年货，看到一家正在打糍粑，习近平总书记饶有兴致地加入其中，和村民一同拿起木槌、打起糍粑来。 心里装着人民、讲话贴近人民、奋斗为了人民，人民更将他视如亲人。一次次走村入户，他进门叫一声“老 阿姨”，就像自己家里人一样亲切；盘腿上炕拉着手，就像亲戚串门一样自在；抱起年幼的孩童，眉目间尽是慈爱。一次次关怀慰问，他开米缸、察地窖、摸火墙，关心着百姓生活的点点滴滴；孩子上学远不远、看病有没有保障、上厕所方不方便……他惦念着群众日常的方 方面面。一次次亲切交谈，他和村民一起“算收支账”，为农家小院规划支招，希望乡亲们同党中央一起“撸起袖子加油干，让好日子芝麻开花节节高”。五年间，他的足迹遍布东西南北，把实现人民的幸福感、获得感当作义不容辞的责任，把最暖的牵挂、最实的关怀带到老少边穷地区的村村寨寨。去偏远、贫困地区看望慰问困难群众。新华社《学习进行时》原创品牌栏目“讲习所”今天推出文章，带您回顾 这几年春节前夕总书记都去了哪儿，见了谁。",
+    },
+    "tr": {"name": "Turkish", "sample": None},
+}
 
 
 def build_unordered_list(items: Iterable[str]) -> str:
-    escaped_items = [item.replace(' ', '&nbsp;') for item in items]
-    return '\n'.join(f'- {item}' for item in escaped_items)
+    escaped_items = [item.replace(" ", "&nbsp;") for item in items]
+    return "\n".join(f"- {item}" for item in escaped_items)
 
 
 def main():
-    st.title('Sentsplit')
-    st.markdown('[![Star](https://img.shields.io/github/stars/zaemyung/sentsplit?style=social)](https://github.com/zaemyung/sentsplit/stargazers)')
+    st.title("Sentsplit")
+    st.markdown(
+        "[![Star](https://img.shields.io/github/stars/zaemyung/sentsplit?style=social)](https://github.com/zaemyung/sentsplit/stargazers)"
+    )
 
     with st.sidebar:
-        st.header('Configuration')
-        st.subheader('Language')
-        language_code = st.selectbox('',
-                                    list(supported_languages.keys()),
-                                    format_func=lambda code: supported_languages[code]['name'])
+        st.header("Configuration")
+        st.subheader("Language")
+        language_code = st.selectbox(
+            "",
+            list(supported_languages.keys()),
+            format_func=lambda code: supported_languages[code]["name"],
+        )
         language = supported_languages[language_code]
 
-        st.subheader('Parameters')
-        config = getattr(sentsplit.config, f'{language_code}_config')
-        mincut = st.number_input(label='mincut',
-                                 min_value=0,
-                                 value=config['mincut'])
-        maxcut = st.number_input(label='maxcut',
-                                 min_value=0,
-                                 value=config['maxcut'])
-        strip_spaces = st.radio(label='strip_spaces',
-                                options=[False, True],
-                                format_func=lambda item: str(item))
-        handle_multiple_spaces = st.radio(label='handle_multiple_spaces',
-                                          options=[False, True],
-                                          index=1,
-                                          format_func=lambda item: str(item))
+        st.subheader("Parameters")
+        config = getattr(sentsplit.config, f"{language_code}_config")
+        mincut = st.number_input(label="mincut", min_value=0, value=config["mincut"])
+        maxcut = st.number_input(label="maxcut", min_value=0, value=config["maxcut"])
+        strip_spaces = st.radio(
+            label="strip_spaces",
+            options=[False, True],
+            format_func=lambda item: str(item),
+        )
+        handle_multiple_spaces = st.radio(
+            label="handle_multiple_spaces",
+            options=[False, True],
+            index=1,
+            format_func=lambda item: str(item),
+        )
 
     my_config = deepcopy(config)
-    my_config['mincut'] = mincut
-    my_config['maxcut'] = maxcut
-    my_config['strip_spaces'] = strip_spaces
-    my_config['handle_multiple_spaces'] = handle_multiple_spaces
+    my_config["mincut"] = mincut
+    my_config["maxcut"] = maxcut
+    my_config["strip_spaces"] = strip_spaces
+    my_config["handle_multiple_spaces"] = handle_multiple_spaces
     sent_splitter = SentSplit(language_code, **my_config)
 
-    text = st.text_area(label='Text to split sentences',
-                        value=language['sample'] if language['sample'] is not None else '',
-                        height=200)
+    text = st.text_area(
+        label="Text to split sentences",
+        value=language["sample"] if language["sample"] is not None else "",
+        height=200,
+    )
     if not text:
-        st.info('Type a text to split sentences in the text area above and press Ctrl+Enter!')
+        st.info("Type a text to split sentences in the text area above and press Ctrl+Enter!")
         st.stop()
 
-    lines = [line for line in text.split('\n') if line.strip()]
+    lines = [line for line in text.split("\n") if line.strip()]
     sentences = sent_splitter.segment(lines)
     st.markdown(build_unordered_list(itertools.chain(*sentences)))
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     main()


Test patch:
diff --git a/tests/test_options.py b/tests/test_options.py
new file mode 100644
index 0000000..26e37e9
--- /dev/null
+++ b/tests/test_options.py
@@ -0,0 +1,64 @@
+from copy import deepcopy
+
+from sentsplit.config import en_config
+from sentsplit.segment import SentSplit
+
+
+def test_default():
+    splitter = SentSplit("en")
+    assert splitter.segment("Hello world. This is a sentence.") == ["Hello world.", " This is a sentence."]
+
+
+def test_mincut():
+    splitter = SentSplit("en", mincut=13)
+    assert splitter.segment("Hello world. This is a sentence.") == ["Hello world. This is a sentence."]
+
+
+def test_maxcut():
+    splitter = SentSplit("en", maxcut=13)
+    assert splitter.segment("Hello world. This is a sentence.") == ["Hello world.", " This is a se", "ntence."]
+
+
+def test_strip_spaces():
+    splitter = SentSplit("en")
+    assert splitter.segment("Hello world.  ") == ["Hello world.  "]
+    assert splitter.segment("  Hello world.") == ["  Hello world."]
+    assert splitter.segment("  Hello world.  ") == ["  Hello world.  "]
+
+    splitter = SentSplit("en", strip_spaces=True)
+    assert splitter.segment("Hello world.  ") == ["Hello world."]
+    assert splitter.segment("  Hello world.") == ["Hello world."]
+    assert splitter.segment("  Hello world.  ") == ["Hello world."]
+
+
+def test_handle_multiple_spaces():
+    splitter = SentSplit("en")
+    assert splitter.segment("Hello world.  This is a sentence.") == ["Hello world.", "  This is a sentence."]
+
+    splitter = SentSplit("en", handle_multiple_spaces=False)
+    assert splitter.segment("Hello world.  This is a sentence.") == ["Hello world.  This is a sentence."]
+
+
+def test_segment_regexes():
+    splitter = SentSplit("en")
+    assert splitter.segment("Hello world~ This is a sentence.") == ["Hello world~ This is a sentence."]
+
+    config = deepcopy(en_config)
+    config["segment_regexes"].append({"name": "tilde_ending", "regex": r"~+", "at": "end"})
+    splitter = SentSplit("en", **config)
+    assert splitter.segment("Hello world~ This is a sentence.") == ["Hello world~", " This is a sentence."]
+
+
+def test_prevent_regexes():
+    splitter = SentSplit("en")
+    assert splitter.segment('"Hello world. This is a sentence."') == ['"Hello world.', ' This is a sentence."']
+
+    config = deepcopy(en_config)
+    config["prevent_regexes"].append({"name": "period_inside_quote", "regex": r'\.(?= *[^"]+")'})
+    splitter = SentSplit("en", **config)
+    assert splitter.segment('"Hello world. This is a sentence."') == ['"Hello world. This is a sentence."']
+
+
+# TODO: Add test_prevent_word_split()
+# def test_prevent_word_split():
+#     pass
diff --git a/tests/test_segment.py b/tests/test_segment.py
new file mode 100644
index 0000000..d103eaa
--- /dev/null
+++ b/tests/test_segment.py
@@ -0,0 +1,121 @@
+from sentsplit.segment import SentSplit
+
+
+def test_segment_en():
+    splitter = SentSplit("en")
+
+    assert splitter.segment("This is a test sentence. And here is another one.") == [
+        "This is a test sentence.",
+        " And here is another one.",
+    ]
+
+
+def test_segment_fr():
+    splitter = SentSplit("fr")
+
+    assert splitter.segment("Ceci est une phrase de test. Et voici une autre.") == [
+        "Ceci est une phrase de test.",
+        " Et voici une autre.",
+    ]
+
+
+def test_segment_de():
+    splitter = SentSplit("de")
+
+    assert splitter.segment("Dies ist ein Test-Satz. Und hier ist noch einer.") == [
+        "Dies ist ein Test-Satz.",
+        " Und hier ist noch einer.",
+    ]
+
+
+def test_segment_it():
+    splitter = SentSplit("it")
+
+    assert splitter.segment("Questo è una frase di prova. Ed ecco un'altra.") == [
+        "Questo è una frase di prova.",
+        " Ed ecco un'altra.",
+    ]
+
+
+def test_segment_ja():
+    splitter = SentSplit("ja")
+
+    assert splitter.segment("これはテストの文です。 ここにもう一つあります。") == [
+        "これはテストの文です。",
+        " ここにもう一つあります。",
+    ]
+
+
+def test_segment_ko():
+    splitter = SentSplit("ko")
+
+    assert splitter.segment("이것은 테스트 문장입니다. 여기에 하나 더 있습니다.") == [
+        "이것은 테스트 문장입니다.",
+        " 여기에 하나 더 있습니다.",
+    ]
+
+
+def test_segment_lt():
+    splitter = SentSplit("lt")
+
+    assert splitter.segment("Tai yra bandymo sakinys. Ir čia yra dar vienas.") == [
+        "Tai yra bandymo sakinys.",
+        " Ir čia yra dar vienas.",
+    ]
+
+
+def test_segment_pl():
+    splitter = SentSplit("pl")
+
+    assert splitter.segment("To jest zdanie testowe. A oto kolejne.") == [
+        "To jest zdanie testowe.",
+        " A oto kolejne.",
+    ]
+
+
+def test_segment_pt():
+    splitter = SentSplit("pt")
+
+    assert splitter.segment("Esta é uma frase de teste. E aqui está mais uma.") == [
+        "Esta é uma frase de teste.",
+        " E aqui está mais uma.",
+    ]
+
+
+def test_segment_ru():
+    splitter = SentSplit("ru")
+
+    assert splitter.segment("Это тестовое предложение. И вот еще одно.") == [
+        "Это тестовое предложение.",
+        " И вот еще одно.",
+    ]
+
+
+def test_segment_zh():
+    splitter = SentSplit("zh")
+
+    assert splitter.segment("这是一个测试句子。 这里还有另一个。") == [
+        "这是一个测试句子。",
+        " 这里还有另一个。",
+    ]
+
+
+def test_segment_tr():
+    splitter = SentSplit("tr")
+
+    assert splitter.segment("Bu bir test cümlesi. İşte başka bir tane.") == [
+        "Bu bir test cümlesi.",
+        " İşte başka bir tane.",
+    ]
+
+
+def test_segment_edge_cases():
+    splitter = SentSplit("en")
+
+    assert splitter.segment("This\n") == [
+        "This\n"
+    ]
+
+    assert splitter.segment("This is a test sentence.\n\n And here's another one.\n") == [
+        "This is a test sentence.\n", "\n", " And here's another one.\n"
+    ]