AtCoderStudyBooster 0.4.0__py3-none-any.whl → 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
atcdr/test.py CHANGED
@@ -26,6 +26,7 @@ from atcdr.util.filetype import (
26
26
  detect_language,
27
27
  lang2str,
28
28
  )
29
+ from atcdr.util.i18n import _
29
30
  from atcdr.util.parse import ProblemHTML
30
31
 
31
32
 
@@ -159,7 +160,7 @@ class TestRunner:
159
160
  ]
160
161
  self.exe = None
161
162
  else:
162
- raise ValueError(f'{lang}の適切な言語のランナーが見つかりませんでした.')
163
+ raise ValueError(_('runner_not_found', lang))
163
164
 
164
165
  return self
165
166
 
@@ -300,9 +301,9 @@ def create_renderable_test_info(
300
301
  status_text = STATUS_TEXT_MAP[test_info.summary]
301
302
 
302
303
  header_text = Text.assemble(
303
- Text.from_markup(f'[cyan]{test_info.sourcename}[/]のテスト \n'),
304
+ Text.from_markup(f'[cyan]{test_info.sourcename}[/]' + _('test_of', '') + '\n'),
304
305
  Text.from_markup(
305
- f'[italic #0f0f0f]コンパイルにかかった時間: [not italic cyan]{test_info.compile_time}[/] ms[/]\n'
306
+ '[italic #0f0f0f]' + _('compile_time', test_info.compile_time) + '\n'
306
307
  )
307
308
  if test_info.compile_time
308
309
  else Text(''),
@@ -319,7 +320,7 @@ def create_renderable_test_info(
319
320
 
320
321
  if test_info.compiler_message:
321
322
  rule = Rule(
322
- title='コンパイラーのメッセージ',
323
+ title=_('compiler_message'),
323
324
  style=COLOR_MAP[ResultStatus.CE],
324
325
  )
325
326
  components.append(rule)
@@ -342,27 +343,29 @@ def create_renderable_test_result(
342
343
 
343
344
  # 以下の部分は if-else ブロックの外に移動
344
345
  status_header = Text.assemble(
345
- 'ステータス ',
346
+ _('status'),
346
347
  STATUS_TEXT_MAP[test_result.result.passed], # status_text をここに追加
347
348
  )
348
349
 
349
350
  execution_time_text = None
350
351
  if test_result.result.executed_time is not None:
351
352
  execution_time_text = Text.from_markup(
352
- f'実行時間 [cyan]{test_result.result.executed_time}[/cyan] ms'
353
+ _('execution_time', test_result.result.executed_time)
353
354
  )
354
355
 
355
356
  table = Table(show_header=True, header_style='bold')
356
- table.add_column('入力', style='cyan', min_width=10)
357
+ table.add_column(_('input'), style='cyan', min_width=10)
357
358
 
358
359
  if test_result.result.passed != ResultStatus.AC:
359
360
  table.add_column(
360
- '出力',
361
+ _('output'),
361
362
  style=COLOR_MAP[test_result.result.passed],
362
363
  min_width=10,
363
364
  overflow='fold',
364
365
  )
365
- table.add_column('正解の出力', style=COLOR_MAP[ResultStatus.AC], min_width=10)
366
+ table.add_column(
367
+ _('expected_output'), style=COLOR_MAP[ResultStatus.AC], min_width=10
368
+ )
366
369
  table.add_row(
367
370
  escape(test_result.testcase.input),
368
371
  escape(test_result.result.output),
@@ -370,7 +373,7 @@ def create_renderable_test_result(
370
373
  )
371
374
  else:
372
375
  table.add_column(
373
- '出力', style=COLOR_MAP[test_result.result.passed], min_width=10
376
+ _('output'), style=COLOR_MAP[test_result.result.passed], min_width=10
374
377
  )
375
378
  table.add_row(
376
379
  escape(test_result.testcase.input), escape(test_result.result.output)
@@ -393,7 +396,9 @@ def render_results(test: TestRunner) -> None:
393
396
  SpinnerColumn(style='white', spinner_name='simpleDots'),
394
397
  BarColumn(),
395
398
  )
396
- task_id = progress.add_task(description='テスト進行中', total=test.info.case_number)
399
+ task_id = progress.add_task(
400
+ description=_('test_in_progress'), total=test.info.case_number
401
+ )
397
402
 
398
403
  current_display = [create_renderable_test_info(test.info, progress)]
399
404
 
@@ -404,7 +409,9 @@ def render_results(test: TestRunner) -> None:
404
409
  current_display.insert(-1, (create_renderable_test_result(i, result)))
405
410
  live.update(Group(*current_display))
406
411
 
407
- progress.update(task_id, description='テスト完了') # 完了メッセージに更新
412
+ progress.update(
413
+ task_id, description=_('test_completed')
414
+ ) # 完了メッセージに更新
408
415
  current_display[-1] = create_renderable_test_info(test.info, progress)
409
416
  live.update(Group(*current_display))
410
417
 
@@ -412,9 +419,7 @@ def render_results(test: TestRunner) -> None:
412
419
  def run_test(path_of_code: str) -> None:
413
420
  html_paths = [f for f in os.listdir('.') if f.endswith('.html')]
414
421
  if not html_paths:
415
- print(
416
- '問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。'
417
- )
422
+ print(_('problem_file_not_found'))
418
423
  return
419
424
 
420
425
  with open(html_paths[0], 'r') as file:
@@ -425,7 +430,7 @@ def run_test(path_of_code: str) -> None:
425
430
  render_results(test)
426
431
 
427
432
 
428
- @click.command(short_help='テストを実行')
433
+ @click.command(short_help=_('cmd_test'), help=_('cmd_test'))
429
434
  @add_file_selector('files', filetypes=COMPILED_LANGUAGES + INTERPRETED_LANGUAGES)
430
435
  def test(files):
431
436
  """指定したソースコードをサンプルケースでテストします。"""
atcdr/util/fileops.py CHANGED
@@ -7,6 +7,7 @@ import questionary as q
7
7
  import rich_click as click
8
8
 
9
9
  from atcdr.util.filetype import FILE_EXTENSIONS, Lang
10
+ from atcdr.util.i18n import _
10
11
 
11
12
 
12
13
  def collect_files(
@@ -41,9 +42,9 @@ def collect_files(
41
42
 
42
43
  def select_files_interactively(files: List[str]) -> List[str]:
43
44
  target_file = q.select(
44
- message='複数のファイルが見つかりました.ファイルを選択してください:',
45
+ message=_('multiple_files_found'),
45
46
  choices=[q.Choice(title=file, value=file) for file in files],
46
- instruction='\n 十字キーで移動, [enter]で実行',
47
+ instruction='\n ' + _('navigate_with_arrows'),
47
48
  pointer='❯',
48
49
  qmark='',
49
50
  style=q.Style(
@@ -78,7 +79,7 @@ def add_file_selector(
78
79
  # 2) ファイル収集 (非再帰固定)
79
80
  files = collect_files(patterns, tuple(exts), recursive=False)
80
81
  if not files:
81
- click.echo('対象ファイルが見つかりません。')
82
+ click.echo(_('target_file_not_found'))
82
83
  ctx.exit(1)
83
84
 
84
85
  # 3) 候補が1つなら即実行
@@ -89,7 +90,7 @@ def add_file_selector(
89
90
  if not patterns:
90
91
  selected = select_files_interactively(files)
91
92
  if not selected:
92
- click.echo('ファイルが選択されませんでした。')
93
+ click.echo(_('file_not_selected'))
93
94
  ctx.exit(1)
94
95
  selected_list = [selected]
95
96
  return ctx.invoke(f, **{arg_name: selected_list}, **kwargs)
atcdr/util/gpt.py CHANGED
@@ -4,6 +4,8 @@ from typing import Dict, List, Optional
4
4
 
5
5
  import requests
6
6
 
7
+ from atcdr.util.i18n import _
8
+
7
9
 
8
10
  class Model(Enum):
9
11
  GPT4O = 'gpt-4o'
@@ -24,27 +26,23 @@ def set_api_key() -> Optional[str]:
24
26
  if api_key and validate_api_key(api_key):
25
27
  return api_key
26
28
  elif api_key:
27
- print('環境変数に設定されているAPIキーの検証に失敗しました ')
29
+ print(_('api_key_validation_failed'))
28
30
  else:
29
31
  pass
30
32
 
31
- api_key = input(
32
- 'https://platform.openai.com/api-keys からchatGPTのAPIキーを入手しましょう。\nAPIキー入力してください: '
33
- )
33
+ api_key = input(_('get_api_key_prompt'))
34
34
  if validate_api_key(api_key):
35
- print('APIキーのテストに成功しました。')
36
- print('以下, ~/.zshrcにAPIキーを保存しますか? [y/n]')
35
+ print(_('api_key_test_success'))
36
+ print(_('save_api_key_prompt'))
37
37
  if input() == 'y':
38
38
  zshrc_path = os.path.expanduser('~/.zshrc')
39
39
  with open(zshrc_path, 'a') as f:
40
40
  f.write(f'export OPENAI_API_KEY={api_key}\n')
41
- print(
42
- f'APIキーを {zshrc_path} に保存しました。次回シェル起動時に読み込まれます。'
43
- )
41
+ print(_('api_key_saved', zshrc_path))
44
42
  os.environ['OPENAI_API_KEY'] = api_key
45
43
  return api_key
46
44
  else:
47
- print('コード生成にはAPIキーが必要です。')
45
+ print(_('api_key_required'))
48
46
  return None
49
47
 
50
48
 
@@ -59,7 +57,7 @@ def validate_api_key(api_key: str) -> bool:
59
57
  if response.status_code == 200:
60
58
  return True
61
59
  else:
62
- print('APIキーの検証に失敗しました。')
60
+ print(_('api_key_validation_error'))
63
61
  return False
64
62
 
65
63
 
@@ -106,7 +104,7 @@ class ChatGPT:
106
104
  try:
107
105
  reply = responsej['choices'][0]['message']['content']
108
106
  except KeyError:
109
- print('Error:レスポンスの形式が正しくありません. \n' + str(responsej))
107
+ print(_('response_format_error') + str(responsej))
110
108
  return 'Error: Unable to retrieve response.'
111
109
 
112
110
  self.messages.append({'role': 'assistant', 'content': reply})
atcdr/util/i18n.py ADDED
@@ -0,0 +1,317 @@
1
+ import locale
2
+ from typing import Dict, Optional
3
+
4
+
5
+ class I18n:
6
+ def __init__(self):
7
+ self._lang: Optional[str] = None
8
+ self._messages: Dict[str, Dict[str, str]] = {'ja': {}, 'en': {}}
9
+ self._load_messages()
10
+ self._detect_language()
11
+
12
+ def _detect_language(self) -> None:
13
+ try:
14
+ system_locale = locale.getdefaultlocale()[0]
15
+ if system_locale and system_locale.startswith('ja'):
16
+ self._lang = 'ja'
17
+ else:
18
+ self._lang = 'en'
19
+ except Exception:
20
+ self._lang = 'en'
21
+
22
+ def _load_messages(self) -> None:
23
+ self._messages = {
24
+ 'ja': {
25
+ # download.py
26
+ 'retry_problem': '再試行します。{}',
27
+ 'redirect_occurred': 'リダイレクトが発生しました。{}',
28
+ 'problem_not_found': '問題が見つかりません。{}',
29
+ 'server_error': 'サーバーエラーが発生しました。{}',
30
+ 'html_fetch_failed': '{}に対応するHTMLファイルを取得できませんでした。',
31
+ 'save_failed': '{}の保存に失敗しました',
32
+ 'file_saved': 'ファイルを保存しました: {}',
33
+ 'solve_contest_problems': 'コンテストの問題を解きたい',
34
+ 'download_one_problem': '1問だけダウンロードする',
35
+ 'exit': '終了する',
36
+ 'download_atcoder_html': 'AtCoderの問題のHTMLファイルをダウンロードします',
37
+ 'navigate_with_arrows': '十字キーで移動,[enter]で実行',
38
+ 'input_contest_name': 'コンテスト名を入力してください (例: abc012, abs, typical90)',
39
+ 'which_problem_download': 'どの問題をダウンロードしますか?',
40
+ 'exiting': '終了します',
41
+ 'invalid_selection': '無効な選択です',
42
+ 'specify_contest_name': 'コンテスト名を指定してください',
43
+ 'invalid_download_args': 'ダウンロードの引数が正しくありません',
44
+ # test.py
45
+ 'runner_not_found': '{}の適切な言語のランナーが見つかりませんでした.',
46
+ 'test_of': '{}のテスト \n',
47
+ 'compile_time': 'コンパイルにかかった時間: [not italic cyan]{}[/] ms[/]',
48
+ 'compiler_message': 'コンパイラーのメッセージ',
49
+ 'status': 'ステータス ',
50
+ 'execution_time': '実行時間 [cyan]{}[/cyan] ms',
51
+ 'input': '入力',
52
+ 'output': '出力',
53
+ 'expected_output': '正解の出力',
54
+ 'test_in_progress': 'テスト進行中',
55
+ 'test_completed': 'テスト完了',
56
+ 'problem_file_not_found': '問題のファイルが見つかりません。\n問題のファイルが存在するディレクトリーに移動してから実行してください。',
57
+ # markdown.py
58
+ 'markdown_created': 'Markdownファイルを作成しました.',
59
+ # login.py
60
+ 'already_logged_in': 'すでにログインしています. ',
61
+ 'username': 'ユーザー名: ',
62
+ 'password': 'パスワード: ',
63
+ 'solve_captcha': 'キャプチャー認証を解決してください',
64
+ 'logging_in': 'ログインします',
65
+ 'waiting_login_result': 'ログインの結果の待機中...',
66
+ 'login_success': 'ログイン成功!',
67
+ 'error': 'エラー: {}',
68
+ # submit.py
69
+ 'select_implementation': '以下の一覧から{}の実装/コンパイラーを選択してください',
70
+ 'submission_failed': '提出に失敗しました',
71
+ 'submission_id_not_found': '提出IDが取得できませんでした',
72
+ 'submission_success': '提出に成功しました!',
73
+ 'waiting_judge': 'ジャッジ待機中',
74
+ 'judge_timeout': '15秒待ってもジャッジが開始されませんでした',
75
+ 'judging': 'ジャッジ中',
76
+ 'judge_completed': 'ジャッジ完了',
77
+ 'not_logged_in': 'ログインしていません.',
78
+ 'login_failed': 'ログインに失敗しました.',
79
+ 'sample_not_ac': 'サンプルケースが AC していないので提出できません',
80
+ 'logout_success': 'ログアウトしました.',
81
+ 'submission_details': '提出ID: {}, URL: {}',
82
+ # open.py
83
+ 'not_found': 'が見つかりません',
84
+ 'url_opened': 'URLを開きました',
85
+ 'url_not_found_in': 'にURLが見つかりませんでした',
86
+ # generate.py
87
+ 'generating_code': 'コード生成中 (by {})',
88
+ 'code_generation_success': 'コードの生成に成功しました. ',
89
+ 'code_by_model': '{}による{}コード',
90
+ 'code_saved': '{} の出力したコードを保存しました:{}',
91
+ 'generating_template': '{}のテンプレートを生成しています...',
92
+ 'template_created': 'テンプレートファイルを作成 :{}',
93
+ 'nth_code_generation': '{}回目のコード生成中 (by {})',
94
+ 'regenerating_with_prompt': '次のプロンプトを{}に与え,再生成します',
95
+ 'code_generation_success_file': 'コードの生成に成功しました!:{}',
96
+ 'testing_generated_code': '{}が生成したコードをテスト中',
97
+ 'test_success': 'コードのテストに成功!',
98
+ 'test_failed': 'コードのテストに失敗!',
99
+ 'log_saved': '{}の出力のログを保存しました:{}',
100
+ # util/gpt.py
101
+ 'api_key_validation_failed': '環境変数に設定されているAPIキーの検証に失敗しました ',
102
+ 'get_api_key_prompt': 'https://platform.openai.com/api-keys からchatGPTのAPIキーを入手しましょう。\nAPIキー入力してください: ',
103
+ 'api_key_test_success': 'APIキーのテストに成功しました。',
104
+ 'save_api_key_prompt': '以下, ~/.zshrcにAPIキーを保存しますか? [y/n]',
105
+ 'api_key_saved': 'APIキーを {} に保存しました。次回シェル起動時に読み込まれます。',
106
+ 'api_key_required': 'コード生成にはAPIキーが必要です。',
107
+ 'api_key_validation_error': 'APIキーの検証に失敗しました。',
108
+ 'response_format_error': 'Error:レスポンスの形式が正しくありません. \n',
109
+ # util/fileops.py
110
+ 'multiple_files_found': '複数のファイルが見つかりました.ファイルを選択してください:',
111
+ 'target_file_not_found': '対象ファイルが見つかりません。',
112
+ 'file_not_selected': 'ファイルが選択されませんでした。',
113
+ # util/problem.py and util/parse.py
114
+ 'name_required': 'nameは必須です',
115
+ 'language_not_supported': '言語は {} に対応していません',
116
+ 'form_not_found': '問題ページにフォームが存在しません',
117
+ 'problem_table_not_found': '問題のテーブルが見つかりませんでした.',
118
+ 'tbody_not_found': 'tbodyが見つかりませんでした.',
119
+ # util/session.py
120
+ 'response_info': 'レスポンス情報',
121
+ 'item': '項目',
122
+ 'content': '内容',
123
+ 'status_code': 'ステータスコード',
124
+ 'reason': '理由',
125
+ 'response_headers': 'レスポンスヘッダー',
126
+ 'key': 'キー',
127
+ 'value': '値',
128
+ 'redirect_history': 'リダイレクト履歴',
129
+ 'step': 'ステップ',
130
+ 'response_body': 'レスポンスボディ',
131
+ 'hello_user': 'こんにちは![cyan]{}[/] さん',
132
+ 'session_check_error': 'セッションチェック中にエラーが発生しました: {}',
133
+ # Click command descriptions
134
+ 'cmd_download': 'AtCoder の問題をダウンロード',
135
+ 'cmd_test': 'テストを実行',
136
+ 'cmd_submit': 'ソースを提出',
137
+ 'cmd_generate': 'コードを生成',
138
+ 'cmd_login': 'AtCoderへログイン',
139
+ 'cmd_logout': 'AtCoderへログアウト',
140
+ 'cmd_markdown': 'Markdown形式で問題を表示します',
141
+ 'cmd_open': 'HTMLファイルを開く',
142
+ # Click option descriptions
143
+ 'opt_no_test': 'テストをスキップ',
144
+ 'opt_no_feedback': 'フィードバックをスキップ',
145
+ 'opt_lang': '出力する言語を指定',
146
+ 'opt_save': '変換結果をファイルに保存',
147
+ 'opt_output_lang': '出力するプログラミング言語',
148
+ 'opt_model': '使用するGPTモデル',
149
+ 'opt_without_test': 'テストケースを省略して生成',
150
+ 'opt_template': 'テンプレートを生成',
151
+ },
152
+ 'en': {
153
+ # download.py
154
+ 'retry_problem': 'Retrying... {}',
155
+ 'redirect_occurred': 'Redirect occurred. {}',
156
+ 'problem_not_found': 'Problem not found. {}',
157
+ 'server_error': 'Server error occurred. {}',
158
+ 'html_fetch_failed': 'Failed to fetch HTML file for {}.',
159
+ 'save_failed': 'Failed to save {}',
160
+ 'file_saved': 'File saved: {}',
161
+ 'solve_contest_problems': 'Solve contest problems',
162
+ 'download_one_problem': 'Download one problem',
163
+ 'exit': 'Exit',
164
+ 'download_atcoder_html': 'Download AtCoder problem HTML files',
165
+ 'navigate_with_arrows': 'Use arrow keys to navigate, [enter] to execute',
166
+ 'input_contest_name': 'Enter contest name (e.g., abc012, abs, typical90)',
167
+ 'which_problem_download': 'Which problem to download?',
168
+ 'exiting': 'Exiting',
169
+ 'invalid_selection': 'Invalid selection',
170
+ 'specify_contest_name': 'Please specify contest name',
171
+ 'invalid_download_args': 'Invalid download arguments',
172
+ # test.py
173
+ 'runner_not_found': 'Appropriate language runner for {} not found.',
174
+ 'test_of': 'Testing {} \n',
175
+ 'compile_time': 'Compile time: [not italic cyan]{}[/] ms[/]',
176
+ 'compiler_message': 'Compiler message',
177
+ 'status': 'Status ',
178
+ 'execution_time': 'Execution time [cyan]{}[/cyan] ms',
179
+ 'input': 'Input',
180
+ 'output': 'Output',
181
+ 'expected_output': 'Expected output',
182
+ 'test_in_progress': 'Testing in progress',
183
+ 'test_completed': 'Test completed',
184
+ 'problem_file_not_found': 'Problem file not found.\nPlease navigate to the directory containing the problem file.',
185
+ # markdown.py
186
+ 'markdown_created': 'Markdown file created.',
187
+ # login.py
188
+ 'already_logged_in': 'Already logged in. ',
189
+ 'username': 'Username: ',
190
+ 'password': 'Password: ',
191
+ 'solve_captcha': 'Please solve the captcha',
192
+ 'logging_in': 'Logging in',
193
+ 'waiting_login_result': 'Waiting for login result...',
194
+ 'login_success': 'Login successful!',
195
+ 'error': 'Error: {}',
196
+ # submit.py
197
+ 'select_implementation': 'Select {} implementation/compiler from the list below',
198
+ 'submission_failed': 'Submission failed',
199
+ 'submission_id_not_found': 'Submission ID not found',
200
+ 'submission_success': 'Submission successful!',
201
+ 'waiting_judge': 'Waiting for judge',
202
+ 'judge_timeout': 'Judge did not start after 15 seconds',
203
+ 'judging': 'Judging',
204
+ 'judge_completed': 'Judge completed',
205
+ 'not_logged_in': 'Not logged in.',
206
+ 'login_failed': 'Login failed.',
207
+ 'sample_not_ac': 'Cannot submit because sample cases are not AC',
208
+ 'logout_success': 'Logged out successfully.',
209
+ 'submission_details': 'Submission ID: {}, URL: {}',
210
+ # open.py
211
+ 'not_found': ' not found',
212
+ 'url_opened': 'URL opened',
213
+ 'url_not_found_in': 'URL not found in ',
214
+ # generate.py
215
+ 'generating_code': 'Generating code (by {})',
216
+ 'code_generation_success': 'Code generation successful. ',
217
+ 'code_by_model': '{} code by {}',
218
+ 'code_saved': 'Code generated by {} saved: {}',
219
+ 'generating_template': 'Generating {} template...',
220
+ 'template_created': 'Template file created: {}',
221
+ 'nth_code_generation': 'Code generation attempt {} (by {})',
222
+ 'regenerating_with_prompt': 'Regenerating with the following prompt for {}',
223
+ 'code_generation_success_file': 'Code generation successful: {}',
224
+ 'testing_generated_code': 'Testing code generated by {}',
225
+ 'test_success': 'Code test successful!',
226
+ 'test_failed': 'Code test failed!',
227
+ 'log_saved': 'Log of {} output saved: {}',
228
+ # util/gpt.py
229
+ 'api_key_validation_failed': 'API key validation failed ',
230
+ 'get_api_key_prompt': 'Get your ChatGPT API key from https://platform.openai.com/api-keys\nEnter API key: ',
231
+ 'api_key_test_success': 'API key test successful.',
232
+ 'save_api_key_prompt': 'Save API key to ~/.zshrc? [y/n]',
233
+ 'api_key_saved': 'API key saved to {}. Will be loaded on next shell startup.',
234
+ 'api_key_required': 'API key required for code generation.',
235
+ 'api_key_validation_error': 'API key validation failed.',
236
+ 'response_format_error': 'Error: Response format is invalid. \n',
237
+ # util/fileops.py
238
+ 'multiple_files_found': 'Multiple files found. Please select a file:',
239
+ 'target_file_not_found': 'Target file not found.',
240
+ 'file_not_selected': 'No file selected.',
241
+ # util/problem.py and util/parse.py
242
+ 'name_required': 'Name is required',
243
+ 'language_not_supported': 'Language {} is not supported',
244
+ 'form_not_found': 'Form not found on problem page',
245
+ 'problem_table_not_found': 'Problem table not found.',
246
+ 'tbody_not_found': 'tbody not found.',
247
+ # util/session.py
248
+ 'response_info': 'Response Information',
249
+ 'item': 'Item',
250
+ 'content': 'Content',
251
+ 'status_code': 'Status Code',
252
+ 'reason': 'Reason',
253
+ 'response_headers': 'Response Headers',
254
+ 'key': 'Key',
255
+ 'value': 'Value',
256
+ 'redirect_history': 'Redirect History',
257
+ 'step': 'Step',
258
+ 'response_body': 'Response Body',
259
+ 'hello_user': 'Hello [cyan]{}[/]!',
260
+ 'session_check_error': 'Error occurred during session check: {}',
261
+ # Click command descriptions
262
+ 'cmd_download': 'Download AtCoder problems',
263
+ 'cmd_test': 'Run tests',
264
+ 'cmd_submit': 'Submit source code',
265
+ 'cmd_generate': 'Generate code',
266
+ 'cmd_login': 'Login to AtCoder',
267
+ 'cmd_logout': 'Logout from AtCoder',
268
+ 'cmd_markdown': 'Display problem in Markdown format',
269
+ 'cmd_open': 'Open HTML file',
270
+ # Click option descriptions
271
+ 'opt_no_test': 'Skip testing',
272
+ 'opt_no_feedback': 'Skip feedback',
273
+ 'opt_lang': 'Specify output language',
274
+ 'opt_save': 'Save conversion result to file',
275
+ 'opt_output_lang': 'Specify output programming language',
276
+ 'opt_model': 'GPT model to use',
277
+ 'opt_without_test': 'Generate without test cases',
278
+ 'opt_template': 'Generate template',
279
+ },
280
+ }
281
+
282
+ def get(self, key: str, *args) -> str:
283
+ """
284
+ メッセージを取得する
285
+ Args:
286
+ key: メッセージキー
287
+ *args: フォーマット引数
288
+ Returns:
289
+ フォーマット済みのメッセージ
290
+ """
291
+ message = self._messages.get(self._lang or 'en', {}).get(key, key)
292
+ if args:
293
+ try:
294
+ return message.format(*args)
295
+ except Exception:
296
+ return message
297
+ return message
298
+
299
+ def set_language(self, lang: str) -> None:
300
+ """言語を明示的に設定する"""
301
+ if lang in self._messages:
302
+ self._lang = lang
303
+
304
+ @property
305
+ def language(self) -> str:
306
+ """現在の言語を取得する"""
307
+ return self._lang or 'en'
308
+
309
+
310
+ # シングルトンインスタンス
311
+ i18n = I18n()
312
+
313
+
314
+ # 便利な関数
315
+ def _(key: str, *args) -> str:
316
+ """i18n.get()のショートカット"""
317
+ return i18n.get(key, *args)
atcdr/util/parse.py CHANGED
@@ -5,6 +5,8 @@ from bs4 import BeautifulSoup as bs
5
5
  from bs4 import Tag
6
6
  from markdownify import MarkdownConverter
7
7
 
8
+ from atcdr.util.i18n import _, i18n
9
+
8
10
 
9
11
  class HTML:
10
12
  def __init__(self, html: str) -> None:
@@ -68,13 +70,16 @@ class ProblemForm(Tag):
68
70
 
69
71
 
70
72
  class ProblemHTML(HTML):
71
- def repair_me(self) -> None:
73
+ def repair_me(self, lang: Optional[str] = None) -> None:
72
74
  html = self.html.replace('//img.atcoder.jp', 'https://img.atcoder.jp')
73
- html = html.replace(
74
- '<meta http-equiv="Content-Language" content="en">',
75
- '<meta http-equiv="Content-Language" content="ja">',
76
- )
77
- html = html.replace('LANG = "en"', 'LANG="ja"')
75
+ # 言語パラメータまたはi18nの設定に基づいて言語を決定
76
+ target_lang = lang or i18n.language
77
+ if target_lang == 'ja':
78
+ html = html.replace(
79
+ '<meta http-equiv="Content-Language" content="en">',
80
+ '<meta http-equiv="Content-Language" content="ja">',
81
+ )
82
+ html = html.replace('LANG = "en"', 'LANG="ja"')
78
83
  self.soup = bs(html, 'html.parser')
79
84
 
80
85
  def abstract_problem_part(self, lang: str) -> Optional[Tag]:
@@ -87,7 +92,7 @@ class ProblemHTML(HTML):
87
92
  elif lang == 'en':
88
93
  lang_class = 'lang-en'
89
94
  else:
90
- raise ValueError(f'言語は {lang} に対応していません')
95
+ raise ValueError(_('language_not_supported', lang))
91
96
  span = task_statement.find('span', {'class': lang_class})
92
97
  return span
93
98
 
@@ -105,7 +110,7 @@ class ProblemHTML(HTML):
105
110
  def load_labeled_testcase(self) -> List:
106
111
  from atcdr.test import LabeledTestCase, TestCase
107
112
 
108
- problem_part = self.abstract_problem_part('en')
113
+ problem_part = self.abstract_problem_part(i18n.language)
109
114
  if problem_part is None:
110
115
  return []
111
116
 
@@ -146,7 +151,7 @@ class ProblemHTML(HTML):
146
151
  def form(self) -> ProblemForm:
147
152
  form = self.soup.find('form', class_='form-code-submit')
148
153
  if not isinstance(form, Tag):
149
- raise ValueError('問題ページにフォームが存在しません')
154
+ raise ValueError(_('form_not_found'))
150
155
  form.__class__ = ProblemForm
151
156
  return form
152
157
 
@@ -178,12 +183,12 @@ def get_problem_urls_from_tasks(html_content: str) -> list[tuple[str, str]]:
178
183
  soup = bs(html_content, 'html.parser')
179
184
  table = soup.find('table')
180
185
  if not table:
181
- raise ValueError('問題のテーブルが見つかりませんでした.')
186
+ raise ValueError(_('problem_table_not_found'))
182
187
 
183
188
  # tbodyタグを見つける
184
189
  tbody = table.find('tbody')
185
190
  if not tbody:
186
- raise ValueError('tbodyが見つかりませんでした.')
191
+ raise ValueError(_('tbody_not_found'))
187
192
 
188
193
  # tbody内の1列目のaタグのリンクと中身を取得
189
194
  links = []
atcdr/util/problem.py CHANGED
@@ -3,19 +3,20 @@ from dataclasses import dataclass
3
3
 
4
4
  import requests
5
5
 
6
+ from atcdr.util.i18n import _
6
7
  from atcdr.util.parse import get_problem_urls_from_tasks
7
8
 
8
9
 
9
10
  class Contest:
10
11
  def __init__(self, name: str, session: requests.Session):
11
12
  if not name:
12
- raise ValueError('nameは必須です')
13
+ raise ValueError(_('name_required'))
13
14
  self.name = name
14
15
 
15
16
  self.url = f'https://atcoder.jp/contests/{name}/tasks'
16
17
  retry_attempts = 2
17
18
  retry_wait = 0.30
18
- for _ in range(retry_attempts):
19
+ for attempt in range(retry_attempts):
19
20
  response = session.get(self.url)
20
21
  if response.ok:
21
22
  break