diff --git a/app.vue b/app.vue
index a14e8a1a..3b501b74 100644
--- a/app.vue
+++ b/app.vue
@@ -12,5 +12,6 @@ const key = computed(() => `${currentUser.value?.server ?? currentServer.value}:
+
diff --git a/components/aria/AriaAnnouncer.vue b/components/aria/AriaAnnouncer.vue
new file mode 100644
index 00000000..04d7997b
--- /dev/null
+++ b/components/aria/AriaAnnouncer.vue
@@ -0,0 +1,56 @@
+
+
+
+
+ {{ ariaMessage }}
+
+
diff --git a/components/aria/AriaLog.vue b/components/aria/AriaLog.vue
new file mode 100644
index 00000000..fd4a3c1e
--- /dev/null
+++ b/components/aria/AriaLog.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
diff --git a/components/aria/AriaStatus.vue b/components/aria/AriaStatus.vue
new file mode 100644
index 00000000..567010be
--- /dev/null
+++ b/components/aria/AriaStatus.vue
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+ {{ status }}
+
+
+
diff --git a/composables/aria/index.ts b/composables/aria/index.ts
new file mode 100644
index 00000000..5546ecba
--- /dev/null
+++ b/composables/aria/index.ts
@@ -0,0 +1,60 @@
+import type { AriaAnnounceType } from '~/composables/aria/types'
+
+const ariaAnnouncer = useEventBus(Symbol('aria-announcer'))
+
+export const useAriaAnnouncer = () => {
+ const announce = (message: string) => {
+ ariaAnnouncer.emit('announce', message)
+ }
+
+ const mute = () => {
+ ariaAnnouncer.emit('mute')
+ }
+
+ const unmute = () => {
+ ariaAnnouncer.emit('unmute')
+ }
+
+ return { announce, ariaAnnouncer, mute, unmute }
+}
+
+export const useAriaLog = () => {
+ let logs = $ref([])
+
+ const announceLogs = (messages: any[]) => {
+ logs = messages
+ }
+
+ const appendLogs = (messages: any[]) => {
+ logs = logs.concat(messages)
+ }
+
+ const clearLogs = () => {
+ logs = []
+ }
+
+ return {
+ announceLogs,
+ appendLogs,
+ clearLogs,
+ logs,
+ }
+}
+
+export const useAriaStatus = () => {
+ let status = $ref('')
+
+ const announceStatus = (message: any) => {
+ status = message
+ }
+
+ const clearStatus = () => {
+ status = ''
+ }
+
+ return {
+ announceStatus,
+ clearStatus,
+ status,
+ }
+}
diff --git a/composables/aria/types.ts b/composables/aria/types.ts
new file mode 100644
index 00000000..ca5457ae
--- /dev/null
+++ b/composables/aria/types.ts
@@ -0,0 +1,2 @@
+export type AriaLive = 'off' | 'polite' | 'assertive'
+export type AriaAnnounceType = 'announce' | 'mute' | 'unmute'
diff --git a/error.vue b/error.vue
index 1c081a7e..7a83fc3e 100644
--- a/error.vue
+++ b/error.vue
@@ -55,5 +55,6 @@ const reload = async () => {
+
diff --git a/locales/en-US.json b/locales/en-US.json
index 6d89c2ea..db65b6d9 100644
--- a/locales/en-US.json
+++ b/locales/en-US.json
@@ -1,4 +1,11 @@
{
+ "a11y": {
+ "loading_page": "Loading page, please wait",
+ "loading_titled_page": "Loading {0} page, please wait",
+ "locale_changed": "Language changed to {0}",
+ "locale_changing": "Changing language, please wait",
+ "route_loaded": "Page {0} loaded"
+ },
"account": {
"avatar_description": "{0}'s avatar",
"blocked_by": "You're blocked by this user.",
diff --git a/locales/es-ES.json b/locales/es-ES.json
index c6fddd42..3121ff88 100644
--- a/locales/es-ES.json
+++ b/locales/es-ES.json
@@ -1,4 +1,11 @@
{
+ "a11y": {
+ "loading_page": "Cargando página, espere por favor",
+ "loading_titled_page": "Cargando página {0}, espere por favor",
+ "locale_changed": "Idioma cambiado a {0}",
+ "locale_changing": "Cambiando idioma, espere por favor",
+ "route_loaded": "Página {0} cargada"
+ },
"account": {
"avatar_description": "avatar de {0}",
"blocked_by": "Estás bloqueado por este usuario.",