develop-updateui #1
3
bun.lock
3
bun.lock
@@ -5,6 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "holistream",
|
"name": "holistream",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.19.11",
|
||||||
"@pinia/colada": "^0.21.7",
|
"@pinia/colada": "^0.21.7",
|
||||||
"@unhead/vue": "^2.1.10",
|
"@unhead/vue": "^2.1.10",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
@@ -169,6 +170,8 @@
|
|||||||
|
|
||||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||||
|
|
||||||
|
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||||
|
|
||||||
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
"@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="],
|
||||||
|
|
||||||
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
"@iconify/utils": ["@iconify/utils@3.1.0", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "mlly": "^1.8.0" } }, "sha512-Zlzem1ZXhI1iHeeERabLNzBHdOa4VhQbqAcOQaMKuTuyZCpwKbC2R4Dd0Zo3g9EAc+Y4fiarO8HIHRAth7+skw=="],
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"tail": "wrangler tail"
|
"tail": "wrangler tail"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hono/node-server": "^1.19.11",
|
||||||
"@pinia/colada": "^0.21.7",
|
"@pinia/colada": "^0.21.7",
|
||||||
"@unhead/vue": "^2.1.10",
|
"@unhead/vue": "^2.1.10",
|
||||||
"@vueuse/core": "^14.2.1",
|
"@vueuse/core": "^14.2.1",
|
||||||
|
|||||||
@@ -13,10 +13,6 @@ async function render() {
|
|||||||
pinia.use(PiniaSharedState({ enable: true, initialize: true }));
|
pinia.use(PiniaSharedState({ enable: true, initialize: true }));
|
||||||
hydrateQueryCache(queryCache, appData.$colada || {});
|
hydrateQueryCache(queryCache, appData.$colada || {});
|
||||||
|
|
||||||
Object.entries(appData).forEach(([key, value]) => {
|
|
||||||
(window as any)[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
await router.isReady();
|
await router.isReady();
|
||||||
app.mount('body', true);
|
app.mount('body', true);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { computed, reactive, readonly } from 'vue';
|
import { computed, reactive, readonly } from 'vue';
|
||||||
import { getActiveI18n } from '@/i18n';
|
|
||||||
|
|
||||||
export type AppConfirmOptions = {
|
export type AppConfirmOptions = {
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { getActiveI18n } from '@/i18n';
|
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
export interface QueueItem {
|
export interface QueueItem {
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
import type { i18n as I18nInstance } from 'i18next';
|
|
||||||
|
|
||||||
import { getClientI18nInstance } from '@/lib/translation/client';
|
|
||||||
|
|
||||||
import { defaultLocale, supportedLocales, type SupportedLocale } from './constants';
|
|
||||||
|
|
||||||
export { supportedLocales, defaultLocale } from './constants';
|
|
||||||
export type { SupportedLocale } from './constants';
|
|
||||||
export { localeCookieKey } from './constants';
|
|
||||||
|
|
||||||
export const normalizeLocale = (locale?: string): SupportedLocale => {
|
|
||||||
if (!locale) return defaultLocale;
|
|
||||||
const normalized = locale.toLowerCase().split('-')[0] as SupportedLocale;
|
|
||||||
return supportedLocales.includes(normalized) ? normalized : defaultLocale;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getActiveI18n = (): I18nInstance | undefined => {
|
|
||||||
return import.meta.env.SSR ? undefined : getClientI18nInstance();
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Hono } from 'hono';
|
import { Hono } from 'hono';
|
||||||
|
|
||||||
|
import { serveStatic } from "@hono/node-server/serve-static";
|
||||||
import { apiProxyMiddleware } from './server/middlewares/apiProxy';
|
import { apiProxyMiddleware } from './server/middlewares/apiProxy';
|
||||||
import { setupMiddlewares } from './server/middlewares/setup';
|
import { setupMiddlewares } from './server/middlewares/setup';
|
||||||
import { registerDisplayRoutes } from './server/routes/display';
|
import { registerDisplayRoutes } from './server/routes/display';
|
||||||
@@ -15,7 +16,7 @@ setupMiddlewares(app);
|
|||||||
|
|
||||||
// API proxy middleware (handles /r/*)
|
// API proxy middleware (handles /r/*)
|
||||||
app.use(apiProxyMiddleware);
|
app.use(apiProxyMiddleware);
|
||||||
|
app.use(serveStatic({ root: './public' }))
|
||||||
// Routes
|
// Routes
|
||||||
registerWellKnownRoutes(app);
|
registerWellKnownRoutes(app);
|
||||||
registerMergeRoutes(app);
|
registerMergeRoutes(app);
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
import type { i18n as I18nInstance } from 'i18next';
|
|
||||||
|
|
||||||
import { createI18nInstance, initI18nInstance } from '@/lib/translation';
|
|
||||||
|
|
||||||
let clientI18n: I18nInstance | undefined;
|
|
||||||
|
|
||||||
export const createI18nForClient = async (language?: string) => {
|
|
||||||
if (!clientI18n) {
|
|
||||||
clientI18n = createI18nInstance(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return initI18nInstance(clientI18n, language, false);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getClientI18nInstance = () => clientI18n;
|
|
||||||
@@ -1,75 +1,44 @@
|
|||||||
import i18next, { type i18n as I18nInstance } from 'i18next';
|
import i18next from "i18next";
|
||||||
import LanguageDetector from 'i18next-browser-languagedetector';
|
import LanguageDetector from "i18next-browser-languagedetector";
|
||||||
import I18NextHttpBackend from 'i18next-http-backend';
|
import I18NextHttpBackend, { HttpBackendOptions } from "i18next-http-backend";
|
||||||
|
const backendOptions: HttpBackendOptions = {
|
||||||
|
loadPath: 'http://localhost:5173/locales/{{lng}}/{{ns}}.json',
|
||||||
|
request: (_options, url, _payload, callback) => {
|
||||||
|
fetch(url)
|
||||||
|
.then((res) =>
|
||||||
|
res.json().then((r) => {
|
||||||
|
callback(null, {
|
||||||
|
data: JSON.stringify(r),
|
||||||
|
status: 200,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
callback(null, {
|
||||||
|
status: 500,
|
||||||
|
data: '',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export const createI18nInstance = (lng: string) => {
|
||||||
|
console.log('Initializing i18n with language:', lng);
|
||||||
|
const i18n = i18next.createInstance();
|
||||||
|
|
||||||
import { defaultLocale, localeCookieKey, supportedLocales, type SupportedLocale } from '@/i18n/constants';
|
i18n
|
||||||
|
.use(I18NextHttpBackend)
|
||||||
const runtimeNamespace = 'translation';
|
.use(LanguageDetector)
|
||||||
|
.init({
|
||||||
const normalizeLanguage = (language?: string): SupportedLocale => {
|
lng,
|
||||||
if (!language) return defaultLocale;
|
supportedLngs: ["en", "vi"],
|
||||||
const normalized = language.toLowerCase().split('-')[0] as SupportedLocale;
|
fallbackLng: "en",
|
||||||
return supportedLocales.includes(normalized) ? normalized : defaultLocale;
|
defaultNS: "translation",
|
||||||
};
|
ns: ['translation'],
|
||||||
|
interpolation: {
|
||||||
const getLoadPath = () => {
|
escapeValue: false,
|
||||||
const cdnBase = import.meta.env.VITE_I18N_CDN_BASE_URL?.trim().replace(/\/+$/, '');
|
},
|
||||||
if (cdnBase) {
|
backend: backendOptions,
|
||||||
return `${cdnBase}/locales/{{lng}}/{{lng}}.json`;
|
});
|
||||||
}
|
return i18n;
|
||||||
return '/locales/{{lng}}/{{lng}}.json';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createI18nInstance = (forServer: boolean) => {
|
|
||||||
const instance = i18next.createInstance();
|
|
||||||
|
|
||||||
instance.use(I18NextHttpBackend);
|
|
||||||
if (!forServer) {
|
|
||||||
instance.use(LanguageDetector);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const initI18nInstance = async (
|
|
||||||
instance: I18nInstance,
|
|
||||||
language?: string,
|
|
||||||
forServer: boolean = import.meta.env.SSR,
|
|
||||||
) => {
|
|
||||||
const lng = normalizeLanguage(language);
|
|
||||||
|
|
||||||
if (!instance.isInitialized) {
|
|
||||||
await instance.init({
|
|
||||||
supportedLngs: [...supportedLocales],
|
|
||||||
fallbackLng: defaultLocale,
|
|
||||||
load: 'languageOnly',
|
|
||||||
lng,
|
|
||||||
ns: [runtimeNamespace],
|
|
||||||
defaultNS: runtimeNamespace,
|
|
||||||
fallbackNS: runtimeNamespace,
|
|
||||||
interpolation: {
|
|
||||||
escapeValue: false,
|
|
||||||
},
|
|
||||||
backend: {
|
|
||||||
loadPath: getLoadPath(),
|
|
||||||
},
|
|
||||||
...(forServer
|
|
||||||
? {}
|
|
||||||
: {
|
|
||||||
detection: {
|
|
||||||
order: ['cookie', 'navigator', 'htmlTag'],
|
|
||||||
lookupCookie: localeCookieKey,
|
|
||||||
caches: ['cookie'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (instance.resolvedLanguage !== lng) {
|
|
||||||
await instance.changeLanguage(lng);
|
|
||||||
}
|
|
||||||
|
|
||||||
return instance;
|
|
||||||
};
|
};
|
||||||
|
export default createI18nInstance;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { ClassValue } from "clsx";
|
import type { ClassValue } from "clsx";
|
||||||
import { clsx } from "clsx";
|
import { clsx } from "clsx";
|
||||||
import { getActiveI18n } from '@/i18n';
|
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
@@ -52,10 +51,10 @@ export function getImageAspectRatio(url: string): Promise<AspectInfo> {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
const getRuntimeLocaleTag = () => {
|
// const getRuntimeLocaleTag = () => {
|
||||||
const locale = getActiveI18n()?.resolvedLanguage;
|
// const locale = getActiveI18n()?.resolvedLanguage;
|
||||||
return locale === 'vi' ? 'vi-VN' : 'en-US';
|
// return locale === 'vi' ? 'vi-VN' : 'en-US';
|
||||||
};
|
// };
|
||||||
|
|
||||||
export const formatBytes = (bytes?: number) => {
|
export const formatBytes = (bytes?: number) => {
|
||||||
if (!bytes) return '0 B';
|
if (!bytes) return '0 B';
|
||||||
@@ -63,7 +62,8 @@ export const formatBytes = (bytes?: number) => {
|
|||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
|
const value = parseFloat((bytes / Math.pow(k, i)).toFixed(2));
|
||||||
return `${new Intl.NumberFormat(getRuntimeLocaleTag()).format(value)} ${sizes[i]}`;
|
return `${value} ${sizes[i]}`;
|
||||||
|
// return `${new Intl.NumberFormat(getRuntimeLocaleTag()).format(value)} ${sizes[i]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const formatDuration = (seconds?: number) => {
|
export const formatDuration = (seconds?: number) => {
|
||||||
@@ -80,7 +80,7 @@ export const formatDuration = (seconds?: number) => {
|
|||||||
|
|
||||||
export const formatDate = (dateString: string = "", dateOnly: boolean = false) => {
|
export const formatDate = (dateString: string = "", dateOnly: boolean = false) => {
|
||||||
if (!dateString) return '';
|
if (!dateString) return '';
|
||||||
return new Date(dateString).toLocaleDateString(getRuntimeLocaleTag(), {
|
return new Date(dateString).toLocaleDateString("en-US", {
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
|
|||||||
19
src/main.ts
19
src/main.ts
@@ -5,13 +5,11 @@ import { createPinia } from 'pinia';
|
|||||||
import { createSSRApp } from 'vue';
|
import { createSSRApp } from 'vue';
|
||||||
import { RouterView } from 'vue-router';
|
import { RouterView } from 'vue-router';
|
||||||
|
|
||||||
import type { i18n as I18nInstance } from 'i18next';
|
|
||||||
import I18NextVue from 'i18next-vue';
|
import I18NextVue from 'i18next-vue';
|
||||||
|
|
||||||
import { createI18nInstance, initI18nInstance } from '@/lib/translation';
|
|
||||||
import { createI18nForClient } from '@/lib/translation/client';
|
|
||||||
|
|
||||||
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
|
||||||
|
import createI18nInstance from './lib/translation';
|
||||||
import createAppRouter from './routes';
|
import createAppRouter from './routes';
|
||||||
|
|
||||||
const bodyClass = ':uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen';
|
const bodyClass = ':uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen';
|
||||||
@@ -21,7 +19,7 @@ const getSerializedAppData = () => {
|
|||||||
return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record<string, any>;
|
return JSON.parse(document.getElementById('__APP_DATA__')?.innerText || '{}') as Record<string, any>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function createApp(lng: string = 'en', i18next?: I18nInstance) {
|
export async function createApp(lng: string = 'en') {
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
const app = createSSRApp(withErrorBoundary(RouterView));
|
const app = createSSRApp(withErrorBoundary(RouterView));
|
||||||
|
|
||||||
@@ -35,13 +33,7 @@ export async function createApp(lng: string = 'en', i18next?: I18nInstance) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
const runtimeI18n = import.meta.env.SSR
|
app.use(I18NextVue, { i18next: createI18nInstance(lng) });
|
||||||
? (i18next ?? createI18nInstance(true))
|
|
||||||
: await createI18nForClient(lng);
|
|
||||||
if (import.meta.env.SSR) {
|
|
||||||
await initI18nInstance(runtimeI18n, lng, true);
|
|
||||||
}
|
|
||||||
app.use(I18NextVue, { i18next: runtimeI18n });
|
|
||||||
app.use(PiniaColada, {
|
app.use(PiniaColada, {
|
||||||
pinia,
|
pinia,
|
||||||
plugins: [
|
plugins: [
|
||||||
@@ -57,8 +49,6 @@ export async function createApp(lng: string = 'en', i18next?: I18nInstance) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const queryCache = useQueryCache();
|
const queryCache = useQueryCache();
|
||||||
const router = createAppRouter();
|
|
||||||
app.use(router);
|
|
||||||
|
|
||||||
if (!import.meta.env.SSR) {
|
if (!import.meta.env.SSR) {
|
||||||
Object.entries(appData).forEach(([key, value]) => {
|
Object.entries(appData).forEach(([key, value]) => {
|
||||||
@@ -69,5 +59,8 @@ export async function createApp(lng: string = 'en', i18next?: I18nInstance) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const router = createAppRouter();
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
return { app, router, head, pinia, bodyClass, queryCache };
|
return { app, router, head, pinia, bodyClass, queryCache };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -172,12 +172,12 @@
|
|||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useTranslation } from 'i18next-vue';
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t, i18next } = useTranslation();
|
||||||
|
console.log('Current locale:', i18next);
|
||||||
const getFeatureList = (key: string): string[] => {
|
const getFeatureList = (key: string): string[] => {
|
||||||
const localized = t(key, { returnObjects: true });
|
const localized = t(key, { returnObjects: true });
|
||||||
return Array.isArray(localized) ? localized.map((item) => String(item)) : [];
|
return Array.isArray(localized) ? localized.map((item) => String(item)) : [];
|
||||||
|
|||||||
@@ -12,9 +12,8 @@ import UploadIcon from '@/components/icons/UploadIcon.vue';
|
|||||||
import { useAppToast } from '@/composables/useAppToast';
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useQuery } from '@pinia/colada';
|
import { useQuery } from '@pinia/colada';
|
||||||
import { computed, ref } from 'vue';
|
|
||||||
import { useTranslation } from 'i18next-vue';
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { getActiveI18n } from '@/i18n';
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const toast = useAppToast();
|
const toast = useAppToast();
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAuthStore } from '@/stores/auth';
|
|
||||||
import AppButton from '@/components/app/AppButton.vue';
|
import AppButton from '@/components/app/AppButton.vue';
|
||||||
import AppDialog from '@/components/app/AppDialog.vue';
|
import AppDialog from '@/components/app/AppDialog.vue';
|
||||||
import AppInput from '@/components/app/AppInput.vue';
|
import AppInput from '@/components/app/AppInput.vue';
|
||||||
@@ -8,19 +7,18 @@ import CheckIcon from '@/components/icons/CheckIcon.vue';
|
|||||||
import LockIcon from '@/components/icons/LockIcon.vue';
|
import LockIcon from '@/components/icons/LockIcon.vue';
|
||||||
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
import TelegramIcon from '@/components/icons/TelegramIcon.vue';
|
||||||
import XCircleIcon from '@/components/icons/XCircleIcon.vue';
|
import XCircleIcon from '@/components/icons/XCircleIcon.vue';
|
||||||
import { supportedLocales, type SupportedLocale } from '@/i18n/constants';
|
|
||||||
import { normalizeLocale } from '@/i18n';
|
|
||||||
import { useAppConfirm } from '@/composables/useAppConfirm';
|
import { useAppConfirm } from '@/composables/useAppConfirm';
|
||||||
import { useAppToast } from '@/composables/useAppToast';
|
import { useAppToast } from '@/composables/useAppToast';
|
||||||
import { computed, ref, watch } from 'vue';
|
import { supportedLocales } from '@/i18n/constants';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import { useTranslation } from 'i18next-vue';
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { computed, ref, watch } from 'vue';
|
||||||
|
|
||||||
const auth = useAuthStore();
|
const auth = useAuthStore();
|
||||||
const toast = useAppToast();
|
const toast = useAppToast();
|
||||||
const confirm = useAppConfirm();
|
const confirm = useAppConfirm();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const selectedLanguage = ref<SupportedLocale>(normalizeLocale((auth.user as any)?.language ?? (auth.user as any)?.locale));
|
|
||||||
const languageSaving = ref(false);
|
const languageSaving = ref(false);
|
||||||
|
|
||||||
const languageOptions = computed(() => supportedLocales.map((value) => ({
|
const languageOptions = computed(() => supportedLocales.map((value) => ({
|
||||||
@@ -29,7 +27,7 @@ const languageOptions = computed(() => supportedLocales.map((value) => ({
|
|||||||
})));
|
})));
|
||||||
|
|
||||||
watch(() => auth.user, (nextUser) => {
|
watch(() => auth.user, (nextUser) => {
|
||||||
selectedLanguage.value = normalizeLocale((nextUser as any)?.language ?? (nextUser as any)?.locale);
|
// selectedLanguage.value = normalizeLocale((nextUser as any)?.language ?? (nextUser as any)?.locale);
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
// 2FA state
|
// 2FA state
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { ModelVideo } from '@/api/client';
|
import type { ModelVideo } from '@/api/client';
|
||||||
import { formatBytes, getStatusSeverity } from '@/lib/utils';
|
import { formatBytes, getStatusSeverity } from '@/lib/utils';
|
||||||
import { computed } from 'vue';
|
|
||||||
import { useTranslation } from 'i18next-vue';
|
import { useTranslation } from 'i18next-vue';
|
||||||
import { getActiveI18n } from '@/i18n';
|
import { computed } from 'vue';
|
||||||
|
// import { getActiveI18n } from '@/i18n';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
video: ModelVideo;
|
video: ModelVideo;
|
||||||
|
|||||||
@@ -3,34 +3,18 @@ import { renderSSRHead } from '@unhead/vue/server';
|
|||||||
import { streamText } from 'hono/streaming';
|
import { streamText } from 'hono/streaming';
|
||||||
import { renderToWebStream } from 'vue/server-renderer';
|
import { renderToWebStream } from 'vue/server-renderer';
|
||||||
|
|
||||||
import { localeCookieKey } from '@/i18n';
|
|
||||||
import { createI18nInstance, initI18nInstance } from '@/lib/translation';
|
|
||||||
import { buildBootstrapScript } from '@/lib/manifest';
|
import { buildBootstrapScript } from '@/lib/manifest';
|
||||||
import { createApp } from '@/main';
|
import { createApp } from '@/main';
|
||||||
import { htmlEscape } from '@/server/utils/htmlEscape';
|
import { htmlEscape } from '@/server/utils/htmlEscape';
|
||||||
import { useAuthStore } from '@/stores/auth';
|
import { useAuthStore } from '@/stores/auth';
|
||||||
import type { Hono } from 'hono';
|
import type { Hono } from 'hono';
|
||||||
|
|
||||||
const parseCookie = (cookieHeader: string | undefined, key: string): string | undefined => {
|
|
||||||
if (!cookieHeader) return undefined;
|
|
||||||
const segments = cookieHeader.split(';');
|
|
||||||
for (const segment of segments) {
|
|
||||||
const [rawKey, ...rest] = segment.trim().split('=');
|
|
||||||
if (rawKey !== key) continue;
|
|
||||||
return decodeURIComponent(rest.join('='));
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function registerSSRRoutes(app: Hono) {
|
export function registerSSRRoutes(app: Hono) {
|
||||||
app.get("*", async (c) => {
|
app.get("*", async (c) => {
|
||||||
const nonce = crypto.randomUUID();
|
const nonce = crypto.randomUUID();
|
||||||
const url = new URL(c.req.url);
|
const url = new URL(c.req.url);
|
||||||
const lang = c.get("language")
|
const lang = c.get("language");
|
||||||
const localeFromCookie = parseCookie(c.req.header('cookie'), localeCookieKey);
|
const { app: vueApp, router, head, pinia, bodyClass, queryCache } = await createApp(lang);
|
||||||
const i18next = createI18nInstance(true);
|
|
||||||
await initI18nInstance(i18next, localeFromCookie ?? lang, true);
|
|
||||||
const { app: vueApp, router, head, pinia, bodyClass, queryCache } = await createApp(localeFromCookie ?? lang, i18next);
|
|
||||||
|
|
||||||
vueApp.provide("honoContext", c);
|
vueApp.provide("honoContext", c);
|
||||||
|
|
||||||
@@ -49,7 +33,7 @@ export function registerSSRRoutes(app: Hono) {
|
|||||||
const appStream = renderToWebStream(vueApp, ctx);
|
const appStream = renderToWebStream(vueApp, ctx);
|
||||||
|
|
||||||
// HTML Head
|
// HTML Head
|
||||||
await stream.write(`<!DOCTYPE html><html lang='${i18next.resolvedLanguage ?? lang}'><head>`);
|
await stream.write(`<!DOCTYPE html><html lang='${lang}'><head>`);
|
||||||
await stream.write("<base href='" + url.origin + "'/>");
|
await stream.write("<base href='" + url.origin + "'/>");
|
||||||
|
|
||||||
// SSR Head tags
|
// SSR Head tags
|
||||||
@@ -79,7 +63,7 @@ export function registerSSRRoutes(app: Hono) {
|
|||||||
Object.assign(ctx, {
|
Object.assign(ctx, {
|
||||||
$p: pinia.state.value,
|
$p: pinia.state.value,
|
||||||
$colada: serializeQueryCache(queryCache),
|
$colada: serializeQueryCache(queryCache),
|
||||||
$locale: i18next.resolvedLanguage ?? lang,
|
$locale: lang,
|
||||||
});
|
});
|
||||||
|
|
||||||
// App data script
|
// App data script
|
||||||
|
|||||||
@@ -1,36 +1,16 @@
|
|||||||
import { defineStore } from 'pinia';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
import { ref, watch } from 'vue';
|
|
||||||
import { client, ResponseResponse, type ModelUser } from '@/api/client';
|
import { client, ResponseResponse, type ModelUser } from '@/api/client';
|
||||||
import { defaultLocale, localeCookieKey, type SupportedLocale } from '@/i18n/constants';
|
|
||||||
import { getActiveI18n, normalizeLocale } from '@/i18n';
|
|
||||||
import { TinyMqttClient } from '@/lib/liteMqtt';
|
import { TinyMqttClient } from '@/lib/liteMqtt';
|
||||||
|
import { useTranslation } from 'i18next-vue';
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
import { ref, watch } from 'vue';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
type ProfileUpdatePayload = { username?: string; email?: string; language?: string; locale?: string };
|
type ProfileUpdatePayload = { username?: string; email?: string; language?: string; locale?: string };
|
||||||
|
|
||||||
const cookieMaxAge = 60 * 60 * 24 * 365;
|
|
||||||
|
|
||||||
const writeLocaleCookie = (locale: SupportedLocale) => {
|
|
||||||
if (typeof document === 'undefined') return;
|
|
||||||
document.cookie = `${localeCookieKey}=${encodeURIComponent(locale)}; path=/; max-age=${cookieMaxAge}; samesite=lax`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const resolveUserLocale = (target: Partial<ModelUser> | null | undefined): SupportedLocale => {
|
|
||||||
const userLocale = (target as any)?.language ?? (target as any)?.locale;
|
|
||||||
return normalizeLocale(typeof userLocale === 'string' ? userLocale : defaultLocale);
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyRuntimeLocale = (locale: SupportedLocale) => {
|
|
||||||
const i18n = getActiveI18n();
|
|
||||||
if (!i18n) return;
|
|
||||||
i18n.changeLanguage(locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useAuthStore = defineStore('auth', () => {
|
export const useAuthStore = defineStore('auth', () => {
|
||||||
const user = ref<ModelUser | null>(null);
|
const user = ref<ModelUser | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = (key: string, params?: Record<string, unknown>) =>
|
const { t } = useTranslation();
|
||||||
getActiveI18n()?.t(key, params) ?? key;
|
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const initialized = ref(false);
|
const initialized = ref(false);
|
||||||
@@ -55,11 +35,13 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
watch(user, (newUser) => {
|
watch(user, (newUser) => {
|
||||||
if (import.meta.env.SSR) return;
|
if (import.meta.env.SSR || !initialized.value || !newUser) return;
|
||||||
const locale = resolveUserLocale(newUser);
|
// const locale = getUserPreferredLocale(newUser);
|
||||||
applyRuntimeLocale(locale);
|
// const activeLocale = getActiveI18n()?.resolvedLanguage;
|
||||||
writeLocaleCookie(locale);
|
// if (!locale || (activeLocale && normalizeLocale(activeLocale) === locale)) return;
|
||||||
}, { deep: true, immediate: true });
|
// applyRuntimeLocale(locale);
|
||||||
|
// writeLocaleCookie(locale);
|
||||||
|
}, { deep: true });
|
||||||
// Initial check for session could go here if there was a /me endpoint or token check
|
// Initial check for session could go here if there was a /me endpoint or token check
|
||||||
async function init() {
|
async function init() {
|
||||||
if (initialized.value) return;
|
if (initialized.value) return;
|
||||||
@@ -70,8 +52,12 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}).then(r => r.json()).then(r => {
|
}).then(r => r.json()).then(r => {
|
||||||
if (r.data) {
|
if (r.data) {
|
||||||
user.value = r.data.user as ModelUser;
|
user.value = r.data.user as ModelUser;
|
||||||
const resolvedLocale = resolveUserLocale(user.value);
|
// const profileLocale = getUserPreferredLocale(user.value);
|
||||||
applyRuntimeLocale(resolvedLocale);
|
// const activeLocale = getActiveI18n()?.resolvedLanguage;
|
||||||
|
// if (profileLocale && (!activeLocale || normalizeLocale(activeLocale) !== profileLocale)) {
|
||||||
|
// applyRuntimeLocale(profileLocale);
|
||||||
|
// writeLocaleCookie(profileLocale);
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}).catch(() => { }).finally(() => {
|
}).catch(() => { }).finally(() => {
|
||||||
initialized.value = true;
|
initialized.value = true;
|
||||||
@@ -108,9 +94,11 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
console.log("body", body);
|
console.log("body", body);
|
||||||
if (body && body.data) {
|
if (body && body.data) {
|
||||||
user.value = body.data.user;
|
user.value = body.data.user;
|
||||||
const resolvedLocale = resolveUserLocale(user.value);
|
// const profileLocale = getUserPreferredLocale(user.value);
|
||||||
applyRuntimeLocale(resolvedLocale);
|
// if (profileLocale) {
|
||||||
writeLocaleCookie(resolvedLocale);
|
// applyRuntimeLocale(profileLocale);
|
||||||
|
// writeLocaleCookie(profileLocale);
|
||||||
|
// }
|
||||||
router.push('/');
|
router.push('/');
|
||||||
} else {
|
} else {
|
||||||
throw new Error(t('auth.errors.loginNoUserData'));
|
throw new Error(t('auth.errors.loginNoUserData'));
|
||||||
@@ -188,18 +176,18 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setLanguage(locale: string) {
|
async function setLanguage(locale: string) {
|
||||||
const normalizedLocale = normalizeLocale(locale);
|
// const normalizedLocale = normalizeLocale(locale);
|
||||||
const previousLocale = resolveUserLocale(user.value);
|
// const previousLocale = resolveUserLocale(user.value);
|
||||||
|
|
||||||
|
// applyRuntimeLocale(normalizedLocale);
|
||||||
|
// writeLocaleCookie(normalizedLocale);
|
||||||
const previousUser = user.value ? { ...user.value } : null;
|
const previousUser = user.value ? { ...user.value } : null;
|
||||||
|
|
||||||
applyRuntimeLocale(normalizedLocale);
|
|
||||||
writeLocaleCookie(normalizedLocale);
|
|
||||||
|
|
||||||
if (user.value) {
|
if (user.value) {
|
||||||
user.value = {
|
user.value = {
|
||||||
...user.value,
|
...user.value,
|
||||||
language: normalizedLocale,
|
// language: normalizedLocale,
|
||||||
locale: normalizedLocale,
|
// locale: normalizedLocale,
|
||||||
} as ModelUser;
|
} as ModelUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,14 +196,14 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await updateProfile({ language: normalizedLocale, locale: normalizedLocale });
|
// await updateProfile({ language: normalizedLocale, locale: normalizedLocale });
|
||||||
return { ok: true as const, fallbackOnly: false as const };
|
return { ok: true as const, fallbackOnly: false as const };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
applyRuntimeLocale(previousLocale);
|
// applyRuntimeLocale(previousLocale);
|
||||||
if (previousUser) {
|
if (previousUser) {
|
||||||
user.value = previousUser as ModelUser;
|
user.value = previousUser as ModelUser;
|
||||||
}
|
}
|
||||||
writeLocaleCookie(normalizedLocale);
|
// writeLocaleCookie(normalizedLocale);
|
||||||
return { ok: false as const, fallbackOnly: true as const, error: e };
|
return { ok: false as const, fallbackOnly: true as const, error: e };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -257,7 +245,8 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
setLanguage,
|
setLanguage,
|
||||||
logout: async () => {
|
logout: async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
const localeBeforeLogout = resolveUserLocale(user.value);
|
// const activeLocale = getActiveI18n()?.resolvedLanguage;
|
||||||
|
// const localeBeforeLogout = typeof activeLocale === 'string' ? normalizeLocale(activeLocale) : undefined;
|
||||||
try {
|
try {
|
||||||
await client.auth.logoutCreate();
|
await client.auth.logoutCreate();
|
||||||
user.value = null;
|
user.value = null;
|
||||||
@@ -267,8 +256,10 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
user.value = null;
|
user.value = null;
|
||||||
router.push('/login');
|
router.push('/login');
|
||||||
} finally {
|
} finally {
|
||||||
writeLocaleCookie(localeBeforeLogout);
|
// if (localeBeforeLogout) {
|
||||||
applyRuntimeLocale(localeBeforeLogout);
|
// writeLocaleCookie(localeBeforeLogout);
|
||||||
|
// applyRuntimeLocale(localeBeforeLogout);
|
||||||
|
// }
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user