remove vue-i18n

This commit is contained in:
2026-03-06 00:08:51 +07:00
parent dba9713d96
commit bbe15d5f3e
15 changed files with 2307 additions and 2289 deletions

View File

@@ -9,8 +9,7 @@ const readAppData = () => {
async function render() {
const appData = readAppData();
const { app, router, queryCache, pinia } = createApp(appData.$locale);
const { app, router, queryCache, pinia } = await createApp(appData.$locale);
pinia.use(PiniaSharedState({ enable: true, initialize: true }));
hydrateQueryCache(queryCache, appData.$colada || {});

View File

@@ -1,7 +0,0 @@
export const supportedLocales = ['en', 'vi'] as const;
export type SupportedLocale = (typeof supportedLocales)[number];
export const defaultLocale: SupportedLocale = 'en';
export const localeCookieKey = 'lang';

View File

@@ -1,75 +0,0 @@
import { createI18n as createVueI18n } from 'vue-i18n';
import type { SupportedLocale } from './constants';
import { defaultLocale, supportedLocales } from './constants';
import en from './messages/en';
import vi from './messages/vi';
export const i18nMessages = {
en,
vi,
} as const;
let activeI18n: ReturnType<typeof createI18n> | null = null;
const normalizeLocaleToken = (locale?: string | null): string | undefined => {
if (!locale) return undefined;
return locale
.trim()
.toLowerCase()
.replace('_', '-');
};
export const toSupportedLocale = (locale?: string | null): SupportedLocale | undefined => {
const normalized = normalizeLocaleToken(locale);
if (!normalized) return undefined;
const direct = supportedLocales.find(item => item === normalized);
if (direct) return direct;
const base = normalized.split('-')[0];
return supportedLocales.find(item => item === base);
};
export const normalizeLocale = (locale?: string | null): SupportedLocale => {
return toSupportedLocale(locale) ?? defaultLocale;
};
export const resolveLocaleFromAcceptLanguage = (acceptLanguage?: string | null): SupportedLocale | undefined => {
if (!acceptLanguage) return undefined;
const candidates = acceptLanguage
.split(',')
.map((part) => {
const [rawLocale, ...params] = part.trim().split(';');
const qParam = params.find(param => param.trim().startsWith('q='));
const quality = qParam ? Number.parseFloat(qParam.split('=')[1] ?? '1') : 1;
return {
locale: rawLocale,
quality: Number.isFinite(quality) ? quality : 1,
};
})
.sort((a, b) => b.quality - a.quality);
for (const candidate of candidates) {
const matched = toSupportedLocale(candidate.locale);
if (matched) return matched;
}
return undefined;
};
export const createI18n = (initialLocale?: string | null) => {
const locale = normalizeLocale(initialLocale);
const i18n = createVueI18n({
legacy: false,
locale,
fallbackLocale: defaultLocale,
messages: i18nMessages,
});
activeI18n = i18n;
return i18n;
};
export const getActiveI18n = () => activeI18n;
export type AppI18n = ReturnType<typeof createI18n>;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
import i18next from "i18next";
import I18NextHttpBackend from "i18next-http-backend";
import LanguageDetector from "i18next-browser-languagedetector";
const i18n = i18next.createInstance();
i18n
.use(I18NextHttpBackend)
.use(LanguageDetector)
.init({
supportedLngs: ["en", "vi"],
fallbackLng: "en",
defaultNS: "common",
ns: [
"common",
"app",
"auth",
"nav",
"settings",
"pageHeader",
"confirm",
"toast",
"overview",
"video",
"notification",
"upload",
"home",
"legal",
"notFound",
],
interpolation: {
escapeValue: false,
},
backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // dynamic fetch JSON
},
});
export default i18n;

View File

@@ -4,7 +4,10 @@ import { createHead as SSRHead } from '@unhead/vue/server';
import { createPinia } from 'pinia';
import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router';
import { createI18n, normalizeLocale } from './i18n';
import I18NextVue from 'i18next-vue';
import i18next from '@/lib/translation';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import createAppRouter from './routes';
@@ -15,18 +18,13 @@ const getSerializedAppData = () => {
return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record<string, any>;
};
export function createApp(initialLocale?: string | null) {
export async function createApp(lng: string = 'en') {
const pinia = createPinia();
const app = createSSRApp(withErrorBoundary(RouterView));
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
const appData = !import.meta.env.SSR ? getSerializedAppData() : ({} as Record<string, any>);
const resolvedInitialLocale = initialLocale
?? (!import.meta.env.SSR ? appData.$locale : undefined)
?? undefined;
const i18n = createI18n(normalizeLocale(resolvedInitialLocale));
app.use(head);
app.directive('nh', {
created(el) {
@@ -34,7 +32,8 @@ export function createApp(initialLocale?: string | null) {
}
});
app.use(pinia);
app.use(i18n);
await i18next.init({lng});
app.use(I18NextVue, {i18next});
app.use(PiniaColada, {
pinia,
plugins: [
@@ -62,5 +61,5 @@ export function createApp(initialLocale?: string | null) {
}
}
return { app, router, head, pinia, bodyClass, queryCache, i18n };
return { app, router, head, pinia, bodyClass, queryCache };
}

View File

@@ -8,9 +8,9 @@
{{ content[route.name as keyof typeof content.value]?.title || '' }}
</h2>
<vue-head :input="{
title: content.value[route.name as keyof typeof content.value]?.headTitle || t('app.name'),
title: content[route.name as keyof typeof content.value]?.headTitle || t('app.name'),
meta: [
{ name: 'description', content: content.value[route.name as keyof typeof content.value]?.subtitle || '' }
{ name: 'description', content: content[route.name as keyof typeof content.value]?.subtitle || '' }
]
}" />
</div>
@@ -23,12 +23,12 @@
</div>
</template>
<script setup lang="ts">
import { useTranslation } from 'i18next-vue';
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
const route = useRoute();
const { t } = useI18n();
const { t } = useTranslation();
const content = computed(() => ({
login: {

View File

@@ -2,9 +2,16 @@ import { contextStorage } from 'hono/context-storage';
import { cors } from 'hono/cors';
import isMobile from 'is-mobile';
import type { Hono } from 'hono';
import { languageDetector } from 'hono/language';
export function setupMiddlewares(app: Hono) {
app.use('*', contextStorage());
app.use('*', languageDetector({
supportedLanguages: ['vi', 'en'],
fallbackLanguage: 'en',
lookupCookie: 'i18next',
lookupFromHeaderKey: 'accept-language',
order: ['cookie', 'header'],
}) ,contextStorage());
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));

View File

@@ -3,12 +3,10 @@ import { renderSSRHead } from '@unhead/vue/server';
import { streamText } from 'hono/streaming';
import { renderToWebStream } from 'vue/server-renderer';
import { createApp } from '@/main';
import { defaultLocale, localeCookieKey } from '@/i18n/constants';
import { normalizeLocale, resolveLocaleFromAcceptLanguage } from '@/i18n';
import { useAuthStore } from '@/stores/auth';
import { buildBootstrapScript } from '@/lib/manifest';
import { createApp } from '@/main';
import { htmlEscape } from '@/server/utils/htmlEscape';
import { useAuthStore } from '@/stores/auth';
import type { Hono } from 'hono';
const parseCookie = (cookieHeader: string | undefined, key: string): string | undefined => {
@@ -22,22 +20,12 @@ const parseCookie = (cookieHeader: string | undefined, key: string): string | un
return undefined;
};
const resolveLocaleFromAuthUser = (authUser: unknown): string | undefined => {
if (!authUser || typeof authUser !== 'object') return undefined;
const maybeLanguage = (authUser as any).language ?? (authUser as any).locale;
return typeof maybeLanguage === 'string' ? maybeLanguage : undefined;
};
export function registerSSRRoutes(app: Hono) {
app.get("*", async (c) => {
const nonce = crypto.randomUUID();
const url = new URL(c.req.url);
const cookieLocaleRaw = parseCookie(c.req.header('cookie'), localeCookieKey);
const acceptLocale = resolveLocaleFromAcceptLanguage(c.req.header('accept-language'));
const bootstrapLocale = normalizeLocale(cookieLocaleRaw ?? acceptLocale ?? defaultLocale);
const { app: vueApp, router, head, pinia, bodyClass, queryCache, i18n } = createApp(bootstrapLocale);
const lang = c.get("language")
const { app: vueApp, router, head, pinia, bodyClass, queryCache } = await createApp(lang);
vueApp.provide("honoContext", c);
@@ -45,10 +33,6 @@ export function registerSSRRoutes(app: Hono) {
auth.$reset();
await auth.init();
const userPreferredLocale = resolveLocaleFromAuthUser(auth.user);
const resolvedLocale = normalizeLocale(userPreferredLocale ?? cookieLocaleRaw ?? acceptLocale ?? defaultLocale);
i18n.global.locale.value = resolvedLocale;
await router.push(url.pathname);
await router.isReady();
@@ -60,7 +44,7 @@ export function registerSSRRoutes(app: Hono) {
const appStream = renderToWebStream(vueApp, ctx);
// HTML Head
await stream.write(`<!DOCTYPE html><html lang='${resolvedLocale}'><head>`);
await stream.write(`<!DOCTYPE html><html lang='${lang}'><head>`);
await stream.write("<base href='" + url.origin + "'/>");
// SSR Head tags
@@ -90,7 +74,7 @@ export function registerSSRRoutes(app: Hono) {
Object.assign(ctx, {
$p: pinia.state.value,
$colada: serializeQueryCache(queryCache),
$locale: resolvedLocale,
$locale: lang,
});
// App data script