feat: make byte size units translatable (#5969)
Make byte size units translatable
This commit is contained in:
@@ -30,9 +30,10 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
|
||||
interface Props {
|
||||
maxValue: number
|
||||
currentValue: number
|
||||
@@ -59,6 +60,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
],
|
||||
})
|
||||
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const currentPhrase = ref('')
|
||||
const usedPhrases = ref(new Set<number>())
|
||||
let phraseInterval: NodeJS.Timeout | null = null
|
||||
|
||||
@@ -50,6 +50,7 @@ import { FolderUpIcon } from '@modrinth/assets'
|
||||
import { fileIsValid } from '@modrinth/utils'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useFormatBytes } from '../../composables'
|
||||
import { injectNotificationManager } from '../../providers'
|
||||
|
||||
const { addNotification } = injectNotificationManager()
|
||||
@@ -78,6 +79,8 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const files = ref<File[]>([])
|
||||
|
||||
function matchesAccept(file: File, accept?: string): boolean {
|
||||
@@ -129,7 +132,7 @@ function addFiles(incoming: FileList, shouldNotReset = false) {
|
||||
alertOnInvalid: true,
|
||||
}
|
||||
|
||||
files.value = files.value.filter((file) => fileIsValid(file, validationOptions))
|
||||
files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
|
||||
|
||||
if (files.value.length > 0) {
|
||||
emit('change', files.value)
|
||||
|
||||
@@ -12,73 +12,62 @@
|
||||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup lang="ts">
|
||||
import { fileIsValid } from '@modrinth/utils'
|
||||
import { defineComponent } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
prompt: {
|
||||
type: String,
|
||||
default: 'Select file',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
import { useFormatBytes } from '../../composables'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
prompt?: string
|
||||
multiple?: boolean
|
||||
accept?: string
|
||||
/**
|
||||
* The max file size in bytes
|
||||
*/
|
||||
maxSize: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
showIcon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
shouldAlwaysReset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
longStyle: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxSize?: number | null
|
||||
showIcon?: boolean
|
||||
shouldAlwaysReset?: boolean
|
||||
longStyle?: boolean
|
||||
disabled?: boolean
|
||||
}>(),
|
||||
{
|
||||
prompt: 'Select file',
|
||||
multiple: false,
|
||||
showIcon: true,
|
||||
shouldAlwaysReset: false,
|
||||
longStyle: false,
|
||||
disabled: false,
|
||||
},
|
||||
emits: ['change'],
|
||||
data() {
|
||||
return {
|
||||
files: [],
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addFiles(files, shouldNotReset) {
|
||||
if (!shouldNotReset || this.shouldAlwaysReset) {
|
||||
this.files = files
|
||||
}
|
||||
const validationOptions = { maxSize: this.maxSize, alertOnInvalid: true }
|
||||
this.files = [...this.files].filter((file) => fileIsValid(file, validationOptions))
|
||||
if (this.files.length > 0) {
|
||||
this.$emit('change', this.files)
|
||||
}
|
||||
},
|
||||
handleDrop(e) {
|
||||
this.addFiles(e.dataTransfer.files)
|
||||
},
|
||||
handleChange(e) {
|
||||
this.addFiles(e.target.files)
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
const emit = defineEmits<{ change: [files: File[]] }>()
|
||||
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const files = ref<File[]>([])
|
||||
|
||||
function addFiles(incoming: FileList, shouldNotReset = false) {
|
||||
if (!shouldNotReset || props.shouldAlwaysReset) {
|
||||
files.value = Array.from(incoming)
|
||||
}
|
||||
const validationOptions = { maxSize: props.maxSize, alertOnInvalid: true }
|
||||
files.value = files.value.filter((file) => fileIsValid(file, validationOptions, formatBytes))
|
||||
if (files.value.length > 0) {
|
||||
emit('change', files.value)
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(e: DragEvent) {
|
||||
addFiles(e.dataTransfer!.files)
|
||||
}
|
||||
|
||||
function handleChange(e: Event) {
|
||||
const input = e.target as HTMLInputElement
|
||||
if (!input.files) return
|
||||
addFiles(input.files)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -268,16 +268,12 @@ import {
|
||||
Pagination,
|
||||
TagItem,
|
||||
useCompactNumber,
|
||||
useFormatBytes,
|
||||
useFormatDateTime,
|
||||
VersionChannelIndicator,
|
||||
VersionFilterControl,
|
||||
} from '@modrinth/ui'
|
||||
import {
|
||||
formatBytes,
|
||||
formatVersionsForDisplay,
|
||||
type GameVersionTag,
|
||||
type Version,
|
||||
} from '@modrinth/utils'
|
||||
import { formatVersionsForDisplay, type GameVersionTag, type Version } from '@modrinth/utils'
|
||||
import { Menu } from 'floating-vue'
|
||||
import { computed, type Ref, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
@@ -293,6 +289,7 @@ const formatDateTime = useFormatDateTime({
|
||||
timeStyle: 'short',
|
||||
dateStyle: 'long',
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
type VersionWithDisplayUrlEnding = Version & {
|
||||
displayUrlEnding: string
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<span>
|
||||
{{
|
||||
formatMessage(messages.extracted, {
|
||||
size: 'bytes_processed' in op ? formatBytes(op.bytes_processed ?? 0) : '0 B',
|
||||
size: formatBytes(op.bytes_processed ?? 0),
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
@@ -35,11 +35,11 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PackageOpenIcon } from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import type { FileOperation } from '#ui/layouts/shared/files-tab/types'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
@@ -53,6 +53,7 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatBytes = useFormatBytes()
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const messages = defineMessages({
|
||||
|
||||
@@ -25,13 +25,15 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { UploadIcon } from '@modrinth/assets'
|
||||
import { formatBytes } from '@modrinth/utils'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import Admonition from '#ui/components/base/Admonition.vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
import { injectModrinthServerContext } from '#ui/providers'
|
||||
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const ctx = injectModrinthServerContext()
|
||||
|
||||
const state = computed(() => ctx.uploadState.value)
|
||||
|
||||
39
packages/ui/src/composables/format-bytes.ts
Normal file
39
packages/ui/src/composables/format-bytes.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { defineMessage, useVIntl } from '#ui/composables/i18n.ts'
|
||||
|
||||
const messages = [
|
||||
defineMessage({
|
||||
id: 'format.bytes.0',
|
||||
defaultMessage: '{count, plural, one {# byte} other {# bytes}}',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'format.bytes.1',
|
||||
defaultMessage: '{count, number} KiB',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'format.bytes.2',
|
||||
defaultMessage: '{count, number} MiB',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'format.bytes.3',
|
||||
defaultMessage: '{count, number} GiB',
|
||||
}),
|
||||
defineMessage({
|
||||
id: 'format.bytes.4',
|
||||
defaultMessage: '{count, number} TiB',
|
||||
}),
|
||||
]
|
||||
|
||||
export function useFormatBytes() {
|
||||
const { formatMessage } = useVIntl()
|
||||
|
||||
function format(bytes: number, decimals = 2): string {
|
||||
if (bytes === 0) return formatMessage(messages[0], { count: 0 })
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), messages.length - 1)
|
||||
return formatMessage(messages[exponent], {
|
||||
count: (bytes / Math.pow(1024, exponent)).toFixed(decimals),
|
||||
})
|
||||
}
|
||||
|
||||
return format
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './debug-logger'
|
||||
export * from './dynamic-font-size'
|
||||
export * from './format-bytes'
|
||||
export * from './format-date-time'
|
||||
export * from './format-money'
|
||||
export * from './format-number'
|
||||
|
||||
@@ -104,6 +104,7 @@ import { computed, ref } from 'vue'
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import Checkbox from '#ui/components/base/Checkbox.vue'
|
||||
import TeleportOverflowMenu from '#ui/components/base/TeleportOverflowMenu.vue'
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
import { useFormatDateTime } from '#ui/composables/format-date-time'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications'
|
||||
@@ -164,8 +165,6 @@ const isDropTarget = computed(
|
||||
)
|
||||
const isDragSource = computed(() => fileDragActive.value && fileDragData.value?.path === props.path)
|
||||
|
||||
const units = Object.freeze(['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'])
|
||||
|
||||
const formatDateTime = useFormatDateTime({
|
||||
year: '2-digit',
|
||||
month: '2-digit',
|
||||
@@ -173,6 +172,7 @@ const formatDateTime = useFormatDateTime({
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
})
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const containerClasses = computed(() => {
|
||||
const dropTarget = isDropTarget.value
|
||||
@@ -307,12 +307,7 @@ const formattedSize = computed(() => {
|
||||
}
|
||||
|
||||
if (props.size === undefined) return ''
|
||||
const bytes = props.size
|
||||
if (bytes === 0) return '0 B'
|
||||
|
||||
const exponent = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)
|
||||
const size = (bytes / Math.pow(1024, exponent)).toFixed(2)
|
||||
return `${size} ${units[exponent]}`
|
||||
return formatBytes(props.size)
|
||||
})
|
||||
|
||||
function openContextMenu(event: MouseEvent) {
|
||||
|
||||
@@ -115,12 +115,14 @@ import { CheckCircleIcon, FolderOpenIcon, SpinnerIcon, XCircleIcon } from '@modr
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
import { defineMessages, useVIntl } from '#ui/composables/i18n'
|
||||
import { injectModrinthClient } from '#ui/providers/api-client'
|
||||
import { injectNotificationManager } from '#ui/providers/web-notifications'
|
||||
import { commonMessages } from '#ui/utils/common-messages'
|
||||
|
||||
const { formatMessage } = useVIntl()
|
||||
const formatBytes = useFormatBytes()
|
||||
const { addNotification } = injectNotificationManager()
|
||||
const client = injectModrinthClient()
|
||||
|
||||
@@ -240,13 +242,6 @@ watch(
|
||||
{ deep: true },
|
||||
)
|
||||
|
||||
const formatFileSize = (bytes: number): string => {
|
||||
if (bytes < 1024) return bytes + ' B'
|
||||
if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + ' KB'
|
||||
if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + ' MB'
|
||||
return (bytes / 1024 ** 3).toFixed(1) + ' GB'
|
||||
}
|
||||
|
||||
const cancelUpload = (item: UploadItem) => {
|
||||
if (item.uploader && item.status === 'uploading') {
|
||||
item.uploader.cancel()
|
||||
@@ -269,7 +264,7 @@ const uploadFile = async (file: File) => {
|
||||
file,
|
||||
progress: 0,
|
||||
status: 'pending',
|
||||
size: formatFileSize(file.size),
|
||||
size: formatBytes(file.size, 1),
|
||||
}
|
||||
|
||||
uploadQueue.value.push(uploadItem)
|
||||
|
||||
@@ -65,6 +65,7 @@ import { useStorage } from '@vueuse/core'
|
||||
import { computed, defineAsyncComponent, onMounted, ref, shallowRef, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { useFormatBytes } from '#ui/composables'
|
||||
import { injectModrinthServerContext, injectPageContext } from '#ui/providers'
|
||||
|
||||
const VueApexCharts = defineAsyncComponent(() => import('vue3-apexcharts'))
|
||||
@@ -92,6 +93,8 @@ const props = withDefaults(
|
||||
},
|
||||
)
|
||||
|
||||
const formatBytes = useFormatBytes()
|
||||
|
||||
const chartsReady = ref(new Set<number>())
|
||||
const userPreferences = useStorage(`pyro-server-${serverId || 'unknown'}-preferences`, {
|
||||
ramAsNumber: false,
|
||||
@@ -178,21 +181,10 @@ const ramChartOptions = computed(() => buildChartOptions(ramWarning.value, 1, ra
|
||||
const cpuSeries = computed(() => [{ name: 'CPU', data: cpuData.value }])
|
||||
const ramSeries = computed(() => [{ name: 'Memory', data: ramData.value }])
|
||||
|
||||
const formatBytes = (bytes: number) => {
|
||||
const units = ['B', 'KB', 'MB', 'GB']
|
||||
let value = bytes
|
||||
let unit = 0
|
||||
while (value >= 1024 && unit < units.length - 1) {
|
||||
value /= 1024
|
||||
unit++
|
||||
}
|
||||
return `${Math.round(value * 10) / 10} ${units[unit]}`
|
||||
}
|
||||
|
||||
const metrics = computed(() => {
|
||||
const storageMetric = {
|
||||
title: 'Storage',
|
||||
value: props.loading ? '0 B' : formatBytes(stats.value.storage_usage_bytes ?? 0),
|
||||
value: formatBytes(props.loading ? 0 : (stats.value.storage_usage_bytes ?? 0), 1),
|
||||
secondary: null as string | null,
|
||||
icon: FolderOpenIcon,
|
||||
showGraph: false,
|
||||
@@ -241,10 +233,10 @@ const metrics = computed(() => {
|
||||
{
|
||||
title: 'Memory',
|
||||
value: showRamAsBytes.value
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0)
|
||||
? formatBytes(stats.value.ram_usage_bytes ?? 0, 1)
|
||||
: `${ramPercent.value.toFixed(2)}%`,
|
||||
secondary: showRamAsBytes.value
|
||||
? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0)}`
|
||||
? `/ ${formatBytes(stats.value.ram_total_bytes ?? 0, 1)}`
|
||||
: (null as string | null),
|
||||
icon: DatabaseIcon,
|
||||
showGraph: true,
|
||||
|
||||
@@ -1217,6 +1217,21 @@
|
||||
"form.placeholder.state": {
|
||||
"defaultMessage": "Enter state/province"
|
||||
},
|
||||
"format.bytes.0": {
|
||||
"defaultMessage": "{count, plural, one {# byte} other {# bytes}}"
|
||||
},
|
||||
"format.bytes.1": {
|
||||
"defaultMessage": "{count, number} KiB"
|
||||
},
|
||||
"format.bytes.2": {
|
||||
"defaultMessage": "{count, number} MiB"
|
||||
},
|
||||
"format.bytes.3": {
|
||||
"defaultMessage": "{count, number} GiB"
|
||||
},
|
||||
"format.bytes.4": {
|
||||
"defaultMessage": "{count, number} TiB"
|
||||
},
|
||||
"header.category.category": {
|
||||
"defaultMessage": "Category"
|
||||
},
|
||||
|
||||
@@ -94,18 +94,6 @@ export const sortedCategories = (tags, formatCategoryName, locale) => {
|
||||
})
|
||||
}
|
||||
|
||||
export const formatBytes = (bytes, decimals = 2) => {
|
||||
if (bytes === 0) return '0 Bytes'
|
||||
|
||||
const k = 1024
|
||||
const dm = decimals < 0 ? 0 : decimals
|
||||
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB']
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
|
||||
}
|
||||
|
||||
export const capitalizeString = (name) => {
|
||||
return name ? name.charAt(0).toUpperCase() + name.slice(1) : name
|
||||
}
|
||||
@@ -240,7 +228,7 @@ export function cycleValue<T extends string>(value: T, values: T[]): T {
|
||||
return values[index % values.length]
|
||||
}
|
||||
|
||||
export const fileIsValid = (file, validationOptions) => {
|
||||
export const fileIsValid = (file, validationOptions, formatBytes) => {
|
||||
const { maxSize, alertOnInvalid } = validationOptions
|
||||
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
|
||||
if (alertOnInvalid) {
|
||||
|
||||
Reference in New Issue
Block a user