feat: 언어 변경 추가

This commit is contained in:
“hyeonggkim”
2025-09-19 19:11:01 +09:00
parent ce08a58118
commit 2ea8b69f7b
10 changed files with 238 additions and 32 deletions

View File

@@ -4,6 +4,7 @@ import { getLayoutType } from '#layers/utils/dataUtil'
const pageDataStore = usePageDataStore()
const { pageData } = storeToRefs(pageDataStore)
console.log("🚀 d2 index ~ pageData:", pageData)
const currentLayout = computed(() => getLayoutType(pageData.value))

View File

@@ -2,7 +2,9 @@
<div class="bg-white">
<select
v-model="selectedLocale"
:disabled="isChanging"
class="text-black px-2 py-1 rounded-md"
:class="{ 'opacity-50 cursor-not-allowed': isChanging }"
@change="switchLanguage"
>
<option
@@ -13,6 +15,9 @@
{{ localeOption }}
</option>
</select>
<span v-if="isChanging" class="ml-2 text-sm text-gray-500">
변경 중...
</span>
</div>
</template>
@@ -24,24 +29,40 @@ const availableLanguages = computed(() => {
return gameDataStore.gameData?.lang_codes || ['ko']
})
const { locale } = useI18n()
const { locale, setLocale } = useI18n()
const switchLocalePath = useSwitchLocalePath()
const router = useRouter()
const pageDataStore = usePageDataStore()
const selectedLocale = ref(locale.value)
const isChanging = ref(false)
// 언어 변경 함수
// 언어 변경 함수 (CSR 방식)
const switchLanguage = async () => {
console.log(
'🚀 ~ switchLanguage ~ selectedLocale.value:',
selectedLocale.value
)
if (selectedLocale.value) {
if (!selectedLocale.value || isChanging.value) return
isChanging.value = true
try {
// URL 경로를 통해 언어 변경
const path = switchLocalePath(selectedLocale.value)
if (path) {
await router.push(path)
// 페이지 데이터 초기화 (새로운 언어로 다시 로드되도록)
pageDataStore.clearPageData()
// 언어 변경 및 라우팅
await setLocale(selectedLocale.value)
// await router.push(path)
// 페이지 새로고침을 통해 데이터 재로드 보장
await nextTick()
window.location.reload()
}
} catch {
// 오류 발생 시 이전 언어로 복원
selectedLocale.value = locale.value
} finally {
isChanging.value = false
}
}

View File

@@ -12,7 +12,7 @@ const extractGameAliasFromHost = (host: string, baseDomain: string): string => {
return ''
}
const subdomain = host.split('.')[0]
const subdomain = host.split(':')[0]
return subdomain && subdomain !== 'www' ? subdomain : ''
}

View File

@@ -7,16 +7,18 @@ export const usePathResolver = () => {
// URL에서 언어 코드 패턴을 찾아서 그 뒤의 경로를 추출
// 예: /ko/about/story -> /about/story
// 예: /en/test/page -> /test/page
// 예: /zh-tw/about/story -> /about/story
// 예: /zh-cn/test/page -> /test/page
// 예: /ko -> "" (빈 문자열)
const languagePattern = /^\/[a-z]{2}\/(.+)$/
const languagePattern = /^\/[a-z]{2}(-[a-z]{2})?\/(.+)$/
const match = targetUrl.match(languagePattern)
if (match && match[1]) {
return `/${match[1]}`
if (match && match[2]) {
return `/${match[2]}`
}
// 언어 코드만 있고 뒤에 아무것도 없는 경우 (예: /ko, /en)
const languageOnlyPattern = /^\/[a-z]{2}$/
// 언어 코드만 있고 뒤에 아무것도 없는 경우 (예: /ko, /en, /zh-tw, /zh-cn)
const languageOnlyPattern = /^\/[a-z]{2}(-[a-z]{2})?$/
if (languageOnlyPattern.test(targetUrl)) {
return ''
}

View File

@@ -1,4 +1,6 @@
<script setup lang="ts"></script>
<script setup lang="ts">
console.log("🚀 ~ promotion 22222222")
</script>
<template>
<div class="promotion-wrap">

View File

@@ -9,24 +9,28 @@ export default defineNuxtRouteMiddleware(async (to, _from) => {
const store = usePageDataStore()
const gameAlias = useGetGameAlias()
const { getPathAfterLanguage } = usePathResolver()
const headers = useRequestHeaders()
const langCode = ssrGetFinalLocale(to.path, headers)
const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v2.0/template/page`
try {
const pageUrl = getPathAfterLanguage(to.path)
console.log("🚀 ~ pageUrl:", pageUrl)
// pageUrl이 빈값이거나 null이면 /brand로 리다이렉트
if (!pageUrl || pageUrl === '' || pageUrl === '/') {
return navigateTo('/brand', { replace: true })
return navigateTo(`/${langCode}/brand`, { replace: true })
}
const queryParams: Record<string, string> = {
game_alias: gameAlias,
lang_code: 'ko',
game_domain: gameAlias,
lang_code: langCode,
page_url: pageUrl,
_t: Date.now().toString(), // 캐시 무효화를 위한 타임스탬프
}
console.log("🚀 ~ queryParams:", queryParams)
const response = (await commonFetch('GET', apiUrl, {
query: queryParams,

View File

@@ -5,6 +5,8 @@ import {
getRequestURL,
} from 'h3'
import { ssrGetFinalLocale } from '../../utils/localeUtil'
export default defineEventHandler(async event => {
const url = getRequestURL(event)
@@ -25,7 +27,7 @@ export default defineEventHandler(async event => {
const isGameAliasExtractable = host.includes(baseDomain)
if (isGameAliasExtractable) {
const gameAlias = host.split('.')[0]
const gameAlias = host.split(':')[0]
if (gameAlias && gameAlias !== 'www') {
event.context.gameAlias = gameAlias
@@ -38,21 +40,23 @@ export default defineEventHandler(async event => {
const stoveApiBaseUrl = config.public.stoveApiUrl
const apiUrl = `${stoveApiBaseUrl}/pub-comm/v1.0/template/game`
const langCode = ssrGetFinalLocale(event?.node.req.url, event.node.req.headers)
// URL의 첫 번째 path를 lang_code로 사용 (파비콘, API 경로 제외)
const url = getRequestURL(event)
const pathSegments = url.pathname
.split('/')
.filter(
segment =>
segment &&
!segment.includes('favicon') &&
!segment.includes('api') &&
!segment.startsWith('_')
)
const langCode = pathSegments[0] || 'ko'
// const pathSegments = url.pathname
// .split('/')
// .filter(
// segment =>
// segment &&
// !segment.includes('favicon') &&
// !segment.includes('api') &&
// !segment.startsWith('_')
// )
// const langCode = pathSegments[0] || 'ko'
const queryParams: Record<string, string> = {
game_alias: event.context.gameAlias || '',
game_domain: event.context.gameAlias || '',
lang_code: langCode,
}

View File

@@ -2,18 +2,25 @@ import type { GameDataValue } from '#layers/types/api/gameData'
export const useGameDataStore = defineStore('gameData', () => {
const gameData = ref<GameDataValue | null>(null)
const langCode = ref<string | null>(null)
const setGameData = (data: GameDataValue) => {
gameData.value = data
}
const setLangCode = (data: string) => {
langCode.value = data
}
const clearGameData = () => {
gameData.value = null
}
return {
langCode,
gameData,
setGameData,
setLangCode,
clearGameData,
}
})

View File

@@ -10,7 +10,6 @@ export const getLayoutType = (
return pageData?.page_type === 1 ? 'default' : 'promotion'
}
// 이미지 호스트 리턴하는 함수
// [TODO] 환경변수 처리 수정
export const getResolvedHost = (path: string): string => {
const config = useRuntimeConfig()

166
layers/utils/localeUtil.ts Normal file
View File

@@ -0,0 +1,166 @@
import { DEFAULT_LOCALE_CODE, DEFAULT_COVERAGES } from '@/i18n.config'
// 사용자 선호 언어 가져오기
export const getPreferredLanguage = (acceptLanguageHeader = '') => {
const languages = acceptLanguageHeader
.split(',')
.map((lang) => {
const [code, priority = 'q=1'] = lang.trim().split(';q=')
return { code, priority: parseFloat(priority) }
})
.sort((a, b) => b.priority - a.priority)
return languages.length > 0 ? languages[0].code : null
}
export const getFinalLanguage = (path = '', defaultLocale: string, coverages: string[]) => {
// const nuxtApp = useNuxtApp()
let finalLocale = ''
let requestedLocale
let acceptLanguage: string
let defaultLang = 'en'
let defaultLangEn: string
if (defaultLocale) {
defaultLangEn = defaultLocale
} else {
defaultLangEn = 'en'
}
requestedLocale = path?.split('/')[1]?.toLowerCase() ?? 'undefined'
if (import.meta.server) {
const headers = useRequestHeaders(['accept-language'])
acceptLanguage = headers['accept-language'] || defaultLangEn
defaultLang =
coverages.find((locale: string) => getPreferredLanguage(acceptLanguage)?.startsWith(locale)) || defaultLangEn
}
// const DEFAULT_COVERAGES = i18n.locales.map((locale) => locale.code)
const DEFAULT_COVERAGES = coverages
const requestedPage = path?.split('/')[2]?.toLowerCase() ?? undefined
const localeMap: Record<string, string> = {
'zh-tw': 'zh-TW',
'zh-cn': 'zh-CN'
}
if (localeMap[requestedLocale]) {
requestedLocale = localeMap[requestedLocale]
}
if (requestedLocale !== undefined && DEFAULT_COVERAGES.includes(requestedLocale)) {
finalLocale = requestedLocale
} else if (
requestedLocale === undefined ||
requestedLocale === '' ||
path !== '' ||
(requestedLocale !== undefined && !DEFAULT_COVERAGES.includes(requestedLocale) && requestedPage !== undefined)
) {
// 요청된 언어가 없을 때 or 잘못된 언어코드로 요청 시 브라우저 언어로 설정
finalLocale = defaultLang
} else {
// 그 외의 경우 기본 언어로 설정 (중국어 번체)
finalLocale = defaultLangEn
}
return finalLocale.toLowerCase()
}
/**
* 우선순위에 따른 언어 조회 (CSR)
*
* @param {string} path - 현재 URL 경로
*/
export const csrGetFinalLocale = (path = '') => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
const localeMap: Record<string, string> = {
'zh-tw': 'zh-TW',
'zh-cn': 'zh-CN'
}
// 1. URL 패스에 포함된 언어
if (path && path !== '' && path.split('/').length > 1) {
const pathLocal = path.split('/')[1]
// URL 패스에 포함된 언어가 지원하는 언어인지 체크
if (pathLocal && pathLocal !== '' && DEFAULT_COVERAGES.includes(pathLocal)) {
finalLocale = pathLocal // .toLowerCase()
if (localeMap[pathLocal]) {
finalLocale = localeMap[pathLocal]
}
}
return finalLocale
}
// 2. 브라우저 언어
const browserLanguage = navigator.language || navigator.languages[0]
if (browserLanguage && browserLanguage !== '' && DEFAULT_COVERAGES.includes(browserLanguage)) {
finalLocale = browserLanguage // .toLowerCase()
if (localeMap[browserLanguage]) {
finalLocale = localeMap[browserLanguage]
}
return finalLocale
}
// 3. 서비스 기본 언어
finalLocale = DEFAULT_LOCALE_CODE
return finalLocale
}
/**
* 우선순위에 따른 언어 조회 (SSR)
*
* @param {string} path - 현재 URL 경로
* @param {any} headers - 요청 헤더
*/
export const ssrGetFinalLocale = (path = '', headers: any) => {
let finalLocale = DEFAULT_LOCALE_CODE // 기본값 설정
try {
// 1. URL path에 포함된 언어 정보
if (path && path !== '' && path.split('/').length > 1) {
const pathLocale = path.split('/')[1]
// URL path에 포함된 언어 정보가 지원하는 언어인지 체크
if (pathLocale && pathLocale !== '' && DEFAULT_COVERAGES.includes(pathLocale)) {
finalLocale = pathLocale // .toLowerCase()
return finalLocale
}
}
// 2. 요청 헤더의 브라우저 언어 (accept-language)
if (headers && headers['accept-language']) {
const acceptLanguage = Array.isArray(headers['accept-language'])
? headers['accept-language'][0]
: headers['accept-language']
if (acceptLanguage && typeof acceptLanguage === 'string') {
const preferredLocale = getPreferredLanguage(acceptLanguage)
if (preferredLocale) {
// 선호 언어의 기본 코드와 일치하는 지원 로케일 찾기
const matchedLocale = DEFAULT_COVERAGES.find((locale: string) =>
preferredLocale.toLowerCase().startsWith(locale.toLowerCase())
)
if (matchedLocale) {
finalLocale = matchedLocale
// return matchedLocale.toLowerCase()
return finalLocale
}
}
}
}
// 3. 서비스 기본 언어
finalLocale = DEFAULT_LOCALE_CODE
} catch (e) {
console.error('[Exception] localeUtil.ssrGetFinalLocale: ', e)
finalLocale = DEFAULT_LOCALE_CODE
}
return finalLocale
}