Claude API를 이용한 스프레드 시트 번역 기능 추가
Claude : https://console.anthropic.com/dashboard
위의 주소에서 API키를 발급 받아야 한다.
+ API를 사용하기 위해서 카드를 등록해야하고 사용량에 따라 비용이 나간다.
비용은 번역 90000개 기준 약 5만원 정도 들 것으로 예상 (지침이나 양에 따라 달라짐)
위 사이트에 들어가 로그인을 하고 상단의 Setting으로 들어간다.
좌측 메뉴에서 API Keys로 들어간다.
Create Key를 눌러 API 키를 생성한다. (반드시 다른 곳에 복사해두자)
만들게 되면 위와 같이 나온다
API키 발급은 끝났다.
이제 스프레드 시트로 가자
스프레드 시트 상단 메뉴 중 [확장 프로그램] 에서 Apps Script를 눌러 편집기를 연다.
아무것도 없는 화면이 뜨는데 이제 Code.gs를 수정 할 차례다.
function onOpen() {
var ui = SpreadsheetApp.getUi();
ui.createMenu('번역 도구')
.addItem('번역 설정 보기/수정', 'showSettings')
.addItem('선택한 셀 번역', 'translateSingleCell')
.addItem('선택한 범위 번역', 'translateRange')
.addToUi();
}
// 번역 설정 저장
function saveSettings(instruction) {
var userProperties = PropertiesService.getUserProperties();
userProperties.setProperty('instruction', instruction);
}
// 저장된 설정 불러오기
function getSettings() {
var userProperties = PropertiesService.getUserProperties();
return {
instruction: userProperties.getProperty('instruction') || ''
};
}
// 설정 대화상자 표시
function showSettings() {
var settings = getSettings();
var html = HtmlService.createHtmlOutput(`
<form id="settingsForm">
<div style="margin-bottom: 10px;">
<label for="instruction">번역 지침:</label><br>
<textarea id="instruction" name="instruction" rows="6" style="width: 300px;"
placeholder="예시: - 전문 용어는 영어로 유지 - 비즈니스 문서 형식으로 번역 - 존댓말 사용">${settings.instruction}</textarea>
</div>
<input type="button" value="저장" onclick="saveAndClose()" style="margin-top: 10px;">
</form>
<script>
function saveAndClose() {
var form = document.getElementById('settingsForm');
google.script.run.withSuccessHandler(function() {
google.script.host.close();
}).saveSettings(form.instruction.value);
}
</script>
`).setWidth(350).setHeight(250);
SpreadsheetApp.getUi().showModalDialog(html, '번역 설정');
}
// 언어 정보 가져오기
function getLanguageInfo() {
var sheet = SpreadsheetApp.getActiveSheet();
var headerRow = sheet.getRange("1:1").getValues()[0];
return headerRow.map(lang => lang.toString().trim());
}
// 진행 상태 대화상자 ID를 저장할 변수
var PROGRESS_DIALOG_ID = null;
// 진행 상태 표시 UI - 자동 닫기 기능 추가
function showProgress(title, current, total, isComplete = false) {
var html = HtmlService.createHtmlOutput(`
<div style="padding: 20px;">
<p>${title}</p>
<p>${current} / ${total} (${Math.round(current/total*100)}%)</p>
${isComplete ? '<script>google.script.host.close();</script>' : ''}
</div>
`)
.setWidth(300)
.setHeight(100);
if (!PROGRESS_DIALOG_ID) {
PROGRESS_DIALOG_ID = SpreadsheetApp.getUi().showModelessDialog(html, '번역 진행 상황');
} else {
PROGRESS_DIALOG_ID.close();
PROGRESS_DIALOG_ID = SpreadsheetApp.getUi().showModelessDialog(html, '번역 진행 상황');
}
}
// 배치 번역 실행
function batchTranslateTexts(texts, sourceLang, targetLang) {
var settings = getSettings();
var CLAUDE_API_KEY = PropertiesService.getScriptProperties().getProperty('CLAUDE_API_KEY');
var CLAUDE_API_URL = "https://api.anthropic.com/v1/messages";
try {
var instruction = settings.instruction ? settings.instruction + "\n\n" : "";
var prompt = `Translate the following texts from ${sourceLang} to ${targetLang}.
${instruction}
Texts to translate:
${texts.map((text, index) => `${index + 1}. ${text}`).join('\n')}
Provide translations in the following format, numbers only:
1. [translation1]
2. [translation2]
...`;
var payload = {
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"messages": [{
"role": "user",
"content": prompt
}]
};
var options = {
'method': 'post',
'headers': {
'Content-Type': 'application/json',
'x-api-key': CLAUDE_API_KEY,
'anthropic-version': '2023-06-01'
},
'payload': JSON.stringify(payload)
};
var response = UrlFetchApp.fetch(CLAUDE_API_URL, options);
var jsonResponse = JSON.parse(response.getContentText());
// 응답 텍스트를 줄별로 파싱하여 번역 결과 추출
var translations = jsonResponse.content[0].text
.split('\n')
.filter(line => /^\d+\./.test(line))
.map(line => line.replace(/^\d+\.\s*/, '').trim());
return translations;
} catch (error) {
Logger.log("Batch translation error: " + error);
throw error;
}
}
// 단일 셀 번역
function translateSingleCell() {
var sheet = SpreadsheetApp.getActiveSheet();
var activeCell = sheet.getActiveCell();
var activeCol = activeCell.getColumn();
var activeRow = activeCell.getRow();
if (activeRow === 1) {
SpreadsheetApp.getUi().alert('첫 번째 행은 언어 정보입니다. 다른 셀을 선택해주세요.');
return;
}
var languages = getLanguageInfo();
var koreanCol = languages.indexOf('Korean') + 1;
var englishCol = languages.indexOf('English') + 1;
var targetLang = languages[activeCol - 1];
try {
if (targetLang === 'English') {
var koreanText = sheet.getRange(activeRow, koreanCol).getValue();
if (!koreanText) {
SpreadsheetApp.getUi().alert('번역할 한국어 텍스트가 없습니다.');
return;
}
var translations = batchTranslateTexts([koreanText], 'Korean', 'English');
sheet.getRange(activeRow, activeCol).setValue(translations[0]);
} else {
var englishText = sheet.getRange(activeRow, englishCol).getValue();
if (!englishText) {
SpreadsheetApp.getUi().alert('번역할 영어 텍스트가 없습니다.');
return;
}
var translations = batchTranslateTexts([englishText], 'English', targetLang);
sheet.getRange(activeRow, activeCol).setValue(translations[0]);
}
} catch (error) {
SpreadsheetApp.getUi().alert('번역 중 오류가 발생했습니다: ' + error.toString());
}
}
// 범위 번역
function translateRange() {
var BATCH_SIZE = 20;
var sheet = SpreadsheetApp.getActiveSheet();
var selectedRange = sheet.getActiveRange();
var startRow = selectedRange.getRow();
var startCol = selectedRange.getColumn();
var numRows = selectedRange.getNumRows();
var numCols = selectedRange.getNumColumns();
if (startRow === 1) {
SpreadsheetApp.getUi().alert('첫 번째 행은 언어 정보입니다.');
return;
}
var languages = getLanguageInfo();
var koreanCol = languages.indexOf('Korean') + 1;
var englishCol = languages.indexOf('English') + 1;
// 1단계: Korean → English 번역이 필요한 행 확인 및 처리
var koreanToEnglishQueue = [];
for (var row = startRow; row < startRow + numRows; row++) {
var englishText = sheet.getRange(row, englishCol).getValue();
var koreanText = sheet.getRange(row, koreanCol).getValue();
if (!englishText && koreanText) {
koreanToEnglishQueue.push({
row: row,
text: koreanText
});
}
}
// Korean → English 배치 처리
if (koreanToEnglishQueue.length > 0) {
for (var i = 0; i < koreanToEnglishQueue.length; i += BATCH_SIZE) {
var batch = koreanToEnglishQueue.slice(i, i + BATCH_SIZE);
var texts = batch.map(item => item.text);
showProgress('한국어 → 영어 번역 중...', i + batch.length, koreanToEnglishQueue.length);
var translations = batchTranslateTexts(texts, 'Korean', 'English');
translations.forEach((translation, index) => {
var item = batch[index];
sheet.getRange(item.row, englishCol).setValue(translation);
});
if (i + BATCH_SIZE < koreanToEnglishQueue.length) {
Utilities.sleep(1000);
}
}
}
// 2단계: English → 타겟 언어 번역
var translationGroups = {};
for (var row = startRow; row < startRow + numRows; row++) {
var englishText = sheet.getRange(row, englishCol).getValue();
if (!englishText) {
SpreadsheetApp.getUi().alert(`${row}행의 영어 텍스트가 없습니다.`);
return;
}
for (var col = startCol; col < startCol + numCols; col++) {
if (col === koreanCol || col === englishCol) continue;
var targetLang = languages[col - 1];
if (!translationGroups[targetLang]) {
translationGroups[targetLang] = [];
}
translationGroups[targetLang].push({
row: row,
col: col,
text: englishText
});
}
}
// 각 타겟 언어별로 배치 처리
var totalTranslations = Object.values(translationGroups)
.reduce((sum, group) => sum + group.length, 0);
var completedTranslations = 0;
try {
for (var targetLang in translationGroups) {
var group = translationGroups[targetLang];
for (var i = 0; i < group.length; i += BATCH_SIZE) {
var batch = group.slice(i, i + BATCH_SIZE);
var texts = batch.map(item => item.text);
showProgress('다국어 번역 중...', completedTranslations + batch.length, totalTranslations);
var translations = batchTranslateTexts(texts, 'English', targetLang);
translations.forEach((translation, index) => {
var item = batch[index];
sheet.getRange(item.row, item.col).setValue(translation);
completedTranslations++;
});
if (i + BATCH_SIZE < group.length) {
Utilities.sleep(1000);
}
}
}
// 마지막 진행 상태 업데이트 및 완료 처리
showProgress('번역 완료', totalTranslations, totalTranslations, true);
SpreadsheetApp.getUi().alert('번역이 완료되었습니다.');
} catch (error) {
showProgress('번역 중 오류 발생', completedTranslations, totalTranslations, true);
SpreadsheetApp.getUi().alert('번역 중 오류가 발생했습니다: ' + error.toString());
}
}
해당 코드를 복사 붙여넣기 후 저장을 한다.
좌측 프로젝트 설정 하단에서
아까 적어놓은 API키를 속성으로 추가한다.
속성 이름은 CLAUDE_API_KEY 로 하고, 값은 아까의 API키를 넣어주면 된다.
이후 스프레드 시트로 가서 새로고침을 하면
사진과 같이 번역 도구 메뉴가 생겨있을 것이다.
번역 도구에서 [번역 설정 보기/수정]으로 번역 지침을 설정할 수 있다.
번역 지침이 상세 할수록(내용이 길수록) 비용은 올라가게 된다.
[예시]
당신은 게임 용어 전문 번역가입니다. 다음 지침을 따라 번역해주세요:
메모나 설명 없이 번역만 합니다.
No Note and Translate:
1. 번역 스타일:
- 게임 용어는 한국 게임 시장의 일반적인 관행을 따를 것
- 자연스러운 한국어로 번역할 것
2. 용어 통일:
- ATK → 공격력
- HP → 체력
3. 형식 규칙:
- 줄바꿈 유지
- 특수문자 유지
- 숫자는 그대로 유지
4. 문맥:
- 게임 내 아이템/스킬 설명임을 고려할 것
- 간결하고 명확한 표현 사용
주의!!!
해당 코드는 1열은 [언어]로 인식한다.
1열에 언어들을 넣고 2열부터 번역하고 싶은 내용을 넣고 번역하면 된다.
해당 코드는 Korean과 English가 무조건 있어야 한다.
Korean내용으로 English를 번역 후 English를 기준으로 나머지 언어들을 번역한다.
번역 하고 싶은 내용을 Korean에 입력 후
[선택한 셀 번역]으로 하나만 번역하거나
[선택한 범위 번역]으로 지정한 셀들을 번역할 수 있다.