1. 결과 화면
2. 구글 페이지 번역
1. 구글 페이지 번역 구현
구글에서 특정 텍스트에 대한 번역은 api호출을 통한 번역이 가능하다. 페이지 번역의 경우 찾아보다 보니, Google Translate Website Translator로 가능하다는 것을 알게 되었다.
심지어 이놈은 api key를 발급받아 사용량에 따라 가격이 책정되는 방식이 아니라, 그저 script를 불러와 번역을 할 수 있는 기능을 가지고 있는 굉장한 녀석이었다.
script로 불러온 기능을 원하는 부분만 번역도 가능한 것 같지만, 필자가 원하는 기능은 페이지 전체의 번역이기 때문에, root 또는 body와 같은 요소에 해당 script를 넣으면, 페이지 전체 번역이 가능하다.
'공짜'에 '전체 페이지 번역'이라고???!?!?!!! 홀리 쉿 하면서 사용했지만 단점도 존재한다.
단점으로는 body에 넣은 경우 상단에 뜨는 하찮은 UI의 구글 자체 번역 탭이 켜지는 것이 단점이다.
또 다른 단점으로는 상단 바에 뜨는 번역 탭을 클릭했을 때 언어 변경이 한글로 밖에 되어있지 않다.
그렇다면 우리 사이트의 경우 필요한 요구사항은 외국인이 사용할 수 있도록 번역을 하는 것인데 한글만 뜨면 외국인이 어떻게 사용함???? 🤔
그래서 한국어로 적혀있는 것이 아닌 국기가 들어간 언어 리스트를 선택할 수 있는 번역 버튼을 만들려고 한다.
2. useEffect로 호출
useEffect(() => {
const addGoogleTranslateScript = document.createElement('script');
addGoogleTranslateScript.src =
'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
document.body.appendChild(addGoogleTranslateScript);
window.googleTranslateElementInit = () => {
new window.google.translate.TranslateElement(
{ pageLanguage: 'ko', autoDisplay: true },
'google_translate_element'
);
};
return () => {
document.body.removeChild(addGoogleTranslateScript);
};
}, []);
위 코드는 페이지 번역을 하기 위한 코드의 전체이다.
전반적으로 큰 틀은, Mount 하게 되면 모든 body아래 자식 요소에게 addGoogleTranslateScript를 전달한다.
그리고 unMount시에 document.body.removeChild(addGoogleTranslateScript);를 통해 제거해준다.
1. 구글 페이지 번역 스크립트 불러와 페이지 모든 html에 적용하기.
const addGoogleTranslateScript = document.createElement('script');
addGoogleTranslateScript.src =
'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
document.body.appendChild(addGoogleTranslateScript);
2. 번역을 시작했을 때 한국어를 기본적으로 선택하고, 나중에 언어 변경을 할 수 있는 부분
window.googleTranslateElementInit = () => {
new window.google.translate.TranslateElement(
{ pageLanguage: 'ko', autoDisplay: true },
'google_translate_element'
);
};
3. 메모리 누수 방지와 다른 언어를 선택했을 때 script별 충돌 방지 용 unMount return문
return () => {
document.body.removeChild(addGoogleTranslateScript);
};
3. 언어 선택 리스트
const [chooseCountry, setChooseCountry] = useState({
code: 'ko',
name: '한국어',
flag: 'kr',
});
우리는 언어와 국기를 선택해야 하는데, google에서 사용하는 언어 코드는 ISO 639-1을 사용하는데, 한국을 예시로 들면 'ko'이다.
해당 나라 국기에 대한 사진은
위와 같이 구현하였는데, 여기에 들어가야 하는 국기 코드는 ISO 3166-1 alpha-2를 따른다.
한국을 예시로 들면, kr에 해당한다.
그렇기에 여러 나라의 리스트를 구현하려 할 때에 위처럼, 3가지의 attribute를 갖게 되었다.
그리고 약 60개의 언어를 배열로 아래와 같이 저장해 두었다.
export const languages = [
{ code: 'ko', name: '한국어', flag: 'kr' }, // 한국어
{ code: 'en', name: 'English', flag: 'us' }, // 영어
{ code: 'fr', name: 'Français', flag: 'fr' }, // 프랑스어
{ code: 'de', name: 'Deutsch', flag: 'de' }, // 독일어
.......
4. return문
return (
<>
<div id="google_translate_element" style={{ display: 'none' }}></div>
<ButtonCotainer
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Flag code={chooseCountry.flag} />
{chooseCountry.name}
{/* 번역버튼 hover 시 리스트 표시 */}
{isHovered && (
<LanguageList
onWheel={handleWheel}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{languages.map((lang) => (
<LanguageItem
key={lang.code}
onClick={() => handleLanguageChange(lang)}
>
<Flag code={lang.flag} />
{lang.name}
</LanguageItem>
))}
</LanguageList>
)}
</ButtonCotainer>
</>
);
<div id="google_translate_element" style={{ display: 'none' }}></div>가 없다면, 번역이 되지 않는다!
1. 번역 버튼과 현재 선택된 언어 표시
// 현재 선택된 언어 state
const [chooseCountry, setChooseCountry] = useState({
code: 'ko',
name: '한국어',
flag: 'kr',
});
// Button 컴포넌트
<ButtonCotainer
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Flag code={chooseCountry.flag} />
{chooseCountry.name}
.....
</ButtonCotainer>
//css
const ButtonCotainer = styled.li`
display: flex;
align-items: center;
gap: 5px;
padding: 5px 5px;
width: 95px;
height: 45px;
cursor: pointer;
background-color: #ffffff;
outline: none;
border: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
border-radius: 5px;
font-size: 14px;
position: relative;
z-index: 999;
`;
- 현재 선택한 언어에 대한 국기와 언어 이름을 state로 관리하며 버튼에서 보여준다.
- hover관리를 css적으로 하지 않고, state로 관리하는 이유는 다음 리스트에서 설명하도록 하겠다.
- z- index를 낮게 주는 경우, 웹 안에 더 높은 z-index가 존재하면 오류가 발생할 수 있다.
- realtive 속성을 주어 리스트가 absolute 일 때 붙어 있도록 하였다.
2. 버튼 hover시에 생기는 리스트 ( but 버튼을 벗어나 리스트 위에 마우스가 있어도 유지되어야 하기에 hover를 state로 관리)
// 언어 변경 함수
const handleLanguageChange = (lang) => {
const value = lang.code;
const gtCombo = document.querySelector('.goog-te-combo');
if (gtCombo) {
gtCombo.value = value;
gtCombo.dispatchEvent(new Event('change'));
}
setChooseCountry(lang);
};
//return 문
{isHovered && (
<LanguageList
onWheel={handleWheel}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{languages.map((lang) => (
<LanguageItem
key={lang.code}
onClick={() => handleLanguageChange(lang)}
>
<Flag code={lang.flag} />
{lang.name}
</LanguageItem>
))}
</LanguageList>
)}
//css
const LanguageList = styled.ul`
position: absolute;
top: -80vh;
left: 0;
background-color: white;
border: 1px solid #ccc;
list-style: none;
padding: 10px;
margin: 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 100px;
height: 80vh;
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px;
z-index: 1000;
`;
const LanguageItem = styled.li`
cursor: pointer;
padding: 10px 0px;
&:hover {
background-color: #f0f0f0;
}
font-size: 14px;
border-bottom: 1px solid #b7b7b7;
`;
- onWheel은 필자가 홈에서 스크롤 이벤트를 막아두었기 때문에, 영향을 받지 않기 위해 따로 빼두었다. 이와 같이 스크롤 관련 코드들의 간섭이 없는 경우 필요 없다.
- MouseEvent를 통해 hover state를 관리하였다.
- 리스트 안에 Langage Item을 map속성을 통해 각 국가 별 컴포넌트를 구현.
- LanguageItem을 클릭 시에 선택한 언어로 번역이 될 수 있도록, onClick 이벤트를 {() => handleLanguageChange(lang)} 과 같이 설정.
- 구현 중 발생했던 오류
error reading 'L'오류가 발생하는 경우
z-index에 관하여 오류가 발생하는데, 번역 버튼 컴포넌트를 페이지에서 가장 위쪽에 배치하면서 해결하였다.
3. 소스코드
1. 전체 코드
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { languages } from './TranslateCountry';
const GoogleTranslate = () => {
const [chooseCountry, setChooseCountry] = useState({
code: 'ko',
name: '한국어',
flag: 'kr',
});
const [isHovered, setIsHovered] = useState(false); // hover 상태 관리
useEffect(() => {
const addGoogleTranslateScript = document.createElement('script');
addGoogleTranslateScript.src =
'https://translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
document.body.appendChild(addGoogleTranslateScript);
window.googleTranslateElementInit = () => {
new window.google.translate.TranslateElement(
{ pageLanguage: 'ko', autoDisplay: true },
'google_translate_element'
);
};
return () => {
document.body.removeChild(addGoogleTranslateScript);
};
}, []);
// 스크롤 이벤를 home요소에서 상속 안 받으며, 상위 요소로 전파 방지
const handleWheel = (e) => {
e.stopPropagation();
};
const handleLanguageChange = (lang) => {
const value = lang.code;
const gtCombo = document.querySelector('.goog-te-combo');
if (gtCombo) {
gtCombo.value = value;
gtCombo.dispatchEvent(new Event('change'));
}
setChooseCountry(lang);
};
return (
<>
<div id="google_translate_element" style={{ display: 'none' }}></div>
<ButtonCotainer
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<Flag code={chooseCountry.flag} />
{chooseCountry.name}
{/* 번역버튼 hover 시 리스트 표시 */}
{isHovered && (
<LanguageList
onWheel={handleWheel}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{languages.map((lang) => (
<LanguageItem
key={lang.code}
onClick={() => handleLanguageChange(lang)}
>
<Flag code={lang.flag} />
{lang.name}
</LanguageItem>
))}
</LanguageList>
)}
</ButtonCotainer>
</>
);
};
export default GoogleTranslate;
const ButtonCotainer = styled.li`
display: flex;
align-items: center;
gap: 5px;
padding: 5px 5px;
width: 95px;
height: 45px;
cursor: pointer;
background-color: #ffffff;
outline: none;
border: none;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.4);
border-radius: 5px;
font-size: 14px;
position: relative;
z-index: 999;
`;
const LanguageList = styled.ul`
position: absolute;
top: -80vh;
left: 0;
background-color: white;
border: 1px solid #ccc;
list-style: none;
padding: 10px;
margin: 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
width: 100px;
height: 80vh;
overflow-y: auto;
overflow-x: hidden;
border-radius: 8px;
z-index: 1000;
`;
const LanguageItem = styled.li`
cursor: pointer;
padding: 10px 0px;
&:hover {
background-color: #f0f0f0;
}
font-size: 14px;
border-bottom: 1px solid #b7b7b7;
`;
const Flag = styled.div`
width: 30px;
height: 25px;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
background-image: url(${(props) =>
`https://cdn.weglot.com/flags/square/${props.code}.svg`});
`;
2. 사용한 언어 리스트
export const languages = [
{ code: 'ko', name: '한국어', flag: 'kr' }, // 한국어
{ code: 'en', name: 'English', flag: 'us' }, // 영어
{ code: 'fr', name: 'Français', flag: 'fr' }, // 프랑스어
{ code: 'de', name: 'Deutsch', flag: 'de' }, // 독일어
{ code: 'zh', name: '中文(简体)', flag: 'cn' }, // 중국어(간체)
{ code: 'zh-TW', name: '中文(繁體)', flag: 'tw' }, // 중국어(번체, 대만)
{ code: 'ja', name: '日本語', flag: 'jp' }, // 일본어
{ code: 'es', name: 'Español', flag: 'es' }, // 스페인어
{ code: 'ru', name: 'Русский', flag: 'ru' }, // 러시아어
{ code: 'it', name: 'Italiano', flag: 'it' }, // 이탈리아어
{ code: 'pt', name: 'Português', flag: 'pt' }, // 포르투갈어
{ code: 'pt-BR', name: 'Português (BR)', flag: 'br' }, // 포르투갈어 (브라질)
{ code: 'ar', name: 'العربية', flag: 'sa' }, // 아랍어
{ code: 'hi', name: 'हिन्दी', flag: 'in' }, // 힌디어
{ code: 'th', name: 'ไทย', flag: 'th' }, // 태국어
{ code: 'tr', name: 'Türkçe', flag: 'tr' }, // 터키어
{ code: 'el', name: 'Ελληνικά', flag: 'gr' }, // 그리스어
{ code: 'vi', name: 'Tiếng Việt', flag: 'vn' }, // 베트남어
{ code: 'ms', name: 'Bahasa Melayu', flag: 'my' }, // 말레이어
{ code: 'bn', name: 'বাংলা', flag: 'bd' }, // 벵골어
{ code: 'sv', name: 'Svenska', flag: 'se' }, // 스웨덴어
{ code: 'no', name: 'Norsk', flag: 'no' }, // 노르웨이어
{ code: 'fi', name: 'Suomi', flag: 'fi' }, // 핀란드어
{ code: 'da', name: 'Dansk', flag: 'dk' }, // 덴마크어
{ code: 'nl', name: 'Nederlands', flag: 'nl' }, // 네덜란드어
{ code: 'pl', name: 'Polski', flag: 'pl' }, // 폴란드어
{ code: 'cs', name: 'Čeština', flag: 'cz' }, // 체코어
{ code: 'hu', name: 'Magyar', flag: 'hu' }, // 헝가리어
{ code: 'ro', name: 'Română', flag: 'ro' }, // 루마니아어
{ code: 'sk', name: 'Slovenčina', flag: 'sk' }, // 슬로바키아어
{ code: 'bg', name: 'Български', flag: 'bg' }, // 불가리아어
{ code: 'hr', name: 'Hrvatski', flag: 'hr' }, // 크로아티아어
{ code: 'uk', name: 'Українська', flag: 'ua' }, // 우크라이나어
{ code: 'he', name: 'עברית', flag: 'il' }, // 히브리어
{ code: 'fa', name: 'فارسی', flag: 'ir' }, // 페르시아어
{ code: 'sw', name: 'Kiswahili', flag: 'ke' }, // 스와힐리어
{ code: 'tl', name: 'Tagalog', flag: 'ph' }, // 타갈로그어
{ code: 'ur', name: 'اردو', flag: 'pk' }, // 우르두어
{ code: 'am', name: 'አማርኛ', flag: 'et' }, // 암하라어
{ code: 'az', name: 'Azərbaycan', flag: 'az' }, // 아제르바이잔어
{ code: 'kk', name: 'Қазақ тілі', flag: 'kz' }, // 카자흐어
{ code: 'uz', name: 'Oʻzbekcha', flag: 'uz' }, // 우즈베크어
{ code: 'ka', name: 'ქართული', flag: 'ge' }, // 그루지야어 (조지아)
{ code: 'pa', name: 'ਪੰਜਾਬੀ', flag: 'in' }, // 펀자브어 (인도)
{ code: 'mr', name: 'मराठी', flag: 'in' }, // 마라티어 (인도)
{ code: 'ta', name: 'தமிழ்', flag: 'in' }, // 타밀어 (인도)
{ code: 'te', name: 'తెలుగు', flag: 'in' }, // 텔루구어 (인도)
{ code: 'kn', name: 'ಕನ್ನಡ', flag: 'in' }, // 칸나다어 (인도)
{ code: 'ml', name: 'മലയാളം', flag: 'in' }, // 말라얄람어 (인도)
{ code: 'si', name: 'සිංහල', flag: 'lk' }, // 싱할라어 (스리랑카)
{ code: 'my', name: 'မြန်မာ', flag: 'mm' }, // 미얀마어
{ code: 'km', name: 'ភាសាខ្មែរ', flag: 'kh' }, // 크메르어 (캄보디아)
{ code: 'lo', name: 'ລາວ', flag: 'la' }, // 라오스어
{ code: 'mn', name: 'Монгол', flag: 'mn' }, // 몽골어
{ code: 'ne', name: 'नेपाली', flag: 'np' }, // 네팔어
{ code: 'tg', name: 'Тоҷикӣ', flag: 'tj' }, // 타지크어
{ code: 'ps', name: 'پښتو', flag: 'af' }, // 파슈토어 (아프가니스탄)
{ code: 'sq', name: 'Shqip', flag: 'al' }, // 알바니아어
{ code: 'hy', name: 'Հայերեն', flag: 'am' }, // 아르메니아어
];
- 참고
'React.js > React 프로젝트 및 구현' 카테고리의 다른 글
[React.js] 리액트 글자 타이핑 효과 구현 (직접 구현/전체 코드) (0) | 2024.10.27 |
---|---|
[React.js] 리액트 원페이지 스크롤 구현 (직접 구현/스크롤 유도) (4) | 2024.10.27 |
[React.js] vite.config.js에서 env사용하기 (0) | 2024.10.13 |
[React.js] 리액트 모달 만들기/ 배경 흐리게/ 간단하게 (0) | 2024.10.06 |
[React.js] input checkbox css / 간단하게 checkbox css 구현 ( styled-components) (0) | 2024.08.19 |