diff --git a/packages/component-library/src/components/form/my-i18n/my-i18n.vue b/packages/component-library/src/components/form/my-i18n/my-i18n.vue
new file mode 100644
index 00000000..3e6cf3b2
--- /dev/null
+++ b/packages/component-library/src/components/form/my-i18n/my-i18n.vue
@@ -0,0 +1,22 @@
+
+ {{ t("foo.bar") }}
+
+
+
diff --git a/packages/component-library/src/components/theme/mt-theme-provider.vue b/packages/component-library/src/components/theme/mt-theme-provider.vue
index 9a03a435..0aee951f 100644
--- a/packages/component-library/src/components/theme/mt-theme-provider.vue
+++ b/packages/component-library/src/components/theme/mt-theme-provider.vue
@@ -3,13 +3,14 @@
diff --git a/packages/component-library/src/composables/useI18n.spec.ts b/packages/component-library/src/composables/useI18n.spec.ts
new file mode 100644
index 00000000..f739cfa6
--- /dev/null
+++ b/packages/component-library/src/composables/useI18n.spec.ts
@@ -0,0 +1,34 @@
+import { render, screen } from "@testing-library/vue";
+import { useI18n } from "./useI18n";
+
+describe("useI18n", () => {
+ it("returns the translation for the given path", () => {
+ render({
+ setup() {
+ const { t } = useI18n({
+ messages: { en: { greeting: "Hello!" } },
+ });
+
+ return { t };
+ },
+ template: "{{ t('greeting') }}",
+ });
+
+ expect(screen.getByText("Hello!")).toBeInTheDocument();
+ });
+
+ it("returns the path to the translation if no translation is found", () => {
+ render({
+ setup() {
+ const { t } = useI18n({
+ messages: { en: { greeting: "Hello!" } },
+ });
+
+ return { t };
+ },
+ template: "{{ t('path.to.translation') }}",
+ });
+
+ expect(screen.getByText("path.to.translation")).toBeInTheDocument();
+ });
+});
diff --git a/packages/component-library/src/composables/useI18n.ts b/packages/component-library/src/composables/useI18n.ts
new file mode 100644
index 00000000..f8ac95ef
--- /dev/null
+++ b/packages/component-library/src/composables/useI18n.ts
@@ -0,0 +1,53 @@
+import { provide, inject } from "vue";
+
+function get(obj: Record, path: string) {
+ if (typeof obj !== "object" || obj === null) return undefined;
+
+ const keys = path.split(".");
+ let result = obj;
+
+ for (const key of keys) {
+ if (result === undefined || result === null) return undefined;
+
+ // @ts-ignore
+ result = result[key];
+ }
+
+ return result;
+}
+
+interface TranslationDictionary {
+ [key: string]: string | TranslationDictionary;
+}
+
+type Options = { messages: TranslationDictionary };
+
+const defaultI18nState = {
+ locale: "en",
+ defaultLocale: "en",
+};
+
+const i18nInjectionKey = Symbol("mt-i18n");
+
+export function provideI18n(locale: string = "en") {
+ const state = {
+ ...defaultI18nState,
+ locale: locale,
+ };
+
+ provide(i18nInjectionKey, state);
+}
+
+export function useI18n({ messages }: Options) {
+ const i18n = inject(i18nInjectionKey, defaultI18nState);
+
+ function translate(path: string): string {
+ const translation = get(messages, `${i18n.locale}.${path}`);
+ if (translation) return translation.toString();
+
+ const fallback = get(messages, `${i18n.defaultLocale}.${path}`);
+ return fallback ? fallback.toString() : path;
+ }
+
+ return { t: translate };
+}