feat: [claude-code] 뉴스 페이지 한/영 토글 기능 추가

- Google Translate API 연동으로 릴리즈 노트 한국어 자동 번역
- 코드 블록 보호 처리 (번역 대상에서 제외)
- 긴 텍스트 단락 분할 번역 지원
- Alpine.js 한국어/English 토글 버튼 (localStorage 저장)
- 기본값: 한국어
This commit is contained in:
김보곤
2026-03-02 10:55:32 +09:00
parent ab042cb132
commit bec0df29e1
2 changed files with 125 additions and 6 deletions

View File

@@ -15,7 +15,7 @@ class ClaudeCodeNewsService
private const API_URL = 'https://api.github.com/repos/anthropics/claude-code/releases';
/**
* GitHub Releases 목록 조회 (캐싱)
* GitHub Releases 목록 조회 (캐싱, 한국어 번역 포함)
*/
public function getReleases(int $perPage = 20): array
{
@@ -33,7 +33,7 @@ public function clearCache(): void
}
/**
* GitHub API에서 릴리즈 정보 가져오기
* GitHub API에서 릴리즈 정보 가져오기 + 한국어 번역
*/
private function fetchFromGitHub(int $perPage): array
{
@@ -51,11 +51,15 @@ private function fetchFromGitHub(int $perPage): array
return collect($response->json())
->map(function ($release) {
$body = $release['body'] ?? '';
$bodyKo = $this->translateToKorean($body);
return [
'id' => $release['id'],
'tag_name' => $release['tag_name'],
'name' => $release['name'] ?? $release['tag_name'],
'body_html' => Str::markdown($release['body'] ?? ''),
'body_html' => Str::markdown($body),
'body_html_ko' => Str::markdown($bodyKo),
'published_at' => $release['published_at'],
'author' => $release['author']['login'] ?? 'unknown',
'author_avatar' => $release['author']['avatar_url'] ?? '',
@@ -71,4 +75,102 @@ private function fetchFromGitHub(int $perPage): array
return [];
}
}
/**
* 영문 마크다운을 한국어로 번역 (코드 블록 보호)
*/
private function translateToKorean(string $text): string
{
if (empty(trim($text))) {
return '';
}
// 코드 블록/인라인 코드를 플레이스홀더로 보호
$protected = [];
$safe = preg_replace_callback('/```[\s\S]*?```|`[^`\n]+`/', function ($match) use (&$protected) {
$key = 'ZXCBLK'.count($protected).'QWE';
$protected[$key] = $match[0];
return $key;
}, $text);
// 번역
if (mb_strlen($safe) <= 4000) {
$translated = $this->callGoogleTranslate($safe);
} else {
$translated = $this->translateLongText($safe);
}
// 코드 블록 복원
foreach ($protected as $key => $code) {
$translated = str_replace($key, $code, $translated);
}
return $translated;
}
/**
* 긴 텍스트를 단락 단위로 분할 번역
*/
private function translateLongText(string $text): string
{
$parts = preg_split('/(\n\n+)/', $text, -1, PREG_SPLIT_DELIM_CAPTURE);
$chunks = [];
$current = '';
foreach ($parts as $part) {
if (mb_strlen($current.$part) > 4000 && $current !== '') {
$chunks[] = $current;
$current = $part;
} else {
$current .= $part;
}
}
if ($current !== '') {
$chunks[] = $current;
}
$result = '';
foreach ($chunks as $chunk) {
$result .= $this->callGoogleTranslate($chunk);
}
return $result;
}
/**
* Google Translate API 호출
*/
private function callGoogleTranslate(string $text): string
{
try {
$response = Http::timeout(5)
->get('https://translate.googleapis.com/translate_a/single', [
'client' => 'gtx',
'sl' => 'en',
'tl' => 'ko',
'dt' => 't',
'q' => $text,
]);
if (! $response->successful()) {
return $text;
}
$result = $response->json();
$translated = '';
if (is_array($result) && isset($result[0]) && is_array($result[0])) {
foreach ($result[0] as $segment) {
if (is_array($segment) && isset($segment[0])) {
$translated .= $segment[0];
}
}
}
return $translated !== '' ? $translated : $text;
} catch (\Exception $e) {
return $text;
}
}
}