ㆍProject Diary/Vue (Roblox WebSite)
Vue.js와 Vue I18n을 사용하여 다국어 지원을 구현하는 방법 기록
1. Vue I18n 설정
먼저, Vue I18n을 설치하고 설정합니다. Vue I18n은 Vue.js 애플리케이션에서 국제화를 쉽게 구현할 수 있게 해주는 라이브러리입니다.
i18n.js 파일
i18n.js 파일에서는 다국어 JSON 파일을 로드하여 Vue I18n에 등록하는 기능을 포함합니다. messages 객체에 각 언어의 번역 데이터를 저장합니다
import Vue from "vue";
import VueI18n from "vue-i18n";
import axios from "axios";
Vue.use(VueI18n);
const messages = {
ko: {},
en: {},
};
const languages = ["ko", "en"];
const fileNames = [
"Careers",
"Education",
"Footer",
"Header",
"Home",
"Members",
"News",
"TechTalks",
];
const loadTranslation = async () => {
for (let lang of languages) {
for (let fileName of fileNames) {
await axios
.get(`./assets/locales/${lang}/${fileName}.json`) // JSON 파일 경로
.then((res) => {
if (lang === "ko") {
messages.ko[fileName] = res.data; // 한국어 번역 데이터 저장
} else {
messages.en[fileName] = res.data; // 영어 번역 데이터 저장
}
})
.catch((err) => {
console.log(err);
});
}
}
};
loadTranslation().then(() => {
console.log(messages.en);
console.log(messages.ko);
});
export const i18n = new VueI18n({
locale: "ko", // 기본 언어
fallbackLocale: "en", // 기본 언어 표시에 문제가 있을 경우 대체할 언어
messages,
});
이 코드는 다국어 JSON 파일을 로드하여 messages 객체에 저장하고, Vue I18n 인스턴스를 생성하여 설정합니다.
2. JSON 파일 준비
다국어 번역 데이터를 포함하는 JSON 파일을 준비합니다. 예시로 Careers.json 파일을 준비했습니다. 모든 페이지에 대해 유사한 형식의 JSON 파일을 준비합니다.
Careers.json (en)
{
"aboutus": {
"h3text1": "Work at Roblox",
"h3text2": "Be part of a values-driven company",
"h3text3": "Join our culture of continuous learning",
"h3text4": "Do career-defining work",
"h3text5": "Focus on your well-being",
// 생략
},
"recruitinfo": {
// 생략
},
"story": {
// 생략
}
}
Careers.json (ko)
{
"aboutus": {
"h3text1": "Roblox에서 근무",
"h3text2": "가치 중심 기업의 일원이 되세요",
"h3text3": "지속적인 학습 문화에 참여하세요",
"h3text4": "경력을 정의하는 일을 하세요",
"h3text5": "당신의 웰빙에 집중하세요",
//생략
},
"recruitinfo": {
//생략
},
"story": {
//생략
}
}
3. 컴포넌트에서 다국어 사용하기
다국어 지원을 적용하기 위해 컴포넌트에서 Vue I18n을 사용합니다. v-t 디렉티브나 $t 메서드를 사용하여 텍스트를 번역합니다.
<template>
<section id="career__story" :class="{ dark: changeDarkMode }">
<div class="slide__outer">
<div class="slide__inner">
<h2 :class="{ dark: changeDarkMode }">{{ $t("[0].story.h2") }}</h2>
<swiper class="swiper" :options="swiperOption">
<Swiper-slide
class="slideBox1"
:class="{ dark: changeDarkMode }"
v-for="(slider, index) in sliders"
:key="index"
>
<div class="slideBox2">
<img :src="slider.image" alt="" />
<h3>{{ $t(slider.h3) }}</h3>
<a :href="slider.link" target="_blank">{{ $t("[0].story.a") }}</a>
</div>
</Swiper-slide>
<div class="swiper-pagination" slot="pagination"></div>
<div class="swiper-button-prev" slot="button-prev"></div>
<div class="swiper-button-next" slot="button-next"></div>
</swiper>
</div>
</div>
</section>
</template>
<script>
import { Swiper, SwiperSlide } from "vue-awesome-swiper";
import "@/assets/css/myswiper.css";
export default {
name: "ValueOfRoblox",
components: {
Swiper,
SwiperSlide,
},
data() {
return {
sliders: [
{
image: "./assets/images/career-story/RBLX_EOY_BlogHeader_Final-1024x576.webp",
h3: "[0].story.first-h3",
link: "https://blog.roblox.com/2023/12/2023-year-review-letter-ceo/",
},
{
image: "./assets/images/career-story/RBLX_RDC-Banner_-Blog-1.webp",
h3: "[0].story.second-h3",
link: "https://blog.roblox.com/2023/09/rdc-2023-roblox-going-next/",
},
{
image: "./assets/images/career-story/AI-Blog-Header.webp",
h3: "[0].story.third-h3",
link: "https://blog.roblox.com/2023/09/revolutionizing-creation-roblox/",
},
{
image: "./assets/images/career-story/_Roblox-Career-Center-Header._001-1.webp",
h3: "[0].story.fourth-h3",
link: "https://blog.roblox.com/2023/08/introducing-roblox-career-center/",
},
{
image: "./assets/images/career-story/image 1.webp",
h3: "[0].story.fifth-h3",
link: "https://www.fastcompany.com/90878692/roblox-grows-up",
},
{
image: "./assets/images/career-story/image 2.webp",
h3: "[0].story.sixth-h3",
link: "https://blog.roblox.com/2023/05/our-vision-for-all-ages/",
},
],
swiperOption: {
slidesPerView: 1,
spaceBetween: 30,
slidesPerGroup: 1,
freemode: true,
effect: "slide", // slide, cube, coverflow, flip
autoplay: {
delay: 5000,
},
loop: true,
pagination: {
el: ".swiper-pagination",
clickable: true,
},
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
breakpoints: {
393: {
slidesPerView: 3,
slidesPerGroup: 1,
centeredSlides: true,
},
},
},
};
},
computed: {
changeDarkMode() {
return this.$store.getters.fnGetDark;
},
},
};
</script>
{{ $t("[0].story.h2") }}와 같은 표현식을 사용합니다.
이 표현식은 다국어 JSON 파일에서 번역된 텍스트를 가져와 표시합니다. 컴포넌트 내의 텍스트뿐만 아니라 data 객체에서도 다국어 처리를 할 수 있습니다.
4. 다국어 처리 적용 예시
다음은 HTML 태그와 data에서 다국어 처리를 적용한 예시입니다.
<template>
<header id="header">
<div class="member" :class="{ dark: changeDarkMode }" v-if="token">
<a href="#" @click.prevent="logOut">{{ $t("[3].logout") }} <i class="fa-solid fa-right-from-bracket"></i></a>
</div>
<div class="member" :class="{ dark: changeDarkMode }" v-else>
<router-link to="/loginView">{{ $t("[3].login") }}</router-link>
<router-link to="/joinView">{{ $t("[3].join") }}</router-link>
</div>
<nav id="header__nav">
<h1 class="header__logo">
<router-link to="/"><img :src="imgUrl" alt="roblox logo" /></router-link>
</h1>
<div class="menu__open" @click="menuOpen">
<span class="blind">{{ $t("[3].openMenu") }}</span>
<i :class="openClass"></i>
</div>
<ul class="header__menuBar" :class="{ on: openNav }">
<li v-for="(menu, index) in menus" :key="index" @click="closeNav">
<router-link :to="menu.to" v-if="!menu.submenus">
{{ $t(menu.name) }}
</router-link>
<p v-else @click="toggleSubMenu(index)" class="globe" @click.stop>
<i class="fa-solid fa-globe"></i>
</p>
<ul v-if="activeMenu === index" class="globeLanguage">
<li v-for="(submenu, subIndex) in menu.submenus" :key="subIndex" @click="changeLanguage(submenu.en, index)" style="cursor: pointer">
{{ $t(submenu.ko) }}
</li>
</ul>
</li>
<li>
<p class="dark dark__mode" :class="{ light: changeDarkMode }" @click="onOff" @click.stop>
<i class="fa-solid fa-moon"></i>
<span>{{ $t("[3].darkmode") }}</span>
</p>
</li>
</ul>
</nav>
</header>
</template>
<script>
export default {
name: "Header",
data() {
return {
menus: [
{ name: "[3].name1", to: "/members" },
{ name: "[3].name2", to: "/news" },
{ name: "[3].name3", to: "/techtalks" },
{ name: "[3].name4", to: "/education" },
{ name: "[3].name5", to: "/careers" },
{ name: "[3].name6", to: "/qna" },
{
name: "globe",
submenus: [
{ ko: "한국어", en: "ko" },
{ ko: "English", en: "en" },
],
},
],
activeMenu: null,
openClass: "fa-solid fa-bars",
openNav: false,
imgUrl: "./assets/images/roblox_logo_white_new.svg",
};
},
methods: {
closeNav() {
this.openNav = false;
this.$emit("openNav", false);
this.openClass = "fa-solid fa-bars";
},
menuOpen() {
if (this.openClass === "fa-solid fa-bars") {
this.openClass = "fa-solid fa-times";
this.openNav = true;
this.$emit("openNav", true);
} else {
this.openClass = "fa-solid fa-bars";
this.openNav = false;
this.$emit("openNav", false);
}
},
toggleSubMenu(index) {
if (this.activeMenu === index) {
this.activeMenu = null;
} else {
this.activeMenu = index;
}
},
onOff() {
this.$store.commit("on__ChangeDark");
},
changeLanguage(locale, index) {
this.$i18n.locale = locale; // 언어 변경
this.$store.commit("on__Click", locale);
if (this.activeMenu === index) {
this.activeMenu = null;
} else {
this.activeMenu = index;
}
},
logOut() {
this.token = false;
this.$store.commit("fnLogout");
window.location.reload();
},
},
computed: {
changeDarkMode() {
return this.$store.getters.fnGetDark;
},
token() {
return this.$store.getters.fnGetLogined;
},
},
};
</script>
위 예시에서 중요한 부분은 메뉴 항목과 관련된 텍스트를 다국어 처리한 부분입니다.
{{ $t(menu.name) }}와 같이 v-t 디렉티브나 $t 메서드를 사용하여 다국어 처리를 합니다.
또한, 언어 변경 버튼을 클릭하면 changeLanguage 메서드를 호출하여 Vue I18n의 locale을 변경합니다.
5. 다국어 JSON 파일 경로 설정 및 데이터 로드 이유
JSON 파일을 각각의 언어 폴더에 저장하고, 이 파일들을 로드하여 Vue I18n에 등록하는 이유는 다음과 같습니다:
- 유지보수 용이성: 번역 데이터를 별도의 JSON 파일로 분리하면 번역 내용을 쉽게 업데이트하고 관리할 수 있습니다.
- 확장성: 새로운 언어를 추가할 때, 해당 언어의 JSON 파일만 추가하면 되므로 확장성이 뛰어납니다.
- 성능: 필요한 언어의 JSON 파일만 로드하여 메모리를 효율적으로 사용합니다.