Repository: enzonotario/vitepress-openapi
Pull request #72 (2024-10-07 02:13:18)

Operation Badges && i18n configuration
# Description

Allows to customize Badges to shown above Operations. Also allows to configure i18n and passing custom `messages`.

## Related issues/external references


## Types of changes

- New feature


vercel[bot]: [vc]: #dXJmqdDSn3dC0CKKV0sCiXfj2k8dBAR3BqN+jBKB+iw=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ2aXRlcHJlc3Mtb3BlbmFwaSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJ2aXRlcHJlc3Mtb3BlbmFwaS1naXQtZmVhdHVyZS1oZWEtNDFkZWY4LWVuem8tbm90YXJpb3MtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vZW56by1ub3Rhcmlvcy1wcm9qZWN0cy92aXRlcHJlc3Mtb3BlbmFwaS8zTjQ0eGd3TEtlS1kxY25Xd0RjVmFzTGhEb2RhIiwicHJldmlld1VybCI6InZpdGVwcmVzcy1vcGVuYXBpLWdpdC1mZWF0dXJlLWhlYS00MWRlZjgtZW56by1ub3Rhcmlvcy1wcm9qZWN0cy52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIn1dfQ==
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **vitepress-openapi** | ✅ Ready ([Inspect](https://vercel.com/enzo-notarios-projects/vitepress-openapi/3N44xgwLKeKY1cnWwDcVasLhDoda)) | [Visit Preview](https://vercel.live/open-feedback/vitepress-openapi-git-feature-hea-41def8-enzo-notarios-projects.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vitepress-openapi-git-feature-hea-41def8-enzo-notarios-projects.vercel.app?via=pr-comment-feedback-link) | Oct 7, 2024 2:13am |


coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: review in progress by coderabbit.ai -->

> [!NOTE]
> Currently processing new changes in this PR. This may take a few minutes, please wait...
> 
> 	<details>
> <summary>📥 Commits</summary>
> 
> Files that changed from the base of the PR and between 317a6b83d18f9a55623bd736cb9b4df37432e898 and 91d90160a7ca674ad2f70bd97dae9ec8033f45f6.
> 
> </details>
> 
> <details>
> <summary>📒 Files selected for processing (13)</summary>
> 
> * docs/.vitepress/config.mts (1 hunks)
> * docs/.vitepress/theme/index.ts (2 hunks)
> * docs/composables/useTheme.md (3 hunks)
> * docs/customizations/operation-badges.md (1 hunks)
> * docs/layouts/multiple-specs.md (2 hunks)
> * src/components/Common/OAHeaderBadges.vue (1 hunks)
> * src/components/Common/OAOperation.vue (2 hunks)
> * src/composables/useTheme.ts (4 hunks)
> * src/index.ts (2 hunks)
> * src/locales/en.json (1 hunks)
> * src/locales/es.json (1 hunks)
> * src/locales/index.ts (1 hunks)
> * test/composables/useTheme.test.ts (1 hunks)
> 
> </details>
> 
> 	```ascii
>  ___________________________________________
> < Sending Skynet back to the drawing board. >
>  -------------------------------------------
>   \
>    \   (\__/)
>        (•ㅅ•)
>        / 　 づ
> ```

<!-- end of auto-generated comment: review in progress by coderabbit.ai --><!-- tips_start -->

---

Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://coderabbit.ai/docs) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
pkg-pr-new[bot]: 
[Open in Stackblitz](https://pkg.pr.new/template/e140c9de-562b-496c-b8a0-aaf6c5e36b60)


```
pnpm add https://pkg.pr.new/vitepress-openapi@72
```


```
pnpm add https://pkg.pr.new/vitepress-openapi@72
```


_commit: <a href="https://github.com/enzonotario/vitepress-openapi/runs/31149625712"><code>91d9016</code></a>_


Patch:
diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts
index 7a6ce70..3876717 100644
--- a/docs/.vitepress/config.mts
+++ b/docs/.vitepress/config.mts
@@ -68,6 +68,19 @@ export default defineConfigWithTheme({
           },
         ],
       },
+      {
+        text: 'Customizations',
+        items: [
+          {
+            items: [
+              {
+                text: 'Operation Badges',
+                link: '/customizations/operation-badges',
+              },
+            ],
+          },
+        ],
+      },
       {
         text: 'Example',
         collapsed: true,
diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts
index d9f0a02..daa7042 100644
--- a/docs/.vitepress/theme/index.ts
+++ b/docs/.vitepress/theme/index.ts
@@ -1,4 +1,4 @@
-import { theme, useOpenapi, useTheme } from 'vitepress-openapi'
+import { locales, theme, useOpenapi, useTheme } from 'vitepress-openapi'
 import DefaultTheme from 'vitepress/theme'
 import spec from '../../public/openapi.json' assert {type: 'json'}
 import 'vitepress-openapi/dist/style.css'
@@ -10,6 +10,21 @@ export default {
     const openapi = useOpenapi()
     openapi.setSpec(spec)
 
+    // Optionally, set the i18n configuration.
+    const themeConfig = useTheme()
+    themeConfig.setI18nConfig({
+      messages: {
+        en: {
+          ...locales.en,
+          'operation.badgePrefix.operationId': 'Operation ID: ',
+        },
+        es: {
+          ...locales.es,
+          'operation.badgePrefix.operationId': 'ID de operación: ',
+        },
+      },
+    })
+
     // Use the theme.
     theme.enhanceApp({ app })
   },
diff --git a/docs/composables/useTheme.md b/docs/composables/useTheme.md
index dfcd8e7..505c882 100644
--- a/docs/composables/useTheme.md
+++ b/docs/composables/useTheme.md
@@ -5,15 +5,12 @@ The `useTheme` composable provides functions to configure the theme.
 You can use the `useTheme` composable to configure the theme in your `.vitepress/theme/index.js` file, or in any `.md` page/file.
 
 ```ts
-import { useTheme } from 'vitepress-openapi'
+import { useTheme, locales } from 'vitepress-openapi'
 
 export default {
     async enhanceApp({app, router, siteData}) {
         const themeConfig = useTheme()
         
-        // Set the language.
-        themeConfig.setLocale('en') // en or es
-        
         // Set the default schema view.
         themeConfig.setSchemaDefaultView('schema') // schema or contentType
         
@@ -47,16 +44,35 @@ export default {
         
         // Set the visibility of the navigation bar.
         themeConfig.setPlaygroundJsonEditorNavigationBar(false)
+        
+        // Get the current operation badges.
+        themeConfig.getOperationBadges() // ['deprecated']
+        
+        // Set the operation badges.
+        themeConfig.setOperationBadges(['deprecated'])
+        
+        // Get the current i18n configuration.
+        themeConfig.getI18nConfig() // { locale: 'en', fallbackLocale: 'en', messages: locales }
+        
+        // Set the i18n configuration.
+        themeConfig.setI18nConfig({
+            locale: 'en', // en or es
+            fallbackLocale: 'en', // en or es
+            messages: {
+                en: {
+                    ...locales.en,
+                    'operation.badgePrefix.operationId': 'Operation ID',
+                },
+                es: {
+                    ...locales.es,
+                    'operation.badgePrefix.operationId': 'ID de operación',
+                },
+            },
+        })
     }
 }
 ```
 
-## Global Configuration
-
-| Function    | Description                           | Default Value | Allowed Values |
-|-------------|---------------------------------------|---------------|----------------|
-| `setLocale` | Sets the language (`'es'` or `'en'`). | `'en'`        | `'es'`, `'en'` |
-
 ## Schema Configuration
 
 | Function               | Description                         | Default Value | Allowed Values              |
@@ -96,3 +112,15 @@ export default {
 | `setPlaygroundJsonEditorMode`          | Sets the mode of the JSON editor.          | `'tree'`      | `'text'`, `'tree'`, `'table'` |
 | `setPlaygroundJsonEditorMainMenuBar`   | Sets the visibility of the main menu bar.  | `false`       | `true`, `false`               |
 | `setPlaygroundJsonEditorNavigationBar` | Sets the visibility of the navigation bar. | `false`       | `true`, `false`               |
+
+## Operation Configuration
+
+| Function             | Description                                        | Default Value    | Allowed Values                  |
+|----------------------|----------------------------------------------------|------------------|---------------------------------|
+| `setOperationBadges` | Sets the operation badges. The order is respected. | `['deprecated']` | `['deprecated', 'operationId']` |
+
+## I18n Configuration
+
+| Function        | Description                  | Default Value                                               | Allowed Values                                                                                                                   |
+|-----------------|------------------------------|-------------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------|
+| `setI18nConfig` | Sets the i18n configuration. | `{ locale: 'en', fallbackLocale: 'en', messages: locales }` | `{ locale: 'es' \| 'en', fallbackLocale: 'es' \| 'en', messages: Record<'es' \| 'en', Record<string, Record<string, string>>> }` |
diff --git a/docs/customizations/operation-badges.md b/docs/customizations/operation-badges.md
new file mode 100644
index 0000000..2f01dd9
--- /dev/null
+++ b/docs/customizations/operation-badges.md
@@ -0,0 +1,85 @@
+---
+aside: false
+outline: false
+---
+
+# Operation Badges
+
+Each operation can have different badges that indicate its state, for example if it is deprecated, the operation id, etc. The available badges are:
+
+- `deprecated`
+- `operationId`
+
+By default, only the `deprecated` badge is shown, as appropriate. You can customize the operation badges using the `useTheme().setOperationBadges()` method. **The order in which you set the badges is the order in which they will be displayed.**
+
+For example:
+
+```markdown
+---
+aside: false
+outline: false
+title: vitepress-openapi
+---
+
+<script setup lang="ts">
+import { useData } from 'vitepress'
+import { useTheme } from 'vitepress-openapi'
+import spec from '../public/openapi.json'
+
+const { isDark } = useData()
+
+const themeConfig = useTheme()
+themeConfig.setOperationBadges(['deprecated', 'operationId'])
+</script>
+
+<OAOperation :spec="spec" operationId="getAllData" :isDark="isDark" />
+```
+
+You can also customize the prefix of the badges by setting the `operation.badgePrefix.{badgeName}` key in the i18n messages. For example, in your `.vitepress/theme/index.ts`, before calling `theme.enhanceApp({ app })`, you can set the following:
+
+```typescript
+import { locales, theme, useTheme } from 'vitepress-openapi'
+import DefaultTheme from 'vitepress/theme'
+import 'vitepress-openapi/dist/style.css'
+
+export default {
+    ...DefaultTheme,
+    enhanceApp({ app }) {
+        // Optionally, set the i18n configuration.
+        const themeConfig = useTheme()
+        themeConfig.setI18nConfig({
+            messages: {
+                en: {
+                    ...locales.en,
+                    'operation.badgePrefix.operationId': 'Operation ID: ',
+                },
+                es: {
+                    ...locales.es,
+                    'operation.badgePrefix.operationId': 'ID de operación: ',
+                },
+            },
+        })
+
+        // Use the theme.
+        theme.enhanceApp({ app })
+    },
+}
+
+```
+
+## Example
+
+---
+
+<script setup lang="ts">
+import { useData } from 'vitepress'
+import { useTheme } from 'vitepress-openapi'
+import spec from '../public/openapi.json'
+
+const { isDark } = useData()
+
+const themeConfig = useTheme()
+themeConfig.setOperationBadges(['deprecated', 'operationId'])
+</script>
+
+<OAOperation :spec="spec" operationId="getAllData" :isDark="isDark" />
diff --git a/docs/layouts/multiple-specs.md b/docs/layouts/multiple-specs.md
index 572b1a5..5a895e3 100644
--- a/docs/layouts/multiple-specs.md
+++ b/docs/layouts/multiple-specs.md
@@ -31,7 +31,7 @@ const { isDark } = useData()
 Using [default spec](../public/openapi.json)
 :::
 
-<OAOperation operationId="buyMuseumTickets" :spec="spec" :isDark="isDark" />
+<OAOperation operationId="getAllData" :spec="spec" :isDark="isDark" />
 
 ---
 
@@ -54,7 +54,7 @@ const { isDark } = useData()
 Using [default spec](../public/openapi.json)
 :::
 
-<OAOperation operationId="buyMuseumTickets" :spec="spec" :isDark="isDark" />
+<OAOperation operationId="getAllData" :spec="spec" :isDark="isDark" />
 
 ---
 
diff --git a/src/components/Common/OAHeaderBadges.vue b/src/components/Common/OAHeaderBadges.vue
new file mode 100644
index 0000000..9152650
--- /dev/null
+++ b/src/components/Common/OAHeaderBadges.vue
@@ -0,0 +1,44 @@
+<script setup>
+import { computed, defineProps } from 'vue'
+import { useTheme } from 'vitepress-openapi'
+import { Badge } from 'vitepress-openapi/components/ui/badge'
+
+const { deprecated } = defineProps({
+  operation: {
+    type: Object,
+    required: true,
+  },
+  deprecated: {
+    type: Boolean,
+    required: false,
+  },
+})
+
+const themeConfig = useTheme()
+
+const operationBadges = computed(() => themeConfig.getOperationBadges().filter((badge) => {
+  if (badge === 'deprecated' && !deprecated) {
+    return false
+  }
+
+  return true
+}))
+</script>
+
+<template>
+  <div class="flex flex-row gap-2">
+    <Badge
+      v-for="badge in operationBadges"
+      :key="badge"
+      variant="outline"
+    >
+      <template v-if="badge === 'deprecated'">
+        {{ $t('Deprecated') }}
+      </template>
+
+      <template v-else-if="badge === 'operationId'">
+        {{ $t('operation.badgePrefix.operationId') }}{{ operation.operationId }}
+      </template>
+    </Badge>
+  </div>
+</template>
diff --git a/src/components/Common/OAOperation.vue b/src/components/Common/OAOperation.vue
index 55830cb..5db93b1 100644
--- a/src/components/Common/OAOperation.vue
+++ b/src/components/Common/OAOperation.vue
@@ -1,6 +1,6 @@
 <script setup>
 import { computed, useSlots } from 'vue'
-import { Badge } from 'vitepress-openapi/components/ui/badge'
+import OAHeaderBadges from 'vitepress-openapi/components/Common/OAHeaderBadges.vue'
 
 const props = defineProps({
   operationId: {
@@ -61,14 +61,10 @@ function hasSlot(name) {
       #header="header"
     >
       <div class="flex flex-col">
-        <Badge
-          v-if="header.deprecated"
-          type="deprecated"
-          variant="outline"
-          class="self-start"
-        >
-          {{ $t('Deprecated') }}
-        </Badge>
+        <OAHeaderBadges
+          :operation="header.operation"
+          :deprecated="header.deprecated"
+        />
 
         <OAHeading
           level="h1"
diff --git a/src/composables/useTheme.ts b/src/composables/useTheme.ts
index 39f04a5..5ba442a 100644
--- a/src/composables/useTheme.ts
+++ b/src/composables/useTheme.ts
@@ -1,6 +1,7 @@
 import { ref } from 'vue'
 import vitesseLight from 'shiki/themes/vitesse-light.mjs'
 import vitesseDark from 'shiki/themes/vitesse-dark.mjs'
+import { locales } from '../locales'
 
 interface HeadingLevels {
   h1: number
@@ -14,7 +15,6 @@ interface HeadingLevels {
 type PlaygroundJsonEditorMode = 'text' | 'tree' | 'table'
 
 const themeConfig = {
-  locale: ref<'es' | 'en'>('en'),
   highlighterTheme: {
     light: vitesseLight,
     dark: vitesseDark,
@@ -66,13 +66,31 @@ const securityConfig = {
   selectedScheme: ref<string | null>(null),
 }
 
+type OperationBadges = 'deprecated' | 'operationId'
+
+const operationConfig = {
+  badges: ref<OperationBadges[]>(['deprecated']),
+}
+
+interface I18nConfig {
+  locale: Ref<'es' | 'en' | string>
+  fallbackLocale: Ref<'es' | 'en' | string>
+  messages: Record<'es' | 'en', Record<string, Record<string, string>>>
+}
+
+const i18nConfig: I18nConfig = {
+  locale: ref<'es' | 'en'>('en'),
+  fallbackLocale: ref<'es' | 'en'>('en'),
+  messages: locales,
+}
+
 export function useTheme() {
-  function getLocale(): 'es' | 'en' {
-    return themeConfig.locale.value
+  function getLocale(): 'es' | 'en' | string {
+    return getI18nConfig().locale.value
   }
 
-  function setLocale(value: 'es' | 'en') {
-    themeConfig.locale.value = value
+  function setLocale(value: 'es' | 'en' | string) {
+    setI18nConfig({ locale: value })
   }
 
   function getHighlighterTheme() {
@@ -173,6 +191,32 @@ export function useTheme() {
     playgroundConfig.jsonEditor.navigationBar.value = value
   }
 
+  function getOperationBadges(): OperationBadges[] {
+    return [...operationConfig.badges.value]
+  }
+
+  function setOperationBadges(value: OperationBadges[]) {
+    operationConfig.badges.value = value
+  }
+
+  function getI18nConfig(): I18nConfig {
+    return i18nConfig
+  }
+
+  function setI18nConfig(config: Partial<I18nConfig>) {
+    if (config.locale) {
+      i18nConfig.locale.value = config.locale
+    }
+
+    if (config.fallbackLocale) {
+      i18nConfig.fallbackLocale.value = config.fallbackLocale
+    }
+
+    if (config.messages) {
+      i18nConfig.messages = config.messages
+    }
+  }
+
   return {
     schemaConfig,
     securityConfig,
@@ -200,5 +244,9 @@ export function useTheme() {
     setPlaygroundJsonEditorMainMenuBar,
     getPlaygroundJsonEditorNavigationBar,
     setPlaygroundJsonEditorNavigationBar,
+    getOperationBadges,
+    setOperationBadges,
+    getI18nConfig,
+    setI18nConfig,
   }
 }
diff --git a/src/index.ts b/src/index.ts
index f94bf51..017fc00 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -6,9 +6,6 @@ import { useTheme } from './composables/useTheme'
 import { useShiki } from './composables/useShiki'
 import * as components from './components'
 
-import es from './locales/es.json'
-import en from './locales/en.json'
-
 import 'tailwindcss/tailwind.css'
 import './style.css'
 import './json.css'
@@ -19,6 +16,7 @@ export { useTheme } from './composables/useTheme'
 export { useShiki } from './composables/useShiki'
 export { usePlayground } from './composables/usePlayground'
 export { OpenApi } from './lib/OpenApi'
+export { locales } from './locales'
 
 interface VPTheme {
   Layout: Component
@@ -31,12 +29,9 @@ export const theme = {
     const themeConfig = useTheme()
 
     const i18n = VueI18n.createI18n({
-      locale: themeConfig.getLocale(),
-      fallbackLocale: 'en',
-      messages: {
-        en,
-        es,
-      },
+      locale: themeConfig.getI18nConfig().locale,
+      fallbackLocale: themeConfig.getI18nConfig().fallbackLocale,
+      messages: themeConfig.getI18nConfig().messages,
     })
     app.use(i18n)
 
diff --git a/src/locales/en.json b/src/locales/en.json
index 8fdb6ab..f1bd4f4 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -33,5 +33,6 @@
   "Your browser does not support the audio element.": "Your browser does not support the audio element.",
   "Unrecognized response type. Raw content:": "Unrecognized response type. Raw content:",
   "Download file": "Download file",
-  "Select...": "Select..."
+  "Select...": "Select...",
+  "operation.badgePrefix.operationId": "ID: "
 }
diff --git a/src/locales/es.json b/src/locales/es.json
index a464876..e49a1f3 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -33,5 +33,6 @@
   "Your browser does not support the audio element.": "Tu navegador no soporta el elemento de audio.",
   "Unrecognized response type. Raw content:": "Tipo de respuesta no reconocido. Contenido en crudo:",
   "Download file": "Descargar archivo",
-  "Select...": "Seleccionar..."
+  "Select...": "Seleccionar...",
+  "operation.badgePrefix.operationId": "ID: "
 }
diff --git a/src/locales/index.ts b/src/locales/index.ts
new file mode 100644
index 0000000..28e092c
--- /dev/null
+++ b/src/locales/index.ts
@@ -0,0 +1,7 @@
+import es from './es.json'
+import en from './en.json'
+
+export const locales = {
+  en,
+  es,
+}


Test patch:
diff --git a/test/composables/useTheme.test.ts b/test/composables/useTheme.test.ts
index 82389ea..d9e7bcc 100644
--- a/test/composables/useTheme.test.ts
+++ b/test/composables/useTheme.test.ts
@@ -151,4 +151,30 @@ describe('useTheme', () => {
     const result = theme.getPlaygroundJsonEditorNavigationBar()
     expect(result).toBe(true)
   })
+
+  it('returns the operation badges', () => {
+    const result = theme.getOperationBadges()
+    expect(result).toEqual(['deprecated'])
+  })
+
+  it('sets and gets the operation badges', () => {
+    theme.setOperationBadges(['operationId'])
+    const result = theme.getOperationBadges()
+    expect(result).toEqual(['operationId'])
+  })
+
+  it('returns the i18n config', () => {
+    const result = theme.getI18nConfig()
+    expect(result).toEqual({
+      locale: expect.any(Object),
+      fallbackLocale: expect.any(Object),
+      messages: expect.any(Object),
+    })
+  })
+
+  it('sets and gets the i18n config', () => {
+    theme.setI18nConfig({ locale: 'es' })
+    const result = theme.getI18nConfig()
+    expect(result.locale.value).toBe('es')
+  })
 })