replace vue-i18n with i18next-vue

Complete the i18n migration by switching runtime setup and remaining components to i18next-vue, and add shared locale constants/helpers for SSR and client language handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 02:11:46 +00:00
parent bbe15d5f3e
commit 6d04f1cbdc
67 changed files with 257 additions and 168 deletions

View File

@@ -1,7 +1,7 @@
# AGENTS.md # AGENTS.md
This file provides guidance for AI coding agents working with the Holistream codebase. This file provides guidance for AI coding agents working with the Holistream codebase.
hallo
## Project Overview ## Project Overview
**Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities for content creators. **Holistream** is a Vue 3 streaming application with Server-Side Rendering (SSR) deployed on Cloudflare Workers. It provides video upload, management, and streaming capabilities for content creators.

View File

@@ -6,14 +6,14 @@ import SettingsIcon from "@/components/icons/SettingsIcon.vue";
// import Upload from "@/components/icons/Upload.vue"; // import Upload from "@/components/icons/Upload.vue";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { computed, createStaticVNode, ref } from "vue"; import { computed, createStaticVNode, ref } from "vue";
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import NotificationDrawer from "./NotificationDrawer.vue"; import NotificationDrawer from "./NotificationDrawer.vue";
const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0"; const className = ":uno: w-12 h-12 p-2 rounded-2xl hover:bg-primary/15 flex press-animated items-center justify-center shrink-0";
const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1); const homeHoist = createStaticVNode(`<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />`, 1);
const notificationPopover = ref<InstanceType<typeof NotificationDrawer>>(); const notificationPopover = ref<InstanceType<typeof NotificationDrawer>>();
const isNotificationOpen = ref(false); const isNotificationOpen = ref(false);
const { t } = useI18n(); const { t } = useTranslation();
const handleNotificationClick = (event: Event) => { const handleNotificationClick = (event: Event) => {
notificationPopover.value?.toggle(event); notificationPopover.value?.toggle(event);

View File

@@ -3,13 +3,13 @@ import { useUploadQueue } from '@/composables/useUploadQueue';
import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue'; import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue';
import { useUIState } from '@/stores/uiState'; import { useUIState } from '@/stores/uiState';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
const router = useRouter(); const router = useRouter();
const { items, completeCount, pendingCount, startQueue, removeItem, cancelItem, removeAll } = useUploadQueue(); const { items, completeCount, pendingCount, startQueue, removeItem, cancelItem, removeAll } = useUploadQueue();
const uiState = useUIState(); const uiState = useUIState();
const { t } = useI18n(); const { t } = useTranslation();
const isCollapsed = ref(false); const isCollapsed = ref(false);

View File

@@ -2,7 +2,7 @@
import NotificationItem from '@/routes/notification/components/NotificationItem.vue'; import NotificationItem from '@/routes/notification/components/NotificationItem.vue';
import { onClickOutside } from '@vueuse/core'; import { onClickOutside } from '@vueuse/core';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
// Ensure client-side only rendering to avoid hydration mismatch // Ensure client-side only rendering to avoid hydration mismatch
const isMounted = ref(false); const isMounted = ref(false);
@@ -28,7 +28,7 @@ interface Notification {
const visible = ref(false); const visible = ref(false);
const drawerRef = ref(null); const drawerRef = ref(null);
const { t } = useI18n(); const { t } = useTranslation();
// Mock notifications data // Mock notifications data
const notifications = computed<Notification[]>(() => [ const notifications = computed<Notification[]>(() => [

View File

@@ -2,7 +2,7 @@
import XIcon from '@/components/icons/XIcon.vue'; import XIcon from '@/components/icons/XIcon.vue';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
// Ensure client-side only rendering to avoid hydration mismatch // Ensure client-side only rendering to avoid hydration mismatch
const isMounted = ref(false); const isMounted = ref(false);
@@ -26,7 +26,7 @@ const emit = defineEmits<{
(e: 'close'): void; (e: 'close'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const close = () => { const close = () => {
emit('update:visible', false); emit('update:visible', false);

View File

@@ -6,11 +6,11 @@ import XCircleIcon from '@/components/icons/XCircleIcon.vue';
import XIcon from '@/components/icons/XIcon.vue'; import XIcon from '@/components/icons/XIcon.vue';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { onBeforeUnmount, watchEffect } from 'vue'; import { onBeforeUnmount, watchEffect } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useAppToast, type AppToastSeverity } from '@/composables/useAppToast'; import { useAppToast, type AppToastSeverity } from '@/composables/useAppToast';
const { toasts, remove } = useAppToast(); const { toasts, remove } = useAppToast();
const { t } = useI18n(); const { t } = useTranslation();
const timers = new Map<string, ReturnType<typeof setTimeout>>(); const timers = new Map<string, ReturnType<typeof setTimeout>>();

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { VNode } from 'vue'; import { VNode } from 'vue';
interface Trend { interface Trend {
@@ -19,7 +19,7 @@ withDefaults(defineProps<Props>(), {
color: 'primary' color: 'primary'
}); });
const { t } = useI18n(); const { t } = useTranslation();
// const gradients = { // const gradients = {
// primary: 'from-primary/20 to-primary/5', // primary: 'from-primary/20 to-primary/5',

View File

@@ -32,9 +32,9 @@ const state = reactive<AppConfirmState>({
const requireConfirm = (options: AppConfirmOptions) => { const requireConfirm = (options: AppConfirmOptions) => {
const i18n = getActiveI18n(); const i18n = getActiveI18n();
const defaultHeader = i18n?.global.t('confirm.defaultHeader') ?? 'Confirm'; const defaultHeader = i18n?.t('confirm.defaultHeader') ?? 'Confirm';
const defaultAccept = i18n?.global.t('confirm.defaultAccept') ?? 'OK'; const defaultAccept = i18n?.t('confirm.defaultAccept') ?? 'OK';
const defaultReject = i18n?.global.t('confirm.defaultReject') ?? 'Cancel'; const defaultReject = i18n?.t('confirm.defaultReject') ?? 'Cancel';
state.visible = true; state.visible = true;
state.loading = false; state.loading = false;

View File

@@ -42,7 +42,7 @@ const abortItem = (id: string) => {
export function useUploadQueue() { export function useUploadQueue() {
const t = (key: string, params?: Record<string, unknown>) => const t = (key: string, params?: Record<string, unknown>) =>
getActiveI18n()?.global.t(key, params) ?? key; getActiveI18n()?.t(key, params) ?? key;
const remainingSlots = computed(() => Math.max(0, MAX_ITEMS - items.value.length)); const remainingSlots = computed(() => Math.max(0, MAX_ITEMS - items.value.length));
@@ -331,7 +331,7 @@ export function useUploadQueue() {
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(getActiveI18n()?.global.locale.value === 'vi' ? 'vi-VN' : 'en-US').format(value)} ${sizes[i]}`; return `${new Intl.NumberFormat(getActiveI18n()?.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US').format(value)} ${sizes[i]}`;
}; };
const totalSize = computed(() => { const totalSize = computed(() => {

7
src/i18n/constants.ts Normal file
View File

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

19
src/i18n/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { i18n as I18nInstance } from 'i18next';
import { getActiveI18nInstance } from '@/lib/translation';
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 getActiveI18nInstance();
};

View File

@@ -1,37 +1,98 @@
import i18next from "i18next"; import i18next, { type i18n as I18nInstance } from 'i18next';
import I18NextHttpBackend from "i18next-http-backend"; import LanguageDetector from 'i18next-browser-languagedetector';
import LanguageDetector from "i18next-browser-languagedetector"; import I18NextHttpBackend from 'i18next-http-backend';
const i18n = i18next.createInstance(); import { tryGetContext } from 'hono/context-storage';
i18n import { defaultLocale, localeCookieKey, supportedLocales, type SupportedLocale } from '@/i18n/constants';
.use(I18NextHttpBackend)
.use(LanguageDetector) const runtimeNamespace = 'translation';
.init({
supportedLngs: ["en", "vi"], let clientI18n: I18nInstance | undefined;
fallbackLng: "en",
defaultNS: "common", const normalizeLanguage = (language?: string): SupportedLocale => {
ns: [ if (!language) return defaultLocale;
"common", const normalized = language.toLowerCase().split('-')[0] as SupportedLocale;
"app", return supportedLocales.includes(normalized) ? normalized : defaultLocale;
"auth", };
"nav",
"settings", const getLoadPath = () => {
"pageHeader", const cdnBase = import.meta.env.VITE_I18N_CDN_BASE_URL?.trim().replace(/\/+$/, '');
"confirm", if (cdnBase) {
"toast", return `${cdnBase}/locales/{{lng}}/{{lng}}.json`;
"overview", }
"video", return '/locales/{{lng}}/{{lng}}.json';
"notification", };
"upload",
"home", const createInstance = () => {
"legal", const instance = i18next.createInstance();
"notFound",
], instance.use(I18NextHttpBackend);
if (!import.meta.env.SSR) {
instance.use(LanguageDetector);
}
return instance;
};
const initInstance = async (instance: I18nInstance, language?: string) => {
const lng = normalizeLanguage(language);
if (!instance.isInitialized) {
await instance.init({
supportedLngs: [...supportedLocales],
fallbackLng: defaultLocale,
load: 'languageOnly',
lng,
ns: [runtimeNamespace],
defaultNS: runtimeNamespace,
fallbackNS: runtimeNamespace,
interpolation: { interpolation: {
escapeValue: false, escapeValue: false,
}, },
backend: { backend: {
loadPath: "/locales/{{lng}}/{{ns}}.json", // dynamic fetch JSON loadPath: getLoadPath(),
}, },
...(import.meta.env.SSR
? {}
: {
detection: {
order: ['cookie', 'navigator', 'htmlTag'],
lookupCookie: localeCookieKey,
caches: ['cookie'],
},
}),
}); });
export default i18n;
return instance;
}
if (instance.resolvedLanguage !== lng) {
await instance.changeLanguage(lng);
}
return instance;
};
export const createI18nForRuntime = async (language?: string) => {
if (import.meta.env.SSR) {
const serverI18n = await initInstance(createInstance(), language);
const context = tryGetContext<any>();
context?.set?.('i18n', serverI18n);
return serverI18n;
}
if (!clientI18n) {
clientI18n = createInstance();
}
return initInstance(clientI18n, language);
};
export const getActiveI18nInstance = () => {
if (!import.meta.env.SSR) {
return clientI18n;
}
const context = tryGetContext<any>();
return context?.get?.('i18n') as I18nInstance | undefined;
};

View File

@@ -53,7 +53,7 @@ export function getImageAspectRatio(url: string): Promise<AspectInfo> {
const getRuntimeLocaleTag = () => { const getRuntimeLocaleTag = () => {
const locale = getActiveI18n()?.global.locale.value; const locale = getActiveI18n()?.resolvedLanguage;
return locale === 'vi' ? 'vi-VN' : 'en-US'; return locale === 'vi' ? 'vi-VN' : 'en-US';
}; };

View File

@@ -6,7 +6,7 @@ import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import I18NextVue from 'i18next-vue'; import I18NextVue from 'i18next-vue';
import i18next from '@/lib/translation'; import { createI18nForRuntime } from '@/lib/translation';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary'; import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import createAppRouter from './routes'; import createAppRouter from './routes';
@@ -32,7 +32,7 @@ export async function createApp(lng: string = 'en') {
} }
}); });
app.use(pinia); app.use(pinia);
await i18next.init({lng}); const i18next = await createI18nForRuntime(lng);
app.use(I18NextVue, { i18next }); app.use(I18NextVue, { i18next });
app.use(PiniaColada, { app.use(PiniaColada, {
pinia, pinia,

View File

@@ -9,7 +9,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { VueHead } from '@/components/VueHead'; import { VueHead } from '@/components/VueHead';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>

View File

@@ -31,11 +31,11 @@
import { client } from '@/api/client'; import { client } from '@/api/client';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { reactive } from 'vue'; import { reactive } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { z } from 'zod'; import { z } from 'zod';
const toast = useAppToast(); const toast = useAppToast();
const { t } = useI18n(); const { t } = useTranslation();
const form = reactive({ const form = reactive({
email: '' email: ''

View File

@@ -80,13 +80,13 @@
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { z } from 'zod'; import { z } from 'zod';
const toast = useAppToast(); const toast = useAppToast();
const auth = useAuthStore(); const auth = useAuthStore();
const showPassword = ref(false); const showPassword = ref(false);
const { t } = useI18n(); const { t } = useTranslation();
const form = reactive({ const form = reactive({
email: '', email: '',

View File

@@ -51,12 +51,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { z } from 'zod'; import { z } from 'zod';
const auth = useAuthStore(); const auth = useAuthStore();
const showPassword = ref(false); const showPassword = ref(false);
const { t } = useI18n(); const { t } = useTranslation();
const form = reactive({ const form = reactive({
name: '', name: '',

View File

@@ -173,13 +173,13 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useTranslation } from 'i18next-vue';
const { t, tm } = useI18n(); const { t } = useTranslation();
const getFeatureList = (key: string): string[] => { const getFeatureList = (key: string): string[] => {
const localized = tm(key); const localized = t(key, { returnObjects: true });
return Array.isArray(localized) ? localized.map((item) => String(item)) : []; return Array.isArray(localized) ? localized.map((item) => String(item)) : [];
}; };

View File

@@ -80,7 +80,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { Head } from '@unhead/vue/components' import { Head } from '@unhead/vue/components'
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>

View File

@@ -21,10 +21,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useHead } from '@unhead/vue'; import { useHead } from '@unhead/vue';
const { t } = useI18n(); const { t } = useTranslation();
const pageContent = computed(() => { const pageContent = computed(() => {
const title = t('legal.privacy.title'); const title = t('legal.privacy.title');

View File

@@ -21,10 +21,10 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useHead } from '@unhead/vue'; import { useHead } from '@unhead/vue';
const { t } = useI18n(); const { t } = useTranslation();
const pageContent = computed(() => { const pageContent = computed(() => {
const title = t('legal.terms.title'); const title = t('legal.terms.title');

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import PageHeader from '@/components/dashboard/PageHeader.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue';
import NotificationActions from './components/NotificationActions.vue'; import NotificationActions from './components/NotificationActions.vue';
import NotificationList from './components/NotificationList.vue'; import NotificationList from './components/NotificationList.vue';
@@ -21,7 +21,7 @@ interface Notification {
const loading = ref(false); const loading = ref(false);
const activeTab = ref('all'); const activeTab = ref('all');
const { t } = useI18n(); const { t } = useTranslation();
const notifications = ref<Notification[]>([ const notifications = ref<Notification[]>([
{ {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
interface Props { interface Props {
loading?: boolean; loading?: boolean;
@@ -13,7 +13,7 @@ const emit = defineEmits<{
clearAll: []; clearAll: [];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import InfoIcon from '@/components/icons/InfoIcon.vue'; import InfoIcon from '@/components/icons/InfoIcon.vue';
import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue'; import CheckCircleIcon from '@/components/icons/CheckCircleIcon.vue';
import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue'; import AlertTriangleIcon from '@/components/icons/AlertTriangleIcon.vue';
@@ -32,7 +32,7 @@ const emit = defineEmits<{
delete: [id: string]; delete: [id: string];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const iconComponent = computed(() => { const iconComponent = computed(() => {
const icons: Record<string, any> = { const icons: Record<string, any> = {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import NotificationItem from './NotificationItem.vue'; import NotificationItem from './NotificationItem.vue';
interface Notification { interface Notification {
@@ -24,7 +24,7 @@ const emit = defineEmits<{
delete: [id: string]; delete: [id: string];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -2,7 +2,7 @@
import { client, type ModelVideo } from '@/api/client'; import { client, type ModelVideo } from '@/api/client';
import PageHeader from '@/components/dashboard/PageHeader.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue';
import { onMounted, ref } from 'vue'; import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import NameGradient from './components/NameGradient.vue'; import NameGradient from './components/NameGradient.vue';
import QuickActions from './components/QuickActions.vue'; import QuickActions from './components/QuickActions.vue';
import RecentVideos from './components/RecentVideos.vue'; import RecentVideos from './components/RecentVideos.vue';
@@ -10,7 +10,7 @@ import StatsOverview from './components/StatsOverview.vue';
const loading = ref(true); const loading = ref(true);
const recentVideos = ref<ModelVideo[]>([]); const recentVideos = ref<ModelVideo[]>([]);
const { t } = useI18n(); const { t } = useTranslation();
const stats = ref({ const stats = ref({
totalVideos: 0, totalVideos: 0,

View File

@@ -5,8 +5,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const auth = useAuthStore(); const auth = useAuthStore();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import Chart from '@/components/icons/Chart.vue'; import Chart from '@/components/icons/Chart.vue';
import Credit from '@/components/icons/Credit.vue'; import Credit from '@/components/icons/Credit.vue';
import Upload from '@/components/icons/Upload.vue'; import Upload from '@/components/icons/Upload.vue';
@@ -17,7 +17,7 @@ defineProps<Props>();
const uiState = useUIState(); const uiState = useUIState();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useTranslation();
const quickActions = computed(() => [ const quickActions = computed(() => [
{ {

View File

@@ -2,7 +2,7 @@
import { ModelVideo } from '@/api/client'; import { ModelVideo } from '@/api/client';
import EmptyState from '@/components/dashboard/EmptyState.vue'; import EmptyState from '@/components/dashboard/EmptyState.vue';
import { formatDate, formatDuration } from '@/lib/utils'; import { formatDate, formatDuration } from '@/lib/utils';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
interface Props { interface Props {
@@ -13,7 +13,7 @@ interface Props {
defineProps<Props>(); defineProps<Props>();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useTranslation();
const getStatusClass = (status?: string) => { const getStatusClass = (status?: string) => {
switch (status?.toLowerCase()) { switch (status?.toLowerCase()) {

View File

@@ -28,11 +28,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const auth = useAuthStore(); const auth = useAuthStore();
const isCopied = ref(false); const isCopied = ref(false);
const { t } = useI18n(); const { t } = useTranslation();
const url = computed(() => `${location.origin}/ref/${auth.user?.username || ''}`); const url = computed(() => `${location.origin}/ref/${auth.user?.username || ''}`);

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import StatsCard from '@/components/dashboard/StatsCard.vue'; import StatsCard from '@/components/dashboard/StatsCard.vue';
import { formatBytes } from '@/lib/utils'; import { formatBytes } from '@/lib/utils';
@@ -15,7 +15,7 @@ interface Props {
} }
defineProps<Props>(); defineProps<Props>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { formatBytes } from '@/lib/utils'; import { formatBytes } from '@/lib/utils';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
interface Props { interface Props {
loading: boolean; loading: boolean;
@@ -13,7 +13,7 @@ interface Props {
} }
const props = defineProps<Props>(); const props = defineProps<Props>();
const { t } = useI18n(); const { t } = useTranslation();
const storagePercentage = computed(() => { const storagePercentage = computed(() => {
return Math.round((props.stats.storageUsed / props.stats.storageLimit) * 100); return Math.round((props.stats.storageUsed / props.stats.storageLimit) * 100);

View File

@@ -1,9 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const auth = useAuthStore(); const auth = useAuthStore();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -63,7 +63,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import PageHeader from '@/components/dashboard/PageHeader.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue';
import AppConfirmHost from '@/components/app/AppConfirmHost.vue'; import AppConfirmHost from '@/components/app/AppConfirmHost.vue';
@@ -80,7 +80,7 @@ import VideoPlayIcon from '@/components/icons/VideoPlayIcon.vue';
const route = useRoute(); const route = useRoute();
const auth = useAuthStore(); const auth = useAuthStore();
const { t } = useI18n(); const { t } = useTranslation();
// Map tab values to their paths // Map tab values to their paths
const tabPaths: Record<string, string> = { const tabPaths: Record<string, string> = {
profile: '/settings', profile: '/settings',

View File

@@ -6,7 +6,7 @@ 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 XIcon from '@/components/icons/XIcon.vue'; import XIcon from '@/components/icons/XIcon.vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
dialogVisible: boolean; dialogVisible: boolean;
@@ -31,7 +31,7 @@ const emit = defineEmits<{
(e: 'disconnect-telegram'): void; (e: 'disconnect-telegram'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const handleChangePassword = () => { const handleChangePassword = () => {
emit('change-password'); emit('change-password');

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import AppButton from '@/components/app/AppButton.vue'; import AppButton from '@/components/app/AppButton.vue';
import AppInput from '@/components/app/AppInput.vue'; import AppInput from '@/components/app/AppInput.vue';
import AppProgressBar from '@/components/app/AppProgressBar.vue'; import AppProgressBar from '@/components/app/AppProgressBar.vue';
@@ -12,7 +12,7 @@ import UserIcon from '@/components/icons/UserIcon.vue';
import XIcon from '@/components/icons/XIcon.vue'; import XIcon from '@/components/icons/XIcon.vue';
const auth = useAuthStore(); const auth = useAuthStore();
const { t } = useI18n(); const { t } = useTranslation();
const props = defineProps<{ const props = defineProps<{
editing: boolean; editing: boolean;

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
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';
@@ -33,7 +33,7 @@ const emit = defineEmits<{
const twoFactorDialogVisible = ref(false); const twoFactorDialogVisible = ref(false);
const twoFactorCode = ref(''); const twoFactorCode = ref('');
const twoFactorSecret = ref('JBSWY3DPEHPK3PXP'); const twoFactorSecret = ref('JBSWY3DPEHPK3PXP');
const { t } = useI18n(); const { t } = useTranslation();
const handleToggle2FA = async () => { const handleToggle2FA = async () => {
if (!props.twoFactorEnabled) { if (!props.twoFactorEnabled) {

View File

@@ -12,11 +12,11 @@ import TrashIcon from '@/components/icons/TrashIcon.vue';
import { useAppConfirm } from '@/composables/useAppConfirm'; import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
const { t } = useI18n(); const { t } = useTranslation();
interface VastTemplate { interface VastTemplate {
id: string; id: string;

View File

@@ -13,11 +13,12 @@ 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 { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { getActiveI18n } from '@/i18n';
const toast = useAppToast(); const toast = useAppToast();
const auth = useAuthStore(); const auth = useAuthStore();
const { t, locale } = useI18n(); const { t } = useTranslation();
const { data, isLoading } = useQuery({ const { data, isLoading } = useQuery({
key: () => ['payments-and-plans'], key: () => ['payments-and-plans'],
@@ -93,7 +94,7 @@ const getStatusLabel = (status: string) => {
return map[status] || status; return map[status] || status;
}; };
const currencyFormatter = computed(() => new Intl.NumberFormat(locale.value === 'vi' ? 'vi-VN' : 'en-US', { const currencyFormatter = computed(() => new Intl.NumberFormat(getActiveI18n()?.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US', {
style: 'currency', style: 'currency',
currency: 'USD', currency: 'USD',
maximumFractionDigits: 2, maximumFractionDigits: 2,
@@ -118,7 +119,7 @@ const subscribe = async (plan: ModelPlan) => {
paymentHistory.value.unshift({ paymentHistory.value.unshift({
id: `inv_${Date.now()}`, id: `inv_${Date.now()}`,
date: new Date().toLocaleDateString(locale.value === 'vi' ? 'vi-VN' : 'en-US', { month: 'short', day: 'numeric', year: 'numeric' }), date: new Date().toLocaleDateString(getActiveI18n()?.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US', { month: 'short', day: 'numeric', year: 'numeric' }),
amount: plan.price || 0, amount: plan.price || 0,
plan: plan.name || t('settings.billing.unknownPlan'), plan: plan.name || t('settings.billing.unknownPlan'),
status: 'success', status: 'success',

View File

@@ -6,11 +6,11 @@ import SlidersIcon from '@/components/icons/SlidersIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue'; import TrashIcon from '@/components/icons/TrashIcon.vue';
import { useAppConfirm } from '@/composables/useAppConfirm'; import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
const { t } = useI18n(); const { t } = useTranslation();
const handleDeleteAccount = () => { const handleDeleteAccount = () => {
confirm.require({ confirm.require({

View File

@@ -11,11 +11,11 @@ import TrashIcon from '@/components/icons/TrashIcon.vue';
import { useAppConfirm } from '@/composables/useAppConfirm'; import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
const { t } = useI18n(); const { t } = useTranslation();
const domains = ref([ const domains = ref([
{ id: '1', name: 'example.com', addedAt: '2024-01-15' }, { id: '1', name: 'example.com', addedAt: '2024-01-15' },

View File

@@ -8,10 +8,10 @@ import SendIcon from '@/components/icons/SendIcon.vue';
import TelegramIcon from '@/components/icons/TelegramIcon.vue'; import TelegramIcon from '@/components/icons/TelegramIcon.vue';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const toast = useAppToast(); const toast = useAppToast();
const { t } = useI18n(); const { t } = useTranslation();
const notificationSettings = ref({ const notificationSettings = ref({
email: true, email: true,

View File

@@ -1,13 +1,13 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import AppButton from '@/components/app/AppButton.vue'; import AppButton from '@/components/app/AppButton.vue';
import AppSwitch from '@/components/app/AppSwitch.vue'; import AppSwitch from '@/components/app/AppSwitch.vue';
import CheckIcon from '@/components/icons/CheckIcon.vue'; import CheckIcon from '@/components/icons/CheckIcon.vue';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
const toast = useAppToast(); const toast = useAppToast();
const { t } = useI18n(); const { t } = useTranslation();
const playerSettings = ref({ const playerSettings = ref({
autoplay: true, autoplay: true,

View File

@@ -13,12 +13,12 @@ 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 { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const auth = useAuthStore(); const auth = useAuthStore();
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
const { t } = useI18n(); const { t } = useTranslation();
const selectedLanguage = ref<SupportedLocale>(normalizeLocale((auth.user as any)?.language ?? (auth.user as any)?.locale)); const selectedLanguage = ref<SupportedLocale>(normalizeLocale((auth.user as any)?.language ?? (auth.user as any)?.locale));
const languageSaving = ref(false); const languageSaving = ref(false);

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useUploadQueue } from '@/composables/useUploadQueue'; import { useUploadQueue } from '@/composables/useUploadQueue';
import { useUIState } from '@/stores/uiState'; import { useUIState } from '@/stores/uiState';
import RemoteUrlForm from './components/RemoteUrlForm.vue'; import RemoteUrlForm from './components/RemoteUrlForm.vue';
@@ -8,7 +8,7 @@ import UploadDropzone from './components/UploadDropzone.vue';
const uiState = useUIState(); const uiState = useUIState();
const mode = ref<'local' | 'remote'>('local'); const mode = ref<'local' | 'remote'>('local');
const { t } = useI18n(); const { t } = useTranslation();
const { addFiles, addRemoteUrls, pendingCount, startQueue, remainingSlots, maxItems } = useUploadQueue(); const { addFiles, addRemoteUrls, pendingCount, startQueue, remainingSlots, maxItems } = useUploadQueue();

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
defineProps<{ defineProps<{
pendingCount?: number; pendingCount?: number;
@@ -9,7 +9,7 @@ defineProps<{
const category = ref(''); const category = ref('');
const visibility = ref('public'); const visibility = ref('public');
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ maxUrls?: number }>(); const props = defineProps<{ maxUrls?: number }>();
const urls = ref(''); const urls = ref('');
const emit = defineEmits<{ submit: [urls: string[]] }>(); const emit = defineEmits<{ submit: [urls: string[]] }>();
const { t } = useI18n(); const { t } = useTranslation();
const handleSubmit = () => { const handleSubmit = () => {
const limit = props.maxUrls ?? 5; const limit = props.maxUrls ?? 5;

View File

@@ -1,10 +1,10 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ maxFiles?: number }>(); const props = defineProps<{ maxFiles?: number }>();
const emit = defineEmits<{ filesSelected: [files: FileList] }>(); const emit = defineEmits<{ filesSelected: [files: FileList] }>();
const { t } = useI18n(); const { t } = useTranslation();
const isDragOver = ref(false); const isDragOver = ref(false);
let dragCounter = 0; let dragCounter = 0;

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
modelValue: 'local' | 'remote'; modelValue: 'local' | 'remote';
@@ -11,7 +11,7 @@ const emit = defineEmits<{
'update:modelValue': [value: 'local' | 'remote']; 'update:modelValue': [value: 'local' | 'remote'];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const modeList = computed<{ id: 'local' | 'remote'; label: string; icon: string }[]>(() => [ const modeList = computed<{ id: 'local' | 'remote'; label: string; icon: string }[]>(() => [
{ {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import UploadQueueItem from './UploadQueueItem.vue'; import UploadQueueItem from './UploadQueueItem.vue';
import type { QueueItem } from '@/composables/useUploadQueue'; import type { QueueItem } from '@/composables/useUploadQueue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
defineProps<{ defineProps<{
items?: QueueItem[]; items?: QueueItem[];
@@ -17,7 +17,7 @@ const emit = defineEmits<{
startQueue: []; startQueue: [];
}>() }>()
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -2,7 +2,7 @@
import FileUploadType from '@/components/icons/FileUploadType.vue'; import FileUploadType from '@/components/icons/FileUploadType.vue';
import type { QueueItem } from '@/composables/useUploadQueue'; import type { QueueItem } from '@/composables/useUploadQueue';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
item: QueueItem; item: QueueItem;
@@ -13,7 +13,7 @@ const emit = defineEmits<{
cancel: [id: string]; cancel: [id: string];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const statusLabel = computed(() => { const statusLabel = computed(() => {
switch (props.item.status) { switch (props.item.status) {

View File

@@ -3,7 +3,7 @@ import type { ModelVideo } from '@/api/client';
import { fetchMockVideoById } from '@/mocks/videos'; import { fetchMockVideoById } from '@/mocks/videos';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
videoId: string; videoId: string;
@@ -17,7 +17,7 @@ const toast = useAppToast();
const video = ref<ModelVideo | null>(null); const video = ref<ModelVideo | null>(null);
const loading = ref(true); const loading = ref(true);
const copiedField = ref<string | null>(null); const copiedField = ref<string | null>(null);
const { t } = useI18n(); const { t } = useTranslation();
const fetchVideo = async () => { const fetchVideo = async () => {
loading.value = true; loading.value = true;

View File

@@ -5,7 +5,7 @@ import { deleteMockVideo, fetchMockVideoById, updateMockVideo } from '@/mocks/vi
import { useAppConfirm } from '@/composables/useAppConfirm'; import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import VideoEditForm from './components/Detail/VideoEditForm.vue'; import VideoEditForm from './components/Detail/VideoEditForm.vue';
import VideoHeader from './components/Detail/VideoInfoHeader.vue'; import VideoHeader from './components/Detail/VideoInfoHeader.vue';
@@ -16,7 +16,7 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const toast = useAppToast(); const toast = useAppToast();
const confirm = useAppConfirm(); const confirm = useAppConfirm();
const { t } = useI18n(); const { t } = useTranslation();
const videoId = route.params.id as string; const videoId = route.params.id as string;
const video = ref<ModelVideo | null>(null); const video = ref<ModelVideo | null>(null);

View File

@@ -3,7 +3,7 @@ import type { ModelVideo } from '@/api/client';
import { fetchMockVideoById, updateMockVideo } from '@/mocks/videos'; import { fetchMockVideoById, updateMockVideo } from '@/mocks/videos';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
videoId: string; videoId: string;
@@ -17,7 +17,7 @@ const toast = useAppToast();
const video = ref<ModelVideo | null>(null); const video = ref<ModelVideo | null>(null);
const loading = ref(true); const loading = ref(true);
const saving = ref(false); const saving = ref(false);
const { t } = useI18n(); const { t } = useTranslation();
const form = ref({ const form = ref({
title: '', title: '',

View File

@@ -4,7 +4,7 @@ import EmptyState from '@/components/dashboard/EmptyState.vue';
import PageHeader from '@/components/dashboard/PageHeader.vue'; import PageHeader from '@/components/dashboard/PageHeader.vue';
import { fetchMockVideos } from '@/mocks/videos'; import { fetchMockVideos } from '@/mocks/videos';
import { createStaticVNode, computed, onMounted, onUnmounted, ref, watch } from 'vue'; import { createStaticVNode, computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useUploadQueue } from '@/composables/useUploadQueue'; import { useUploadQueue } from '@/composables/useUploadQueue';
@@ -23,7 +23,7 @@ const uiState = useUIState();
const { addFiles, startQueue } = useUploadQueue(); const { addFiles, startQueue } = useUploadQueue();
const toast = useAppToast(); const toast = useAppToast();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useTranslation();
const videos = ref<ModelVideo[]>([]); const videos = ref<ModelVideo[]>([]);
const loading = ref(true); const loading = ref(true);
const error = ref<string | null>(null); const error = ref<string | null>(null);

View File

@@ -45,7 +45,7 @@ import EllipsisVerticalIcon from '@/components/icons/EllipsisVerticalIcon.vue';
import type { ModelVideo } from '@/api/client'; import type { ModelVideo } from '@/api/client';
import { useAppToast } from '@/composables/useAppToast'; import { useAppToast } from '@/composables/useAppToast';
import { computed, nextTick, ref } from 'vue'; import { computed, nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import type { RouteLocationRaw } from 'vue-router'; import type { RouteLocationRaw } from 'vue-router';
const props = defineProps<{ const props = defineProps<{
@@ -61,7 +61,7 @@ const isOpen = ref(false);
const containerRef = ref<HTMLElement>(); const containerRef = ref<HTMLElement>();
const menuRef = ref<HTMLElement>(); const menuRef = ref<HTMLElement>();
const menuStyle = ref<Record<string, string>>({}); const menuStyle = ref<Record<string, string>>({});
const { t } = useI18n(); const { t } = useTranslation();
const videoUrl = computed(() => { const videoUrl = computed(() => {
return `${window.location.origin}/videos/${props.video.id}`; return `${window.location.origin}/videos/${props.video.id}`;

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
defineProps<{ defineProps<{
title: string; title: string;
@@ -14,7 +14,7 @@ const emit = defineEmits<{
toggleEdit: []; toggleEdit: [];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -2,7 +2,8 @@
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 { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import { getActiveI18n } from '@/i18n';
const props = defineProps<{ const props = defineProps<{
video: ModelVideo; video: ModelVideo;
@@ -14,7 +15,7 @@ const emit = defineEmits<{
delete: []; delete: [];
}>(); }>();
const { t, locale } = useI18n(); const { t } = useTranslation();
const formatFileSize = (bytes?: number): string => { const formatFileSize = (bytes?: number): string => {
if (!bytes) return '-'; if (!bytes) return '-';
@@ -35,7 +36,7 @@ const formatDuration = (seconds?: number): string => {
const formatDate = (dateStr?: string): string => { const formatDate = (dateStr?: string): string => {
if (!dateStr) return '-'; if (!dateStr) return '-';
const date = new Date(dateStr); const date = new Date(dateStr);
return date.toLocaleString(locale.value === 'vi' ? 'vi-VN' : 'en-US', { return date.toLocaleString(getActiveI18n()?.resolvedLanguage === 'vi' ? 'vi-VN' : 'en-US', {
month: 'long', month: 'long',
day: 'numeric', day: 'numeric',
year: 'numeric', year: 'numeric',

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelVideo } from '@/api/client'; import type { ModelVideo } from '@/api/client';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
video: ModelVideo; video: ModelVideo;
@@ -11,7 +11,7 @@ const emit = defineEmits<{
copy: [text: string, label: string]; copy: [text: string, label: string];
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const handleCopy = (text: string, label: string) => { const handleCopy = (text: string, label: string) => {
emit('copy', text, label); emit('copy', text, label);

View File

@@ -1,12 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelVideo } from '@/api/client'; import type { ModelVideo } from '@/api/client';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
defineProps<{ defineProps<{
video: ModelVideo; video: ModelVideo;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelVideo } from '@/api/client'; import type { ModelVideo } from '@/api/client';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
defineProps<{ defineProps<{
selectedVideos: ModelVideo[]; selectedVideos: ModelVideo[];
@@ -11,7 +11,7 @@ const emit = defineEmits<{
(e: 'clear'): void; (e: 'clear'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
</script> </script>
<template> <template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
searchQuery: string; searchQuery: string;
@@ -19,7 +19,7 @@ const emit = defineEmits<{
(e: 'search'): void; (e: 'search'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const pageCount = computed(() => Math.ceil(props.total / props.limit) || 1); const pageCount = computed(() => Math.ceil(props.total / props.limit) || 1);
const first = computed(() => Math.min((props.page - 1) * props.limit + 1, props.total)); const first = computed(() => Math.min((props.page - 1) * props.limit + 1, props.total));
const last = computed(() => Math.min(props.page * props.limit, props.total)); const last = computed(() => Math.min(props.page * props.limit, props.total));

View File

@@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ModelVideo } from '@/api/client'; import type { ModelVideo } from '@/api/client';
import { formatDate, formatDuration, getStatusSeverity } from '@/lib/utils'; import { formatDate, formatDuration, getStatusSeverity } from '@/lib/utils';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
import CardPopover from './CardPopover.vue'; import CardPopover from './CardPopover.vue';
const props = defineProps<{ const props = defineProps<{
@@ -15,7 +15,7 @@ const emit = defineEmits<{
(e: 'delete', videoId: string): void; (e: 'delete', videoId: string): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const severityClasses: Record<string, string> = { const severityClasses: Record<string, string> = {
success: 'bg-green-100 text-green-800', success: 'bg-green-100 text-green-800',

View File

@@ -5,7 +5,7 @@ import PencilIcon from '@/components/icons/PencilIcon.vue';
import TrashIcon from '@/components/icons/TrashIcon.vue'; import TrashIcon from '@/components/icons/TrashIcon.vue';
import VideoIcon from '@/components/icons/VideoIcon.vue'; import VideoIcon from '@/components/icons/VideoIcon.vue';
import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils'; import { formatBytes, formatDate, getStatusSeverity } from '@/lib/utils';
import { useI18n } from 'vue-i18n'; import { useTranslation } from 'i18next-vue';
const props = defineProps<{ const props = defineProps<{
videos: ModelVideo[]; videos: ModelVideo[];
@@ -20,7 +20,7 @@ const emit = defineEmits<{
(e: 'copy', videoId: string): void; (e: 'copy', videoId: string): void;
}>(); }>();
const { t } = useI18n(); const { t } = useTranslation();
const severityClasses: Record<string, string> = { const severityClasses: Record<string, string> = {
success: 'bg-green-100 text-green-800', success: 'bg-green-100 text-green-800',

View File

@@ -23,14 +23,14 @@ const resolveUserLocale = (target: Partial<ModelUser> | null | undefined): Suppo
const applyRuntimeLocale = (locale: SupportedLocale) => { const applyRuntimeLocale = (locale: SupportedLocale) => {
const i18n = getActiveI18n(); const i18n = getActiveI18n();
if (!i18n) return; if (!i18n) return;
i18n.global.locale.value = locale; 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 = (key: string, params?: Record<string, unknown>) =>
getActiveI18n()?.global.t(key, params) ?? key; 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);