diff --git a/.gitignore b/.gitignore
index 313b02a4f497fe12e0b022756543c5fa7b6805f5..da57c694e1f183176f15e045e711faa77c7ff8bb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,3 +27,4 @@ pnpm-debug.log*
 # vitepress
 public
 .vitepress
+docs/.vitepress/cache
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index 95be264b6981d2ac8e78c38c4e35655e5a143801..5d833c9b3183c1be7e4b232004224b4052e9ba62 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -1,20 +1,9 @@
-import { createI18n } from 'vue-i18n'
-
 import DefaultTheme from 'vitepress/theme'
-import en from '~/locales/en.yaml'
 import Funkwhale from '~/main'
 
 export default {
   ...DefaultTheme,
   enhanceApp({ app }) {
-    const i18n = createI18n({
-      legacy: false,
-      locale: 'en',
-      fallbackLocale: 'en',
-      messages: { en }
-    })
-
     app.use(Funkwhale)
-    app.use(i18n)
   }
 }
diff --git a/src/composables/useI18n.ts b/src/composables/useI18n.ts
new file mode 100644
index 0000000000000000000000000000000000000000..525d0dc4b1889c5cfca29bc88587aae6c5ec2c8d
--- /dev/null
+++ b/src/composables/useI18n.ts
@@ -0,0 +1,35 @@
+import type { I18n } from "vue-i18n"
+
+import { isRef, watch, type Ref } from "vue"
+
+import en from '~/locales/en.yaml'
+
+let i18nInstance: I18n
+const provideI18n = (i18n: I18n) => {
+  if (!isRef(i18n.global.locale) || i18n.mode !== 'composition') {
+    console.warn('[vui] locales won\'t be updated when provided i18n instance is in legacy mode')
+    i18nInstance = i18n
+    return
+  }
+
+  const globalMessages = i18n.global.messages as Ref<Record<string, typeof en>>
+  watch(i18n.global.locale, async (locale, from) => {
+    globalMessages.value[locale] ??= {}
+    if ('vui' in globalMessages.value[locale]) return
+
+    try {
+      const { default: messages } = await import(`~/locales/${locale}.yaml`)
+      globalMessages.value[locale] = { ...globalMessages.value[locale], ...messages }
+    } catch (err) {
+      console.warn(`[vui] Locale '${locale}' not found`)
+    }
+  }, { immediate: true })
+
+  i18nInstance = i18n
+}
+
+const useI18nInstance = () => {
+  return i18nInstance
+}
+
+export { provideI18n, useI18nInstance }
diff --git a/src/main.ts b/src/main.ts
index 6b354846903b1a6ef4a2fe76bf399b499d5cbfae..da25feaa3f98d89806ae1c9b033d97377accd4c5 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,12 +1,33 @@
+import { createI18n, type I18n } from 'vue-i18n'
 import type { App } from 'vue'
 
 import '~/styles/funkwhale.scss'
 
 import * as components from '~/components'
+import { provideI18n } from './composables/useI18n'
+import en from '~/locales/en.yaml'
+
 export * from '~/components'
 
+interface VuiOptions {
+  i18n?: I18n
+}
+
 export default {
-  install (app: App) {
+  install (app: App, options: VuiOptions = {}) {
+    if (!options.i18n) {
+      options.i18n = createI18n({
+        legacy: false,
+        locale: 'en',
+        fallbackLocale: 'en',
+        messages: { en }
+      })
+
+      app.use(options.i18n)
+    }
+
+    provideI18n(options.i18n)
+
     for (const prop in components) {
       const component = components[prop as keyof typeof components]
       app.component(prop, component)
diff --git a/vite.config.ts b/vite.config.ts
index 81ce4c84a4b091e373c4308d4da385f93335dc0b..e42eedeeb96f9d7036516df8a1fb3982a467615e 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -1,6 +1,7 @@
 import { defineConfig } from 'vite'
 import { resolve } from 'path'
 
+import yaml from '@modyfi/vite-plugin-yaml'
 import vue from '@vitejs/plugin-vue'
 import dts from 'vite-plugin-dts'
 
@@ -8,6 +9,7 @@ import dts from 'vite-plugin-dts'
 export default defineConfig(() => ({
   plugins: [
     vue(),
+    yaml(),
     dts({
       insertTypesEntry: true,
       outputDir: resolve(__dirname, 'dist/types')