feat: 언어 변경 추가
This commit is contained in:
@@ -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))
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 : ''
|
||||
}
|
||||
|
||||
|
||||
@@ -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 ''
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
console.log("🚀 ~ promotion 22222222")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="promotion-wrap">
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
166
layers/utils/localeUtil.ts
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user