devex: migrate to vue-i18n (#4966)

* sample languages refactor

* feat: consistency + dedupe impl of i18n

* fix: broken imports

* fix: intl formatted component

* fix: use relative imports

* fix: imports

* fix: comment out incomplete locales + fix imports

* feat: cleanup

* fix: ui imports

* fix: lint

* fix: admonition import

* make footer a component, fix language reactivity

* make copyright notice untranslatable

---------

Co-authored-by: Calum H. <contact@cal.engineer>
This commit is contained in:
Prospector
2025-12-27 13:37:37 -08:00
committed by GitHub
parent 3cabc3b967
commit 1bbb01bd42
161 changed files with 1449 additions and 2314 deletions

View File

@@ -100,7 +100,8 @@ import {
XIcon,
} from '@modrinth/assets'
import { capitalizeString } from '@modrinth/utils'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const messages = defineMessages({
acceptedLabel: {

View File

@@ -8,9 +8,10 @@
<script setup lang="ts">
import { CheckIcon, ClipboardCopyIcon } from '@modrinth/assets'
import { defineMessage, useVIntl } from '@vintl/vintl'
import { ref } from 'vue'
import { defineMessage, useVIntl } from '../../composables/i18n'
const copiedMessage = defineMessage({
id: 'omorphia.component.copy.action.copy',
defaultMessage: 'Copy code to clipboard',

View File

@@ -49,7 +49,8 @@
</template>
<script setup lang="ts">
import { ClientIcon, GlobeIcon, InfoIcon, ServerIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
const messages = defineMessages({
clientLabel: {

View File

@@ -17,9 +17,10 @@
<script setup lang="ts">
import { FilterIcon } from '@modrinth/assets'
import { type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { watch } from 'vue'
import { type MessageDescriptor, useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
export type FilterBarOption = {

View File

@@ -1,7 +1,7 @@
<script setup lang="ts">
import { EditIcon, TrashIcon, UploadIcon } from '@modrinth/assets'
import { defineMessages, useVIntl } from '@vintl/vintl'
import { defineMessages, useVIntl } from '../../composables/i18n'
import { Avatar, OverflowMenu } from '../index'
const { formatMessage } = useVIntl()

View File

@@ -0,0 +1,70 @@
<script setup lang="ts">
import IntlMessageFormat, { type FormatXMLElementFn, type PrimitiveType } from 'intl-messageformat'
import { computed, useSlots, type VNode } from 'vue'
import { useI18n } from 'vue-i18n'
import type { MessageDescriptor } from '../../composables/i18n'
const props = defineProps<{
messageId: MessageDescriptor
values?: Record<string, PrimitiveType>
}>()
const slots = useSlots()
const { t, locale } = useI18n()
const formattedParts = computed(() => {
const key = props.messageId.id
const translation = t(key, {}) as string
let msg: string
if (translation && translation !== key) {
msg = translation
} else {
msg = props.messageId.defaultMessage ?? key
}
const slotHandlers: Record<string, FormatXMLElementFn<VNode>> = {}
const slotNames = Object.keys(slots)
for (const slotName of slotNames) {
const normalizedName = slotName.startsWith('~') ? slotName.slice(1) : slotName
slotHandlers[normalizedName] = (chunks) => {
const slot = slots[slotName]
if (slot) {
return slot({
children: chunks,
})
}
return chunks as VNode[]
}
msg = msg.replace(
new RegExp(`\\{${normalizedName}\\}`, 'g'),
`<${normalizedName}></${normalizedName}>`,
)
}
try {
const formatter = new IntlMessageFormat(msg, locale.value)
const result = formatter.format({
...props.values,
...slotHandlers,
})
if (Array.isArray(result)) {
return result
}
return [result]
} catch {
return [msg]
}
})
</script>
<template>
<template v-for="(part, index) in formattedParts" :key="index">
<component :is="() => part" v-if="typeof part === 'object'" />
<template v-else>{{ part }}</template>
</template>
</template>

View File

@@ -292,11 +292,11 @@ import {
XIcon,
YouTubeIcon,
} from '@modrinth/assets'
import NewModal from '@modrinth/ui/src/components/modal/NewModal.vue'
import { markdownCommands, modrinthMarkdownEditorKeymap } from '@modrinth/utils/codemirror'
import { renderHighlightedString } from '@modrinth/utils/highlightjs'
import { type Component, computed, onBeforeUnmount, onMounted, ref, toRef, watch } from 'vue'
import NewModal from '../modal/NewModal.vue'
import Button from './Button.vue'
import Chips from './Chips.vue'
import FileInput from './FileInput.vue'

View File

@@ -31,7 +31,7 @@
</template>
<script setup lang="ts" generic="T">
import { computed, onMounted, ref } from 'vue'
import { computed, onMounted, ref, watch } from 'vue'
const modelValue = defineModel<T>({ required: true })

View File

@@ -34,9 +34,9 @@
<script setup lang="ts">
import { XIcon } from '@modrinth/assets'
import { renderString } from '@modrinth/utils'
import { defineMessages, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import { defineMessages, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import Admonition from './Admonition.vue'
import ButtonStyled from './ButtonStyled.vue'
import CopyCode from './CopyCode.vue'

View File

@@ -1,8 +1,9 @@
<script setup lang="ts">
import type { MessageDescriptor } from '@vintl/vintl'
import { useVIntl } from '@vintl/vintl'
import { computed } from 'vue'
import type { MessageDescriptor } from '../../composables/i18n'
import { useVIntl } from '../../composables/i18n'
const { formatMessage } = useVIntl()
const props = withDefaults(

View File

@@ -1,8 +1,8 @@
<script setup lang="ts" generic="T">
import { HistoryIcon, SaveIcon, SpinnerIcon } from '@modrinth/assets'
import { defineMessage, type MessageDescriptor, useVIntl } from '@vintl/vintl'
import { type Component, computed } from 'vue'
import { defineMessage, type MessageDescriptor, useVIntl } from '../../composables/i18n'
import { commonMessages } from '../../utils'
import ButtonStyled from './ButtonStyled.vue'

View File

@@ -29,6 +29,7 @@ export { default as FilterBar } from './FilterBar.vue'
export { default as HeadingLink } from './HeadingLink.vue'
export { default as HorizontalRule } from './HorizontalRule.vue'
export { default as IconSelect } from './IconSelect.vue'
export { default as IntlFormatted } from './IntlFormatted.vue'
export type { JoinedButtonAction } from './JoinedButtons.vue'
export { default as JoinedButtons } from './JoinedButtons.vue'
export { default as LoadingIndicator } from './LoadingIndicator.vue'