diff --git a/AGENTS.md b/AGENTS.md
index 205d22f..fc9f4d9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
# AGENTS.md
This file provides guidance for AI coding agents working with the Holistream codebase.
-
+hallo
## 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.
diff --git a/src/components/DashboardNav.vue b/src/components/DashboardNav.vue
index f0b6ec1..b1450e3 100644
--- a/src/components/DashboardNav.vue
+++ b/src/components/DashboardNav.vue
@@ -6,14 +6,14 @@ import SettingsIcon from "@/components/icons/SettingsIcon.vue";
// import Upload from "@/components/icons/Upload.vue";
import { cn } from "@/lib/utils";
import { computed, createStaticVNode, ref } from "vue";
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-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 homeHoist = createStaticVNode(`
`, 1);
const notificationPopover = ref>();
const isNotificationOpen = ref(false);
-const { t } = useI18n();
+const { t } = useTranslation();
const handleNotificationClick = (event: Event) => {
notificationPopover.value?.toggle(event);
diff --git a/src/components/GlobalUploadIndicator.vue b/src/components/GlobalUploadIndicator.vue
index af2eb5f..2625d0c 100644
--- a/src/components/GlobalUploadIndicator.vue
+++ b/src/components/GlobalUploadIndicator.vue
@@ -3,13 +3,13 @@ import { useUploadQueue } from '@/composables/useUploadQueue';
import UploadQueueItem from '@/routes/upload/components/UploadQueueItem.vue';
import { useUIState } from '@/stores/uiState';
import { computed, ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import { useRouter } from 'vue-router';
const router = useRouter();
const { items, completeCount, pendingCount, startQueue, removeItem, cancelItem, removeAll } = useUploadQueue();
const uiState = useUIState();
-const { t } = useI18n();
+const { t } = useTranslation();
const isCollapsed = ref(false);
diff --git a/src/components/NotificationDrawer.vue b/src/components/NotificationDrawer.vue
index ea85566..d69ebe7 100644
--- a/src/components/NotificationDrawer.vue
+++ b/src/components/NotificationDrawer.vue
@@ -2,7 +2,7 @@
import NotificationItem from '@/routes/notification/components/NotificationItem.vue';
import { onClickOutside } from '@vueuse/core';
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
const isMounted = ref(false);
@@ -28,7 +28,7 @@ interface Notification {
const visible = ref(false);
const drawerRef = ref(null);
-const { t } = useI18n();
+const { t } = useTranslation();
// Mock notifications data
const notifications = computed(() => [
diff --git a/src/components/app/AppDialog.vue b/src/components/app/AppDialog.vue
index 7dfa68e..ab5b172 100644
--- a/src/components/app/AppDialog.vue
+++ b/src/components/app/AppDialog.vue
@@ -2,7 +2,7 @@
import XIcon from '@/components/icons/XIcon.vue';
import { cn } from '@/lib/utils';
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
const isMounted = ref(false);
@@ -26,7 +26,7 @@ const emit = defineEmits<{
(e: 'close'): void;
}>();
-const { t } = useI18n();
+const { t } = useTranslation();
const close = () => {
emit('update:visible', false);
diff --git a/src/components/app/AppToastHost.vue b/src/components/app/AppToastHost.vue
index 33f2254..5a7e627 100644
--- a/src/components/app/AppToastHost.vue
+++ b/src/components/app/AppToastHost.vue
@@ -6,11 +6,11 @@ import XCircleIcon from '@/components/icons/XCircleIcon.vue';
import XIcon from '@/components/icons/XIcon.vue';
import { cn } from '@/lib/utils';
import { onBeforeUnmount, watchEffect } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import { useAppToast, type AppToastSeverity } from '@/composables/useAppToast';
const { toasts, remove } = useAppToast();
-const { t } = useI18n();
+const { t } = useTranslation();
const timers = new Map>();
diff --git a/src/components/dashboard/StatsCard.vue b/src/components/dashboard/StatsCard.vue
index ea49880..eeb34af 100644
--- a/src/components/dashboard/StatsCard.vue
+++ b/src/components/dashboard/StatsCard.vue
@@ -1,5 +1,5 @@
diff --git a/src/routes/auth/forgot.vue b/src/routes/auth/forgot.vue
index f24d8fe..e9ce56a 100644
--- a/src/routes/auth/forgot.vue
+++ b/src/routes/auth/forgot.vue
@@ -31,11 +31,11 @@
import { client } from '@/api/client';
import { useAppToast } from '@/composables/useAppToast';
import { reactive } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import { z } from 'zod';
const toast = useAppToast();
-const { t } = useI18n();
+const { t } = useTranslation();
const form = reactive({
email: ''
diff --git a/src/routes/auth/login.vue b/src/routes/auth/login.vue
index d42f723..e62ba39 100644
--- a/src/routes/auth/login.vue
+++ b/src/routes/auth/login.vue
@@ -80,13 +80,13 @@
import { useAuthStore } from '@/stores/auth';
import { useAppToast } from '@/composables/useAppToast';
import { reactive, ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import { z } from 'zod';
const toast = useAppToast();
const auth = useAuthStore();
const showPassword = ref(false);
-const { t } = useI18n();
+const { t } = useTranslation();
const form = reactive({
email: '',
diff --git a/src/routes/auth/signup.vue b/src/routes/auth/signup.vue
index 2a18fc3..a792fb5 100644
--- a/src/routes/auth/signup.vue
+++ b/src/routes/auth/signup.vue
@@ -51,12 +51,12 @@
diff --git a/src/routes/home/Privacy.vue b/src/routes/home/Privacy.vue
index fcebfb0..19b15e2 100644
--- a/src/routes/home/Privacy.vue
+++ b/src/routes/home/Privacy.vue
@@ -21,10 +21,10 @@
diff --git a/src/routes/notification/components/NotificationItem.vue b/src/routes/notification/components/NotificationItem.vue
index b326d68..9f4d756 100644
--- a/src/routes/notification/components/NotificationItem.vue
+++ b/src/routes/notification/components/NotificationItem.vue
@@ -1,6 +1,6 @@
diff --git a/src/routes/overview/Overview.vue b/src/routes/overview/Overview.vue
index 024f1f6..e659a97 100644
--- a/src/routes/overview/Overview.vue
+++ b/src/routes/overview/Overview.vue
@@ -2,7 +2,7 @@
import { client, type ModelVideo } from '@/api/client';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import { onMounted, ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import NameGradient from './components/NameGradient.vue';
import QuickActions from './components/QuickActions.vue';
import RecentVideos from './components/RecentVideos.vue';
@@ -10,7 +10,7 @@ import StatsOverview from './components/StatsOverview.vue';
const loading = ref(true);
const recentVideos = ref([]);
-const { t } = useI18n();
+const { t } = useTranslation();
const stats = ref({
totalVideos: 0,
diff --git a/src/routes/overview/components/NameGradient.vue b/src/routes/overview/components/NameGradient.vue
index f524755..ccaa38e 100644
--- a/src/routes/overview/components/NameGradient.vue
+++ b/src/routes/overview/components/NameGradient.vue
@@ -5,8 +5,8 @@
diff --git a/src/routes/overview/components/QuickActions.vue b/src/routes/overview/components/QuickActions.vue
index bdeb99d..488eb07 100644
--- a/src/routes/overview/components/QuickActions.vue
+++ b/src/routes/overview/components/QuickActions.vue
@@ -1,6 +1,6 @@
diff --git a/src/routes/overview/components/StorageUsage.vue b/src/routes/overview/components/StorageUsage.vue
index f04f1d8..6766c6f 100644
--- a/src/routes/overview/components/StorageUsage.vue
+++ b/src/routes/overview/components/StorageUsage.vue
@@ -1,7 +1,7 @@
diff --git a/src/routes/settings/Settings.vue b/src/routes/settings/Settings.vue
index 9357402..ad386df 100644
--- a/src/routes/settings/Settings.vue
+++ b/src/routes/settings/Settings.vue
@@ -63,7 +63,7 @@
diff --git a/src/routes/upload/components/InfoTip.vue b/src/routes/upload/components/InfoTip.vue
index aa17f1c..ca32f04 100644
--- a/src/routes/upload/components/InfoTip.vue
+++ b/src/routes/upload/components/InfoTip.vue
@@ -1,7 +1,7 @@
diff --git a/src/routes/upload/components/RemoteUrlForm.vue b/src/routes/upload/components/RemoteUrlForm.vue
index a168355..e92ad86 100644
--- a/src/routes/upload/components/RemoteUrlForm.vue
+++ b/src/routes/upload/components/RemoteUrlForm.vue
@@ -1,12 +1,12 @@
diff --git a/src/routes/upload/components/UploadQueueItem.vue b/src/routes/upload/components/UploadQueueItem.vue
index 6208718..9a6383b 100644
--- a/src/routes/upload/components/UploadQueueItem.vue
+++ b/src/routes/upload/components/UploadQueueItem.vue
@@ -2,7 +2,7 @@
import FileUploadType from '@/components/icons/FileUploadType.vue';
import type { QueueItem } from '@/composables/useUploadQueue';
import { computed } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
const props = defineProps<{
item: QueueItem;
@@ -13,7 +13,7 @@ const emit = defineEmits<{
cancel: [id: string];
}>();
-const { t } = useI18n();
+const { t } = useTranslation();
const statusLabel = computed(() => {
switch (props.item.status) {
diff --git a/src/routes/video/CopyVideoModal.vue b/src/routes/video/CopyVideoModal.vue
index 50b451c..92af199 100644
--- a/src/routes/video/CopyVideoModal.vue
+++ b/src/routes/video/CopyVideoModal.vue
@@ -3,7 +3,7 @@ import type { ModelVideo } from '@/api/client';
import { fetchMockVideoById } from '@/mocks/videos';
import { useAppToast } from '@/composables/useAppToast';
import { computed, ref, watch } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
const props = defineProps<{
videoId: string;
@@ -17,7 +17,7 @@ const toast = useAppToast();
const video = ref(null);
const loading = ref(true);
const copiedField = ref(null);
-const { t } = useI18n();
+const { t } = useTranslation();
const fetchVideo = async () => {
loading.value = true;
diff --git a/src/routes/video/DetailVideo.vue b/src/routes/video/DetailVideo.vue
index 99929ac..879dd4d 100644
--- a/src/routes/video/DetailVideo.vue
+++ b/src/routes/video/DetailVideo.vue
@@ -5,7 +5,7 @@ import { deleteMockVideo, fetchMockVideoById, updateMockVideo } from '@/mocks/vi
import { useAppConfirm } from '@/composables/useAppConfirm';
import { useAppToast } from '@/composables/useAppToast';
import { computed, onMounted, ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import { useRoute, useRouter } from 'vue-router';
import VideoEditForm from './components/Detail/VideoEditForm.vue';
import VideoHeader from './components/Detail/VideoInfoHeader.vue';
@@ -16,7 +16,7 @@ const route = useRoute();
const router = useRouter();
const toast = useAppToast();
const confirm = useAppConfirm();
-const { t } = useI18n();
+const { t } = useTranslation();
const videoId = route.params.id as string;
const video = ref(null);
diff --git a/src/routes/video/DetailVideoModal.vue b/src/routes/video/DetailVideoModal.vue
index e4877e4..85814db 100644
--- a/src/routes/video/DetailVideoModal.vue
+++ b/src/routes/video/DetailVideoModal.vue
@@ -3,7 +3,7 @@ import type { ModelVideo } from '@/api/client';
import { fetchMockVideoById, updateMockVideo } from '@/mocks/videos';
import { useAppToast } from '@/composables/useAppToast';
import { ref, watch } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
const props = defineProps<{
videoId: string;
@@ -17,7 +17,7 @@ const toast = useAppToast();
const video = ref(null);
const loading = ref(true);
const saving = ref(false);
-const { t } = useI18n();
+const { t } = useTranslation();
const form = ref({
title: '',
diff --git a/src/routes/video/Videos.vue b/src/routes/video/Videos.vue
index 59815f3..534c8ce 100644
--- a/src/routes/video/Videos.vue
+++ b/src/routes/video/Videos.vue
@@ -4,7 +4,7 @@ import EmptyState from '@/components/dashboard/EmptyState.vue';
import PageHeader from '@/components/dashboard/PageHeader.vue';
import { fetchMockVideos } from '@/mocks/videos';
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 { useUploadQueue } from '@/composables/useUploadQueue';
@@ -23,7 +23,7 @@ const uiState = useUIState();
const { addFiles, startQueue } = useUploadQueue();
const toast = useAppToast();
const router = useRouter();
-const { t } = useI18n();
+const { t } = useTranslation();
const videos = ref([]);
const loading = ref(true);
const error = ref(null);
diff --git a/src/routes/video/components/CardPopover.vue b/src/routes/video/components/CardPopover.vue
index 12ab85c..13ca701 100644
--- a/src/routes/video/components/CardPopover.vue
+++ b/src/routes/video/components/CardPopover.vue
@@ -45,7 +45,7 @@ import EllipsisVerticalIcon from '@/components/icons/EllipsisVerticalIcon.vue';
import type { ModelVideo } from '@/api/client';
import { useAppToast } from '@/composables/useAppToast';
import { computed, nextTick, ref } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
import type { RouteLocationRaw } from 'vue-router';
const props = defineProps<{
@@ -61,7 +61,7 @@ const isOpen = ref(false);
const containerRef = ref();
const menuRef = ref();
const menuStyle = ref>({});
-const { t } = useI18n();
+const { t } = useTranslation();
const videoUrl = computed(() => {
return `${window.location.origin}/videos/${props.video.id}`;
diff --git a/src/routes/video/components/Detail/VideoEditForm.vue b/src/routes/video/components/Detail/VideoEditForm.vue
index 712fa68..8ef7043 100644
--- a/src/routes/video/components/Detail/VideoEditForm.vue
+++ b/src/routes/video/components/Detail/VideoEditForm.vue
@@ -1,5 +1,5 @@
diff --git a/src/routes/video/components/Detail/VideoInfoHeader.vue b/src/routes/video/components/Detail/VideoInfoHeader.vue
index e49490d..92a3b11 100644
--- a/src/routes/video/components/Detail/VideoInfoHeader.vue
+++ b/src/routes/video/components/Detail/VideoInfoHeader.vue
@@ -2,7 +2,8 @@
import type { ModelVideo } from '@/api/client';
import { formatBytes, getStatusSeverity } from '@/lib/utils';
import { computed } from 'vue';
-import { useI18n } from 'vue-i18n';
+import { useTranslation } from 'i18next-vue';
+import { getActiveI18n } from '@/i18n';
const props = defineProps<{
video: ModelVideo;
@@ -14,7 +15,7 @@ const emit = defineEmits<{
delete: [];
}>();
-const { t, locale } = useI18n();
+const { t } = useTranslation();
const formatFileSize = (bytes?: number): string => {
if (!bytes) return '-';
@@ -35,7 +36,7 @@ const formatDuration = (seconds?: number): string => {
const formatDate = (dateStr?: string): string => {
if (!dateStr) return '-';
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',
day: 'numeric',
year: 'numeric',
diff --git a/src/routes/video/components/Detail/VideoInfoPanel.vue b/src/routes/video/components/Detail/VideoInfoPanel.vue
index f7fa694..40d22db 100644
--- a/src/routes/video/components/Detail/VideoInfoPanel.vue
+++ b/src/routes/video/components/Detail/VideoInfoPanel.vue
@@ -1,7 +1,7 @@
diff --git a/src/routes/video/components/VideoBulkActions.vue b/src/routes/video/components/VideoBulkActions.vue
index 4c25e23..355eb3b 100644
--- a/src/routes/video/components/VideoBulkActions.vue
+++ b/src/routes/video/components/VideoBulkActions.vue
@@ -1,6 +1,6 @@
diff --git a/src/routes/video/components/VideoFilters.vue b/src/routes/video/components/VideoFilters.vue
index c874f57..e959af4 100644
--- a/src/routes/video/components/VideoFilters.vue
+++ b/src/routes/video/components/VideoFilters.vue
@@ -1,5 +1,5 @@