>({})
+ const optionIds = computed(() =>
+ toValue(settings)
+ .map((setting) => setting.option_id)
+ .sort(),
+ )
+ let generation = 0
+ let stopListening: UnlistenFn | undefined
+ let refreshSources = false
+
+ async function refresh() {
+ if (!toValue(opened) || !optionIds.value.length) return
+ const request = ++generation
+ const reindex = refreshSources
+ refreshSources = false
+ try {
+ const result = await get_game_setting_locale_labels(
+ toValue(instanceId),
+ locale.value,
+ optionIds.value,
+ reindex,
+ )
+ if (request === generation && toValue(opened)) labels.value = result.settings
+ } catch (error) {
+ console.debug('Could not load Minecraft setting labels', error)
+ }
+ }
+
+ watch(
+ () => toValue(opened),
+ (active, _, onCleanup) => {
+ let cancelled = false
+ onCleanup(() => {
+ cancelled = true
+ stopListening?.()
+ stopListening = undefined
+ generation++
+ labels.value = {}
+ })
+ if (!active) return
+ refreshSources = true
+ void listen('game-option-locales-updated', () => void refresh())
+ .then((unlisten) => {
+ if (cancelled) unlisten()
+ else {
+ stopListening = unlisten
+ void refresh()
+ }
+ })
+ .catch((error) => {
+ console.debug('Could not listen for Minecraft setting labels', error)
+ if (!cancelled) void refresh()
+ })
+ },
+ { flush: 'sync' },
+ )
+
+ watch([locale, () => toValue(instanceId), () => optionIds.value.join('\n')], () => {
+ generation++
+ labels.value = {}
+ void refresh()
+ })
+
+ onScopeDispose(() => {
+ generation++
+ stopListening?.()
+ labels.value = {}
+ })
+
+ return labels
+}
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue
index 5482bbf067..085988ab39 100644
--- a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/index.vue
@@ -50,7 +50,7 @@ const messages = defineMessages({
},
resourcePacksDescription: {
id: 'app.settings.synced-options.resource-packs.description',
- defaultMessage: 'Use the same resource packs across your instances',
+ defaultMessage: 'Use the same resource packs across your instances.',
},
dataPacks: { id: 'app.settings.synced-options.data-packs', defaultMessage: 'Sync data packs' },
dataPacksDescription: {
@@ -272,13 +272,13 @@ const baseSourcesLoading = computed(() =>
baseOption.value === null
? false
: baseOption.value === 'game_options'
- ? gameOptionSourcesQuery.isFetching.value
- : instancesQuery.isFetching.value,
+ ? gameOptionSourcesQuery.isPending.value
+ : instancesQuery.isPending.value,
)
const baseSourcesError = computed(() =>
baseOption.value === 'game_options'
- ? gameOptionSourcesQuery.isError.value
- : instancesQuery.isError.value,
+ ? gameOptionSourcesQuery.isError.value && !gameOptionSourcesQuery.data.value
+ : instancesQuery.isError.value && !instancesQuery.data.value,
)
let baseSourceGeneration = 0
diff --git a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue
index e5ac9f5698..4f4802b9b7 100644
--- a/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue
+++ b/apps/app-frontend/src/components/ui/settings/instances/instances-synced-settings/launch-options.vue
@@ -335,6 +335,8 @@ const messages = defineMessages({
:step="64"
:snap-points="snapPoints"
:snap-range="512"
+ min-label="512 MB"
+ :max-label="`${Number((maxMemory / 1024).toFixed(1))} GB`"
unit="MB"
/>
diff --git a/apps/app-frontend/src/components/ui/world/WorldItem.vue b/apps/app-frontend/src/components/ui/world/WorldItem.vue
index c7b1282ba2..1cb1b19655 100644
--- a/apps/app-frontend/src/components/ui/world/WorldItem.vue
+++ b/apps/app-frontend/src/components/ui/world/WorldItem.vue
@@ -6,6 +6,7 @@ import {
EyeIcon,
FolderOpenIcon,
IssuesIcon,
+ Link2Icon,
MoreVerticalIcon,
NoSignalIcon,
PlayIcon,
@@ -252,6 +253,10 @@ const messages = defineMessages({
id: 'instance.worlds.create_shortcut',
defaultMessage: 'Create shortcut',
},
+ syncedServer: {
+ id: 'instance.worlds.synced_server',
+ defaultMessage: 'Synced across instances',
+ },
linkedServer: {
id: 'instance.worlds.linked_server',
defaultMessage: 'Managed by server project',
@@ -498,6 +503,16 @@ function openContextMenu(event: MouseEvent) {
>
+
+
+
+}
+
+export async function get_game_setting_locale_labels(
+ instanceId: string | undefined,
+ locale: string,
+ optionIds: string[],
+ refreshSources = false,
+): Promise<{ settings: Record }> {
+ return await invoke('plugin:instance|instance_get_game_setting_locale_labels', {
+ instanceId,
+ locale,
+ optionIds,
+ refreshSources,
+ })
+}
+
export async function list_game_options_sync_sources(): Promise {
return await invoke('plugin:instance|instance_list_game_options_sync_sources')
}
diff --git a/apps/app-frontend/src/locales/en-US/index.json b/apps/app-frontend/src/locales/en-US/index.json
index ebeec35d0c..3163ae194e 100644
--- a/apps/app-frontend/src/locales/en-US/index.json
+++ b/apps/app-frontend/src/locales/en-US/index.json
@@ -1694,129 +1694,6 @@
"app.settings.game-options.category.video.label": {
"message": "Video"
},
- "app.settings.game-options.choice.all": {
- "message": "All"
- },
- "app.settings.game-options.choice.balanced": {
- "message": "Balanced"
- },
- "app.settings.game-options.choice.by-player": {
- "message": "By player"
- },
- "app.settings.game-options.choice.chat": {
- "message": "Chat"
- },
- "app.settings.game-options.choice.commands-only": {
- "message": "Commands only"
- },
- "app.settings.game-options.choice.constant": {
- "message": "Constant"
- },
- "app.settings.game-options.choice.crosshair": {
- "message": "Crosshair"
- },
- "app.settings.game-options.choice.custom": {
- "message": "Custom"
- },
- "app.settings.game-options.choice.decreased": {
- "message": "Decreased"
- },
- "app.settings.game-options.choice.default": {
- "message": "Default"
- },
- "app.settings.game-options.choice.fabulous": {
- "message": "Fabulous"
- },
- "app.settings.game-options.choice.fancy": {
- "message": "Fancy"
- },
- "app.settings.game-options.choice.far": {
- "message": "Far"
- },
- "app.settings.game-options.choice.fast": {
- "message": "Fast"
- },
- "app.settings.game-options.choice.frequent": {
- "message": "Frequent"
- },
- "app.settings.game-options.choice.hidden": {
- "message": "Hidden"
- },
- "app.settings.game-options.choice.hotbar": {
- "message": "Hotbar"
- },
- "app.settings.game-options.choice.left": {
- "message": "Left"
- },
- "app.settings.game-options.choice.limited": {
- "message": "Limited"
- },
- "app.settings.game-options.choice.max-fps": {
- "message": "Max FPS"
- },
- "app.settings.game-options.choice.maximum": {
- "message": "Maximum"
- },
- "app.settings.game-options.choice.minimal": {
- "message": "Minimal"
- },
- "app.settings.game-options.choice.minimum": {
- "message": "Minimum"
- },
- "app.settings.game-options.choice.nearby": {
- "message": "Nearby"
- },
- "app.settings.game-options.choice.never": {
- "message": "Never"
- },
- "app.settings.game-options.choice.none": {
- "message": "None"
- },
- "app.settings.game-options.choice.normal": {
- "message": "Normal"
- },
- "app.settings.game-options.choice.off": {
- "message": "Off"
- },
- "app.settings.game-options.choice.on": {
- "message": "On"
- },
- "app.settings.game-options.choice.opengl": {
- "message": "OpenGL"
- },
- "app.settings.game-options.choice.pause": {
- "message": "Pause menu"
- },
- "app.settings.game-options.choice.pause-and-toast": {
- "message": "Pause menu and toast"
- },
- "app.settings.game-options.choice.power-saver": {
- "message": "Power saver"
- },
- "app.settings.game-options.choice.right": {
- "message": "Right"
- },
- "app.settings.game-options.choice.short": {
- "message": "Short"
- },
- "app.settings.game-options.choice.shown": {
- "message": "Shown"
- },
- "app.settings.game-options.choice.system": {
- "message": "System"
- },
- "app.settings.game-options.choice.tiny": {
- "message": "Tiny"
- },
- "app.settings.game-options.choice.vulkan": {
- "message": "Vulkan"
- },
- "app.settings.game-options.choice.when-minimized": {
- "message": "When minimized"
- },
- "app.settings.game-options.choice.while-afk": {
- "message": "While AFK"
- },
"app.settings.game-options.compatibility.none": {
"message": "Some of your instances cannot use this setting"
},
@@ -1988,654 +1865,84 @@
"app.settings.game-options.keybind.unsupported-status": {
"message": "That input cannot be used by Minecraft. Press another key."
},
- "app.settings.game-options.setting.advanced-opengl.label": {
- "message": "Advanced OpenGL"
- },
- "app.settings.game-options.setting.allow-cursor-changes.label": {
- "message": "Allow cursor changes"
- },
"app.settings.game-options.setting.allow-server-listing.description": {
"message": "Allow the player's name to appear in server listings."
},
- "app.settings.game-options.setting.allow-server-listing.label": {
- "message": "Server listings"
- },
- "app.settings.game-options.setting.alternate-blocks.label": {
- "message": "Alternate blocks"
- },
- "app.settings.game-options.setting.ambient-occlusion.label": {
- "message": "Smooth lighting"
- },
- "app.settings.game-options.setting.ambient-volume.label": {
- "message": "Ambient/Environment"
- },
- "app.settings.game-options.setting.anaglyph-3d.label": {
- "message": "3D anaglyph"
- },
- "app.settings.game-options.setting.anisotropic-filtering.label": {
- "message": "Anisotropic filtering"
- },
- "app.settings.game-options.setting.anisotropy.label": {
- "message": "Anisotropy"
- },
- "app.settings.game-options.setting.attack-indicator.label": {
- "message": "Attack indicator"
- },
"app.settings.game-options.setting.auto-jump.description": {
"message": "Automatically jump up one-block-high obstacles."
},
- "app.settings.game-options.setting.auto-jump.label": {
- "message": "Auto-jump"
- },
- "app.settings.game-options.setting.auto-suggestions.label": {
- "message": "Command suggestions"
- },
- "app.settings.game-options.setting.autosave-indicator.label": {
- "message": "Autosave indicator"
- },
"app.settings.game-options.setting.biome-blend-radius.description": {
"message": "The distance over which biome colors transition."
},
- "app.settings.game-options.setting.biome-blend-radius.label": {
- "message": "Biome blend"
- },
- "app.settings.game-options.setting.blocks-volume.label": {
- "message": "Blocks"
- },
- "app.settings.game-options.setting.brightness.label": {
- "message": "Brightness"
- },
"app.settings.game-options.setting.cape.description": {
"message": "Show the player's cape, including its elytra texture."
},
- "app.settings.game-options.setting.cape.label": {
- "message": "Cape"
- },
- "app.settings.game-options.setting.chat-background-only.label": {
- "message": "Chat background only"
- },
- "app.settings.game-options.setting.chat-colors.label": {
- "message": "Chat colors"
- },
- "app.settings.game-options.setting.chat-delay.label": {
- "message": "Chat delay"
- },
- "app.settings.game-options.setting.chat-line-spacing.label": {
- "message": "Chat line spacing"
- },
"app.settings.game-options.setting.chat-links-prompt.description": {
"message": "Ask before opening links from chat."
},
- "app.settings.game-options.setting.chat-links-prompt.label": {
- "message": "Prompt on links"
- },
"app.settings.game-options.setting.chat-links.description": {
"message": "Allow web links in chat to be opened."
},
- "app.settings.game-options.setting.chat-links.label": {
- "message": "Web links"
- },
"app.settings.game-options.setting.chat-opacity.description": {
"message": "The opacity of chat text."
},
- "app.settings.game-options.setting.chat-opacity.label": {
- "message": "Chat opacity"
- },
- "app.settings.game-options.setting.chat-preview.label": {
- "message": "Chat preview"
- },
- "app.settings.game-options.setting.chat-scale.label": {
- "message": "Chat scale"
- },
- "app.settings.game-options.setting.chat-visibility.label": {
- "message": "Chat visibility"
- },
- "app.settings.game-options.setting.chat-width.label": {
- "message": "Chat width"
- },
- "app.settings.game-options.setting.chunk-fade-time.label": {
- "message": "Chunk fade time"
- },
- "app.settings.game-options.setting.cloud-range.label": {
- "message": "Cloud distance"
- },
- "app.settings.game-options.setting.clouds.label": {
- "message": "Clouds"
- },
- "app.settings.game-options.setting.ctrl-click-right-click.label": {
- "message": "Control-click as right-click"
- },
- "app.settings.game-options.setting.cutout-leaves.label": {
- "message": "Cutout leaves"
- },
- "app.settings.game-options.setting.damage-tilt.label": {
- "message": "Damage tilt"
- },
"app.settings.game-options.setting.dark-splash.description": {
"message": "Change the Mojang Studios loading screen from red to black."
},
- "app.settings.game-options.setting.dark-splash.label": {
- "message": "Monochrome logo"
- },
- "app.settings.game-options.setting.darkness-pulsing.label": {
- "message": "Darkness pulsing"
- },
- "app.settings.game-options.setting.debug-gui-scale.label": {
- "message": "Debug GUI scale"
- },
- "app.settings.game-options.setting.directional-audio.label": {
- "message": "Directional audio"
- },
"app.settings.game-options.setting.discrete-mouse-scroll.description": {
"message": "Treat each mouse-wheel input as a single scroll step."
},
- "app.settings.game-options.setting.discrete-mouse-scroll.label": {
- "message": "Discrete scrolling"
- },
- "app.settings.game-options.setting.entity-distance.label": {
- "message": "Entity distance"
- },
- "app.settings.game-options.setting.entity-shadows.label": {
- "message": "Entity shadows"
- },
- "app.settings.game-options.setting.exclusive-fullscreen.label": {
- "message": "Exclusive fullscreen"
- },
- "app.settings.game-options.setting.extra-telemetry.label": {
- "message": "Optional telemetry"
- },
- "app.settings.game-options.setting.focused-chat-height.label": {
- "message": "Focused chat height"
- },
- "app.settings.game-options.setting.force-unicode-font.label": {
- "message": "Force Unicode font"
- },
- "app.settings.game-options.setting.fov-effects.label": {
- "message": "FOV effects"
- },
- "app.settings.game-options.setting.fov.label": {
- "message": "Field of view"
- },
- "app.settings.game-options.setting.fullscreen.label": {
- "message": "Fullscreen"
- },
- "app.settings.game-options.setting.glint-speed.label": {
- "message": "Glint speed"
- },
- "app.settings.game-options.setting.glint-strength.label": {
- "message": "Glint strength"
- },
- "app.settings.game-options.setting.graphics-backend.label": {
- "message": "Graphics backend"
- },
"app.settings.game-options.setting.graphics.description": {
"message": "Controls visual quality and performance."
},
- "app.settings.game-options.setting.graphics.label": {
- "message": "Graphics"
- },
"app.settings.game-options.setting.gui-scale.description": {
"message": "The size of the game interface and HUD."
},
- "app.settings.game-options.setting.gui-scale.label": {
- "message": "GUI scale"
- },
"app.settings.game-options.setting.hat.description": {
"message": "Show the hat skin layer."
},
- "app.settings.game-options.setting.hat.label": {
- "message": "Hat"
- },
- "app.settings.game-options.setting.held-item-tooltips.label": {
- "message": "Held item tooltips"
- },
- "app.settings.game-options.setting.hide-lightning-flashes.label": {
- "message": "Hide lightning flashes"
- },
- "app.settings.game-options.setting.hide-matched-names.label": {
- "message": "Hide matched names"
- },
- "app.settings.game-options.setting.hide-server-address.label": {
- "message": "Hide server address"
- },
- "app.settings.game-options.setting.hide-splash-texts.label": {
- "message": "Hide splash texts"
- },
- "app.settings.game-options.setting.high-contrast-outline.label": {
- "message": "High contrast block outline"
- },
"app.settings.game-options.setting.high-contrast.description": {
"message": "Enhance the contrast of interface elements."
},
- "app.settings.game-options.setting.high-contrast.label": {
- "message": "High contrast"
- },
- "app.settings.game-options.setting.hostile-volume.label": {
- "message": "Hostile creatures"
- },
- "app.settings.game-options.setting.improved-transparency.label": {
- "message": "Improved transparency"
- },
- "app.settings.game-options.setting.in-game-notifications.label": {
- "message": "In-game notifications"
- },
- "app.settings.game-options.setting.inactivity-framerate-limit.label": {
- "message": "Reduced framerate"
- },
- "app.settings.game-options.setting.invert-horizontal-mouse.label": {
- "message": "Invert horizontal mouse"
- },
"app.settings.game-options.setting.invert-mouse.description": {
"message": "Invert vertical mouse movement."
},
- "app.settings.game-options.setting.invert-mouse.label": {
- "message": "Invert mouse"
- },
"app.settings.game-options.setting.jacket.description": {
"message": "Show the jacket skin layer."
},
- "app.settings.game-options.setting.jacket.label": {
- "message": "Jacket"
- },
- "app.settings.game-options.setting.japanese-glyph-variants.label": {
- "message": "Japanese glyph variants"
- },
- "app.settings.game-options.setting.key-advancements.label": {
- "message": "Advancements"
- },
- "app.settings.game-options.setting.key-attack.label": {
- "message": "Attack"
- },
- "app.settings.game-options.setting.key-back.label": {
- "message": "Move backward"
- },
- "app.settings.game-options.setting.key-chat.label": {
- "message": "Open chat"
- },
- "app.settings.game-options.setting.key-command.label": {
- "message": "Command"
- },
- "app.settings.game-options.setting.key-debug-advanced-tooltips.label": {
- "message": "Show advanced tooltips"
- },
- "app.settings.game-options.setting.key-debug-chunk-borders.label": {
- "message": "Show chunk borders"
- },
- "app.settings.game-options.setting.key-debug-clear-chat.label": {
- "message": "Clear chat"
- },
- "app.settings.game-options.setting.key-debug-copy-location.label": {
- "message": "Copy location"
- },
- "app.settings.game-options.setting.key-debug-copy-recreate-command.label": {
- "message": "Copy recreate command"
- },
- "app.settings.game-options.setting.key-debug-crash.label": {
- "message": "Trigger debug crash"
- },
- "app.settings.game-options.setting.key-debug-dump-dynamic-textures.label": {
- "message": "Dump dynamic textures"
- },
- "app.settings.game-options.setting.key-debug-dump-version.label": {
- "message": "Dump version"
- },
- "app.settings.game-options.setting.key-debug-focus-pause.label": {
- "message": "Pause on lost focus"
- },
- "app.settings.game-options.setting.key-debug-fps-charts.label": {
- "message": "FPS charts"
- },
- "app.settings.game-options.setting.key-debug-hitboxes.label": {
- "message": "Show hitboxes"
- },
- "app.settings.game-options.setting.key-debug-improved-transparency.label": {
- "message": "Improved transparency debug view"
- },
- "app.settings.game-options.setting.key-debug-lightmap-texture.label": {
- "message": "Lightmap texture"
- },
- "app.settings.game-options.setting.key-debug-modifier.label": {
- "message": "Debug modifier"
- },
- "app.settings.game-options.setting.key-debug-network-charts.label": {
- "message": "Network charts"
- },
- "app.settings.game-options.setting.key-debug-options.label": {
- "message": "Debug options"
- },
- "app.settings.game-options.setting.key-debug-overlay.label": {
- "message": "Debug overlay"
- },
- "app.settings.game-options.setting.key-debug-profiling-chart.label": {
- "message": "Profiling chart"
- },
- "app.settings.game-options.setting.key-debug-profiling.label": {
- "message": "Start profiling"
- },
- "app.settings.game-options.setting.key-debug-reload-chunks.label": {
- "message": "Reload chunks"
- },
- "app.settings.game-options.setting.key-debug-reload-resource-packs.label": {
- "message": "Reload resource packs"
- },
- "app.settings.game-options.setting.key-debug-spectate.label": {
- "message": "Spectate entity"
- },
- "app.settings.game-options.setting.key-debug-switch-game-mode.label": {
- "message": "Switch game mode"
- },
- "app.settings.game-options.setting.key-drop.label": {
- "message": "Drop item"
- },
- "app.settings.game-options.setting.key-forward.label": {
- "message": "Move forward"
- },
- "app.settings.game-options.setting.key-friends.label": {
- "message": "Friends"
- },
- "app.settings.game-options.setting.key-fullscreen.label": {
- "message": "Toggle fullscreen"
- },
- "app.settings.game-options.setting.key-hotbar-1.label": {
- "message": "Hotbar 1"
- },
- "app.settings.game-options.setting.key-hotbar-2.label": {
- "message": "Hotbar 2"
- },
- "app.settings.game-options.setting.key-hotbar-3.label": {
- "message": "Hotbar 3"
- },
- "app.settings.game-options.setting.key-hotbar-4.label": {
- "message": "Hotbar 4"
- },
- "app.settings.game-options.setting.key-hotbar-5.label": {
- "message": "Hotbar 5"
- },
- "app.settings.game-options.setting.key-hotbar-6.label": {
- "message": "Hotbar 6"
- },
- "app.settings.game-options.setting.key-hotbar-7.label": {
- "message": "Hotbar 7"
- },
- "app.settings.game-options.setting.key-hotbar-8.label": {
- "message": "Hotbar 8"
- },
- "app.settings.game-options.setting.key-hotbar-9.label": {
- "message": "Hotbar 9"
- },
- "app.settings.game-options.setting.key-inventory.label": {
- "message": "Inventory"
- },
- "app.settings.game-options.setting.key-jump.label": {
- "message": "Jump"
- },
- "app.settings.game-options.setting.key-left.label": {
- "message": "Strafe left"
- },
- "app.settings.game-options.setting.key-load-toolbar.label": {
- "message": "Load toolbar"
- },
- "app.settings.game-options.setting.key-perspective.label": {
- "message": "Change perspective"
- },
- "app.settings.game-options.setting.key-pick-item.label": {
- "message": "Pick block"
- },
- "app.settings.game-options.setting.key-player-list.label": {
- "message": "Player list"
- },
- "app.settings.game-options.setting.key-quick-actions.label": {
- "message": "Quick actions"
- },
- "app.settings.game-options.setting.key-right.label": {
- "message": "Strafe right"
- },
- "app.settings.game-options.setting.key-save-toolbar.label": {
- "message": "Save toolbar"
- },
- "app.settings.game-options.setting.key-screenshot.label": {
- "message": "Screenshot"
- },
- "app.settings.game-options.setting.key-smooth-camera.label": {
- "message": "Toggle cinematic camera"
- },
- "app.settings.game-options.setting.key-sneak.label": {
- "message": "Sneak"
- },
- "app.settings.game-options.setting.key-social-interactions.label": {
- "message": "Social interactions"
- },
- "app.settings.game-options.setting.key-spectator-hotbar.label": {
- "message": "Spectator hotbar"
- },
- "app.settings.game-options.setting.key-spectator-outlines.label": {
- "message": "Highlight spectators"
- },
- "app.settings.game-options.setting.key-sprint.label": {
- "message": "Sprint"
- },
- "app.settings.game-options.setting.key-swap-offhand.label": {
- "message": "Swap offhand"
- },
- "app.settings.game-options.setting.key-toggle-gui.label": {
- "message": "Toggle HUD"
- },
- "app.settings.game-options.setting.key-toggle-spectator-shader.label": {
- "message": "Toggle spectator shader"
- },
- "app.settings.game-options.setting.key-use.label": {
- "message": "Use item"
- },
- "app.settings.game-options.setting.language.label": {
- "message": "Language"
- },
- "app.settings.game-options.setting.left-pants-leg.label": {
- "message": "Left pants leg"
- },
- "app.settings.game-options.setting.left-sleeve.label": {
- "message": "Left sleeve"
- },
- "app.settings.game-options.setting.legacy-framerate-limit.label": {
- "message": "Framerate limit"
- },
- "app.settings.game-options.setting.legacy-view-distance.label": {
- "message": "View distance"
- },
- "app.settings.game-options.setting.mac-fullscreen-menu.label": {
- "message": "Show macOS menu in fullscreen"
- },
"app.settings.game-options.setting.main-hand.description": {
"message": "Choose whether the main hand is left or right."
},
- "app.settings.game-options.setting.main-hand.label": {
- "message": "Main hand"
- },
- "app.settings.game-options.setting.master-volume.label": {
- "message": "Master volume"
- },
- "app.settings.game-options.setting.max-framerate.label": {
- "message": "Maximum framerate"
- },
- "app.settings.game-options.setting.menu-background-blur.label": {
- "message": "Menu background blur"
- },
"app.settings.game-options.setting.mipmap-levels.description": {
"message": "Texture smoothing at a distance."
},
- "app.settings.game-options.setting.mipmap-levels.label": {
- "message": "Mipmap levels"
- },
- "app.settings.game-options.setting.mouse-wheel-sensitivity.label": {
- "message": "Mouse wheel sensitivity"
- },
- "app.settings.game-options.setting.music-frequency.label": {
- "message": "Music frequency"
- },
"app.settings.game-options.setting.music-toast.description": {
"message": "Choose whether music titles appear in the pause menu and as toasts."
},
- "app.settings.game-options.setting.music-toast.label": {
- "message": "Music notification"
- },
- "app.settings.game-options.setting.music-volume.label": {
- "message": "Music"
- },
- "app.settings.game-options.setting.narrator-hotkey.label": {
- "message": "Narrator hotkey"
- },
"app.settings.game-options.setting.narrator.description": {
"message": "Choose what the narrator reads."
},
- "app.settings.game-options.setting.narrator.label": {
- "message": "Narrator"
- },
- "app.settings.game-options.setting.neutral-volume.label": {
- "message": "Friendly creatures"
- },
"app.settings.game-options.setting.notification-time.description": {
"message": "How long toast notifications remain visible."
},
- "app.settings.game-options.setting.notification-time.label": {
- "message": "Notification time"
- },
- "app.settings.game-options.setting.operator-items-tab.label": {
- "message": "Operator items tab"
- },
- "app.settings.game-options.setting.panorama-speed.label": {
- "message": "Panorama speed"
- },
- "app.settings.game-options.setting.particles.label": {
- "message": "Particles"
- },
- "app.settings.game-options.setting.players-volume.label": {
- "message": "Players"
- },
- "app.settings.game-options.setting.prioritize-chunk-updates.label": {
- "message": "Prioritize chunk updates"
- },
- "app.settings.game-options.setting.quit-shortcuts.label": {
- "message": "Quit shortcuts"
- },
- "app.settings.game-options.setting.raw-mouse-input.label": {
- "message": "Raw mouse input"
- },
- "app.settings.game-options.setting.realms-notifications.label": {
- "message": "Realms notifications"
- },
- "app.settings.game-options.setting.record-volume.label": {
- "message": "Jukebox/Note Blocks"
- },
- "app.settings.game-options.setting.reduced-debug-info.label": {
- "message": "Reduced debug information"
- },
- "app.settings.game-options.setting.render-distance.label": {
- "message": "Render distance"
- },
- "app.settings.game-options.setting.right-pants-leg.label": {
- "message": "Right pants leg"
- },
- "app.settings.game-options.setting.right-sleeve.label": {
- "message": "Right sleeve"
- },
- "app.settings.game-options.setting.rotate-with-minecart.label": {
- "message": "Rotate with minecart"
- },
- "app.settings.game-options.setting.save-chat-drafts.label": {
- "message": "Save chat drafts"
- },
- "app.settings.game-options.setting.screen-effects.label": {
- "message": "Screen effects"
- },
- "app.settings.game-options.setting.secure-chat-only.label": {
- "message": "Only show secure chat"
- },
- "app.settings.game-options.setting.sensitivity.label": {
- "message": "Mouse sensitivity"
- },
- "app.settings.game-options.setting.server-textures.label": {
- "message": "Server textures"
- },
- "app.settings.game-options.setting.share-presence.label": {
- "message": "Share presence"
- },
"app.settings.game-options.setting.simulation-distance.description": {
"message": "How far away entities update and blocks and fluids tick."
},
- "app.settings.game-options.setting.simulation-distance.label": {
- "message": "Simulation distance"
- },
- "app.settings.game-options.setting.snooper.label": {
- "message": "Snooper"
- },
- "app.settings.game-options.setting.sprint-window.label": {
- "message": "Sprint window"
- },
"app.settings.game-options.setting.subtitles.description": {
"message": "Show captions for sounds played in the game."
},
- "app.settings.game-options.setting.subtitles.label": {
- "message": "Subtitles"
- },
- "app.settings.game-options.setting.text-background-opacity.label": {
- "message": "Text background opacity"
- },
- "app.settings.game-options.setting.texture-filtering.label": {
- "message": "Texture filtering"
- },
- "app.settings.game-options.setting.toggle-attack.label": {
- "message": "Toggle attack"
- },
"app.settings.game-options.setting.toggle-crouch.description": {
"message": "Press once to remain crouched."
},
- "app.settings.game-options.setting.toggle-crouch.label": {
- "message": "Toggle crouch"
- },
"app.settings.game-options.setting.toggle-sprint.description": {
"message": "Press once to remain sprinting."
},
- "app.settings.game-options.setting.toggle-sprint.label": {
- "message": "Toggle sprint"
- },
- "app.settings.game-options.setting.toggle-use.label": {
- "message": "Toggle use"
- },
- "app.settings.game-options.setting.touchscreen.label": {
- "message": "Touchscreen mode"
- },
- "app.settings.game-options.setting.ui-volume.label": {
- "message": "UI"
- },
- "app.settings.game-options.setting.unfocused-chat-height.label": {
- "message": "Unfocused chat height"
- },
- "app.settings.game-options.setting.use-vbo.label": {
- "message": "Use VBOs"
- },
"app.settings.game-options.setting.view-bobbing.description": {
"message": "Add a bobbing motion to the camera while walking."
},
- "app.settings.game-options.setting.view-bobbing.label": {
- "message": "View bobbing"
- },
- "app.settings.game-options.setting.vignette.label": {
- "message": "Vignette"
- },
- "app.settings.game-options.setting.voice-volume.label": {
- "message": "Voice and speech"
- },
"app.settings.game-options.setting.vsync.description": {
"message": "Limit the frame rate to the display refresh rate to prevent screen tearing."
},
- "app.settings.game-options.setting.vsync.label": {
- "message": "VSync"
- },
- "app.settings.game-options.setting.weather-radius.label": {
- "message": "Weather radius"
- },
- "app.settings.game-options.setting.weather-volume.label": {
- "message": "Weather"
- },
"app.settings.game-options.validation.changed-since-opened": {
"message": "This setting changed elsewhere. Check it and try again."
},
@@ -2943,7 +2250,7 @@
"message": "Sync resource packs"
},
"app.settings.synced-options.resource-packs.description": {
- "message": "Use the same resource packs across your instances"
+ "message": "Use the same resource packs across your instances."
},
"app.settings.synced-options.resource-packs.none-synced-yet": {
"message": "You haven't synced any resource packs yet"
@@ -4277,6 +3584,9 @@
"instance.worlds.shortcut-creation-failed": {
"message": "Failed to create shortcut"
},
+ "instance.worlds.synced_server": {
+ "message": "Synced across instances"
+ },
"instance.worlds.view_instance": {
"message": "View instance"
},
diff --git a/apps/app-frontend/src/pages/Screenshots.vue b/apps/app-frontend/src/pages/Screenshots.vue
index 650acb3faa..00835b8f65 100644
--- a/apps/app-frontend/src/pages/Screenshots.vue
+++ b/apps/app-frontend/src/pages/Screenshots.vue
@@ -1,5 +1,5 @@
diff --git a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue
index a660b09eaf..c94f3b01a3 100644
--- a/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue
+++ b/apps/app-frontend/src/pages/instance/components/settings-modal/java-settings.vue
@@ -228,6 +228,8 @@ const messages = defineMessages({
:step="64"
:snap-points="snapPoints"
:snap-range="512"
+ min-label="512 MB"
+ :max-label="`${Number((maxMemory / 1024).toFixed(1))} GB`"
unit="MB"
/>
diff --git a/apps/app/src/api/instance.rs b/apps/app/src/api/instance.rs
index ef1e4165b9..f49a365a8d 100644
--- a/apps/app/src/api/instance.rs
+++ b/apps/app/src/api/instance.rs
@@ -69,6 +69,7 @@ pub fn init() -> tauri::plugin::TauriPlugin {
instance_set_global_synced_option,
instance_list_game_options_sync_sources,
instance_get_synced_game_options_config,
+ instance_get_game_setting_locale_labels,
instance_preview_synced_game_option_changes,
instance_save_synced_game_option_changes,
instance_get_local_game_options_config,
@@ -887,6 +888,13 @@ pub async fn instance_get_synced_game_options_config()
Ok(theseus::instance::get_synced_game_options_config().await?)
}
+#[tauri::command]
+pub async fn instance_get_game_setting_locale_labels(
+ instance_id: Option, locale: String, option_ids: Vec, refresh_sources: bool,
+) -> Result {
+ Ok(theseus::instance::get_game_setting_locale_labels(instance_id.as_deref(), &locale, option_ids, refresh_sources).await?)
+}
+
#[tauri::command]
pub async fn instance_preview_synced_game_option_changes(
request: theseus::instance::UpdateGameSettingsRequest,
diff --git a/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json b/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json
new file mode 100644
index 0000000000..8a7b1c4bd1
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "UPDATE game_option_locale_origins SET origin_json = ?\n\t\tWHERE scope = ? AND option_id = ? AND origin_json IS NULL",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 3
+ },
+ "nullable": []
+ },
+ "hash": "5026df81edab9891eb69be610f2d5c5b3ed5c0d5e515a956e1ed4a722408748b"
+}
diff --git a/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json b/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json
new file mode 100644
index 0000000000..df0d2dd0bb
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961.json
@@ -0,0 +1,56 @@
+{
+ "db_name": "SQLite",
+ "query": "SELECT scope, option_id, source_instance_id, source_game_version,\n\t\tbackfilled AS \"backfilled!: bool\", observation_json, origin_json\n\t\tFROM game_option_locale_origins\n\t\tWHERE (? IS NULL OR scope = ?)\n\t\tORDER BY scope, option_id",
+ "describe": {
+ "columns": [
+ {
+ "name": "scope",
+ "ordinal": 0,
+ "type_info": "Text"
+ },
+ {
+ "name": "option_id",
+ "ordinal": 1,
+ "type_info": "Text"
+ },
+ {
+ "name": "source_instance_id",
+ "ordinal": 2,
+ "type_info": "Text"
+ },
+ {
+ "name": "source_game_version",
+ "ordinal": 3,
+ "type_info": "Text"
+ },
+ {
+ "name": "backfilled!: bool",
+ "ordinal": 4,
+ "type_info": "Integer"
+ },
+ {
+ "name": "observation_json",
+ "ordinal": 5,
+ "type_info": "Text"
+ },
+ {
+ "name": "origin_json",
+ "ordinal": 6,
+ "type_info": "Text"
+ }
+ ],
+ "parameters": {
+ "Right": 2
+ },
+ "nullable": [
+ false,
+ false,
+ true,
+ true,
+ false,
+ true,
+ true
+ ]
+ },
+ "hash": "c4a7ef44cae3241d2afb444ea7819b83e3a6663fbd52b0198a0f2a5cd97c3961"
+}
diff --git a/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json b/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json
new file mode 100644
index 0000000000..659da5a9fa
--- /dev/null
+++ b/packages/app-lib/.sqlx/query-d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827.json
@@ -0,0 +1,12 @@
+{
+ "db_name": "SQLite",
+ "query": "INSERT INTO game_option_locale_origins\n\t\t(scope, option_id, source_instance_id, source_game_version, observation_json)\n\t\tSELECT ?, ?, ?, ?, ?\n\t\tWHERE ? <> '' OR EXISTS (SELECT 1 FROM synced_game_option_values WHERE option_id = ?)\n\t\tON CONFLICT(scope, option_id) DO UPDATE SET observation_json = excluded.observation_json\n\t\tWHERE game_option_locale_origins.observation_json IS NULL\n\t\tAND game_option_locale_origins.origin_json IS NULL\n\t\tAND game_option_locale_origins.backfilled = 0\n\t\tAND game_option_locale_origins.source_instance_id = excluded.source_instance_id\n\t\tAND game_option_locale_origins.source_game_version = excluded.source_game_version",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Right": 7
+ },
+ "nullable": []
+ },
+ "hash": "d2c0630471ca3ecabcd9482cec25edb59da2487c0cc099bd965e16424f47d827"
+}
diff --git a/packages/app-lib/migrations/20260908120000_game-option-locales.sql b/packages/app-lib/migrations/20260908120000_game-option-locales.sql
new file mode 100644
index 0000000000..8601b9e8ae
--- /dev/null
+++ b/packages/app-lib/migrations/20260908120000_game-option-locales.sql
@@ -0,0 +1,24 @@
+CREATE TABLE game_option_locale_origins (
+ scope TEXT NOT NULL,
+ option_id TEXT NOT NULL,
+ source_instance_id TEXT,
+ source_game_version TEXT,
+ backfilled INTEGER NOT NULL DEFAULT 0,
+ observation_json TEXT,
+ origin_json TEXT,
+ PRIMARY KEY (scope, option_id)
+);
+
+INSERT INTO game_option_locale_origins
+ (scope, option_id, source_instance_id, source_game_version, backfilled)
+SELECT '', option_id, source_instance_id, source_game_version, 1
+FROM synced_game_option_values;
+
+CREATE TRIGGER record_game_option_locale_origin
+AFTER INSERT ON synced_game_option_values
+BEGIN
+ INSERT INTO game_option_locale_origins
+ (scope, option_id, source_instance_id, source_game_version)
+ VALUES ('', NEW.option_id, NEW.source_instance_id, NEW.source_game_version)
+ ON CONFLICT(scope, option_id) DO NOTHING;
+END;
diff --git a/packages/app-lib/src/api/instance.rs b/packages/app-lib/src/api/instance.rs
index e9ae3da69d..3ae711b885 100644
--- a/packages/app-lib/src/api/instance.rs
+++ b/packages/app-lib/src/api/instance.rs
@@ -93,6 +93,7 @@ pub use self::shared::{
unlink_shared_instance, unpublish_shared_instance, update_shared_instance,
};
pub use self::synced_options::game_options::{
+ GameSettingLocaleLabels, get_game_setting_locale_labels,
CanonicalValue as GameOptionCanonicalValue, EditableGameSetting,
GameOptionCompatibility, GameOptionCompatibilityBucket,
GameOptionCompatibilityReason, GameOptionCompatibilityStatus,
@@ -113,6 +114,7 @@ pub use self::synced_options::game_options::{
sync_before_launch as sync_game_options_before_launch,
};
pub(crate) use self::synced_options::game_options::{
+ GameLocaleIndexer, queue_game_locale_index, start_game_locale_indexer,
shared_fullscreen_value, sync_all_participating_instances,
update_shared_fullscreen_from_app,
};
diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs b/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs
index 0bbedd5464..e73415be40 100644
--- a/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs
+++ b/packages/app-lib/src/api/instance/synced_options/game_options/catalog/version_changes.rs
@@ -69,20 +69,12 @@ pub(in crate::api::instance) const AMBIENT_OCCLUSION_KEYS: &[VersionedKey] = &[
},
];
-pub(in crate::api::instance) const FOV_KEYS: &[VersionedKey] = &[
- VersionedKey {
- key: "fov",
- since: "1.0",
- until: "1.18.2",
- mapping: GameOptionMappingKind::Legacy,
- },
- VersionedKey {
- key: "fov",
- since: "1.19",
- until: "26.3",
- mapping: GameOptionMappingKind::Direct,
- },
-];
+pub(in crate::api::instance) const FOV_KEYS: &[VersionedKey] = &[VersionedKey {
+ key: "fov",
+ since: "1.0",
+ until: "26.3",
+ mapping: GameOptionMappingKind::Direct,
+}];
pub(in crate::api::instance) const CLOUD_KEYS: &[VersionedKey] = &[
VersionedKey {
@@ -551,14 +543,8 @@ pub(in crate::api::instance) fn encode_value(
(ValueEncoding::Fov, CanonicalValue::Integer(value))
if (30..=110).contains(value) =>
{
- if release_version(game_version)
- .is_some_and(|version| version >= (1, 19, 0))
- {
- Some(value.to_string())
- } else {
- let normalized = (*value as f64 - 70.0) / 40.0;
- Some(format_decimal(normalized))
- }
+ let normalized = (*value as f64 - 70.0) / 40.0;
+ Some(format_decimal(normalized))
}
(ValueEncoding::GuiScale, CanonicalValue::Integer(value))
if (0..=8).contains(value)
@@ -914,14 +900,9 @@ pub(in crate::api::instance) fn physical_representation_supported_for_target(
};
}
if matches!(definition.encoding, ValueEncoding::Fov) {
- return if target_version >= (1, 19, 0) {
- raw.parse::()
- .is_ok_and(|value| (30..=110).contains(&value))
- } else {
- raw.parse::().is_ok_and(|value| {
- value.is_finite() && (-1.0..=1.0).contains(&value)
- })
- };
+ return raw.parse::().is_ok_and(|value| {
+ value.is_finite() && (-1.0..=1.0).contains(&value)
+ });
}
if matches!(definition.encoding, ValueEncoding::ChatPreview) {
return if target_version == (1, 19, 0) {
diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs
new file mode 100644
index 0000000000..341e0604a6
--- /dev/null
+++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/archive.rs
@@ -0,0 +1,187 @@
+use super::super::options_file::{input_error, sha1_bytes};
+use serde::{Deserialize, Serialize};
+use std::collections::{BTreeMap, BTreeSet};
+use std::io::{Cursor, Read, Seek};
+use std::path::Path;
+use zip::ZipArchive;
+
+pub(super) const MAX_LANGUAGE_BYTES: usize = 4 * 1024 * 1024;
+const MAX_ARCHIVE_ENTRIES: usize = 100_000;
+const MAX_NESTED_BYTES: usize = 64 * 1024 * 1024;
+const MAX_TOTAL_BYTES: usize = 128 * 1024 * 1024;
+const MAX_NESTED_DEPTH: usize = 4;
+pub(super) type Translations = BTreeMap;
+
+#[derive(Default, Serialize, Deserialize)]
+pub(super) struct ArchiveIndex {
+ #[serde(default)]
+ pub version: u32,
+ pub bundles: Vec,
+}
+
+#[derive(Default, Serialize, Deserialize)]
+pub(super) struct LanguageBundle {
+ pub nested_path: String,
+ pub locales: BTreeMap,
+ #[serde(default)]
+ pub deprecated: DeprecatedTranslations,
+}
+
+#[derive(Default, Serialize, Deserialize)]
+pub(super) struct DeprecatedTranslations {
+ #[serde(default)]
+ removed: Vec,
+ #[serde(default)]
+ renamed: BTreeMap,
+}
+
+impl DeprecatedTranslations {
+ pub fn apply(&self, translations: &mut Translations) {
+ for key in &self.removed {
+ translations.remove(key);
+ }
+ for (from, to) in &self.renamed {
+ if let Some(value) = translations.remove(from) {
+ translations.insert(to.clone(), value);
+ } else {
+ translations.remove(to);
+ }
+ }
+ }
+}
+
+pub(super) fn parse_language(bytes: &[u8], legacy: bool) -> crate::Result {
+ if bytes.len() > MAX_LANGUAGE_BYTES {
+ return Err(input_error("Minecraft language file exceeds size limit"));
+ }
+ let values: Translations = if legacy {
+ String::from_utf8_lossy(bytes)
+ .lines()
+ .filter(|line| !line.starts_with('#'))
+ .filter_map(|line| line.split_once('='))
+ .map(|(key, value)| (key.to_owned(), value.to_owned()))
+ .collect()
+ } else {
+ serde_json::from_slice::>(bytes)?
+ .into_iter()
+ .filter_map(|(key, value)| value.as_str().map(|s| (key, s.to_owned())))
+ .collect()
+ };
+ Ok(values.into_iter().filter(|(key, value)| {
+ !key.is_empty() && key.len() <= 1024 && value.len() <= 32 * 1024
+ }).collect())
+}
+
+pub(super) fn inspect(path: &Path, expected_hash: &str) -> crate::Result {
+ let mut file = std::fs::File::open(path).map_err(crate::util::io::IOError::from)?;
+ let mut hasher = sha1_smol::Sha1::new();
+ let mut buffer = [0; 64 * 1024];
+ loop {
+ let count = file.read(&mut buffer).map_err(crate::util::io::IOError::from)?;
+ if count == 0 { break; }
+ hasher.update(&buffer[..count]);
+ }
+ if hasher.digest().to_string() != expected_hash {
+ return Err(input_error("Locale source changed before it could be indexed"));
+ }
+ file.rewind().map_err(crate::util::io::IOError::from)?;
+ let mut index = ArchiveIndex { version: 2, ..Default::default() };
+ let mut budget = MAX_TOTAL_BYTES;
+ let mut entries = MAX_ARCHIVE_ENTRIES;
+ inspect_archive(file, "", 0, &mut budget, &mut entries, &mut index)?;
+ Ok(index)
+}
+
+fn read_entry(
+ archive: &mut ZipArchive, name: &str, limit: usize, budget: &mut usize,
+) -> crate::Result> {
+ let mut entry = archive.by_name(name).map_err(|e| input_error(e.to_string()))?;
+ let limit = limit.min(*budget);
+ if entry.size() > limit as u64 {
+ return Err(input_error("Locale archive entry exceeds size limit"));
+ }
+ let mut bytes = Vec::new();
+ entry.by_ref().take(limit as u64 + 1).read_to_end(&mut bytes)
+ .map_err(crate::util::io::IOError::from)?;
+ if bytes.len() > limit {
+ return Err(input_error("Locale archive entry exceeds size limit"));
+ }
+ *budget -= bytes.len();
+ Ok(bytes)
+}
+
+fn inspect_archive(
+ reader: R, nested_path: &str, depth: usize, budget: &mut usize,
+ entries: &mut usize, index: &mut ArchiveIndex,
+) -> crate::Result<()> {
+ let mut archive = ZipArchive::new(reader).map_err(|e| input_error(e.to_string()))?;
+ if archive.len() > *entries {
+ return Err(input_error("Too many entries in locale archive"));
+ }
+ *entries -= archive.len();
+ let names: BTreeSet = archive.file_names().map(str::to_owned).collect();
+ let mut bundle = LanguageBundle { nested_path: nested_path.to_owned(), ..Default::default() };
+ for name in &names {
+ let parts: Vec<_> = name.split('/').collect();
+ let (namespace, filename) = match parts.as_slice() {
+ ["assets", namespace, "lang", filename] => (*namespace, *filename),
+ ["lang", filename] => ("minecraft", *filename),
+ _ => continue,
+ };
+ if filename == "deprecated.json" {
+ if namespace == "minecraft" {
+ let bytes = read_entry(&mut archive, name, MAX_LANGUAGE_BYTES, budget)?;
+ bundle.deprecated = serde_json::from_slice(&bytes)?;
+ }
+ continue;
+ }
+ let legacy = filename.ends_with(".lang");
+ let Some(locale) = filename.strip_suffix(if legacy { ".lang" } else { ".json" }) else { continue; };
+ if !valid_locale(locale) { continue; }
+ let bytes = read_entry(&mut archive, name, MAX_LANGUAGE_BYTES, budget)?;
+ match parse_language(&bytes, legacy) {
+ Ok(values) => bundle.locales.entry(locale.to_ascii_lowercase()).or_default().extend(values),
+ Err(error) => tracing::debug!(%name, %error, "Skipping malformed mod language file"),
+ }
+ }
+ index.bundles.push(bundle);
+ let mut nested = BTreeSet::new();
+ for manifest in ["fabric.mod.json", "quilt.mod.json", "META-INF/jarjar/metadata.json"] {
+ if !names.contains(manifest) { continue; }
+ let bytes = read_entry(&mut archive, manifest, MAX_LANGUAGE_BYTES, budget)?;
+ let Ok(value) = serde_json::from_slice::(&bytes) else { continue; };
+ let jars = value.get("jars").or_else(|| value.pointer("/quilt_loader/jars"));
+ for jar in jars.and_then(|v| v.as_array()).into_iter().flatten() {
+ if let Some(path) = jar.as_str().or_else(|| jar.get("file").and_then(|v| v.as_str()))
+ .or_else(|| jar.get("path").and_then(|v| v.as_str())) {
+ nested.insert(path.to_owned());
+ }
+ }
+ }
+ if !nested.is_empty() && depth >= MAX_NESTED_DEPTH {
+ return Err(input_error("Locale archive nesting exceeds limit"));
+ }
+ for name in nested {
+ if !names.contains(&name) || !name.ends_with(".jar") { continue; }
+ let bytes = read_entry(&mut archive, &name, MAX_NESTED_BYTES, budget)?;
+ let path = if nested_path.is_empty() { name } else { format!("{nested_path}!{name}") };
+ inspect_archive(Cursor::new(bytes), &path, depth + 1, budget, entries, index)?;
+ }
+ Ok(())
+}
+
+pub(super) fn valid_locale(locale: &str) -> bool {
+ !locale.is_empty() && locale.len() <= 32
+ && locale.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_')
+}
+
+pub(super) fn valid_hash(hash: &str) -> bool {
+ hash.len() == 40 && hash.bytes().all(|c| c.is_ascii_hexdigit())
+}
+
+pub(super) fn checked_bytes(bytes: Vec, hash: &str) -> crate::Result> {
+ if sha1_bytes(&bytes) != hash {
+ return Err(input_error("Minecraft locale resource checksum mismatch"));
+ }
+ Ok(bytes)
+}
diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs
new file mode 100644
index 0000000000..95f815b608
--- /dev/null
+++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/catalog.rs
@@ -0,0 +1,138 @@
+/// Translation keys mapped to settings.
+pub(super) fn translation_keys(raw: &str, vanilla: bool) -> Vec {
+ if let Some(key) = raw.strip_prefix("key_") { return vec![key.to_owned()]; }
+ if !vanilla { return Vec::new(); }
+ if let Some(key) = raw.strip_prefix("soundCategory_") { return vec![format!("soundCategory.{key}")]; }
+ if let Some(key) = raw.strip_prefix("modelPart_") { return vec![format!("options.modelPart.{key}")]; }
+ let aliases: &[&str] = match raw {
+ "lang" => &["options.language"],
+ "mouseSensitivity" => &["options.sensitivity"],
+ "enableVsync" => &["options.vsync"],
+ "bobView" => &["options.viewBobbing"],
+ "maxFps" | "fpsLimit" => &["options.framerateLimit"],
+ "fancyGraphics" | "graphicsMode" => &["options.graphics"],
+ "graphicsPreset" => &["options.graphics.preset"],
+ "renderClouds" => &["options.renderClouds", "options.clouds"],
+ "cloudRange" => &["options.renderCloudsDistance"],
+ "viewDistance" => &["options.renderDistance"],
+ "invertYMouse" => &["options.invertMouseY", "options.invertMouse"],
+ "invertXMouse" => &["options.invertMouseX"],
+ "toggleCrouch" => &["key.sneak"],
+ "toggleSprint" => &["key.sprint"],
+ "toggleAttack" => &["key.attack"],
+ "toggleUse" => &["key.use"],
+ "chatVisibility" => &["options.chat.visibility"],
+ "chatColors" => &["options.chat.color"],
+ "chatLinks" => &["options.chat.links"],
+ "chatLinksPrompt" => &["options.chat.links.prompt"],
+ "chatOpacity" => &["options.chat.opacity"],
+ "chatScale" => &["options.chat.scale"],
+ "chatWidth" => &["options.chat.width"],
+ "chatHeightFocused" => &["options.chat.height.focused"],
+ "chatHeightUnfocused" => &["options.chat.height.unfocused"],
+ "chatLineSpacing" => &["options.chat.line_spacing"],
+ "chatDelay" => &["options.chat.delay_instant"],
+ "autoSuggestions" => &["options.autoSuggestCommands"],
+ "textBackgroundOpacity" => &["options.accessibility.text_background_opacity"],
+ "backgroundForChatOnly" => &["options.accessibility.text_background"],
+ "darkMojangStudiosBackground" => &["options.darkMojangStudiosBackgroundColor"],
+ "highContrast" => &["options.accessibility.high_contrast"],
+ "highContrastBlockOutline" => &["options.accessibility.high_contrast_block_outline"],
+ "menuBackgroundBlurriness" => &["options.accessibility.menu_background_blurriness"],
+ "notificationDisplayTime" => &["options.notifications.display_time"],
+ "panoramaScrollSpeed" => &["options.accessibility.panorama_speed"],
+ "narratorHotkey" => &["options.accessibility.narrator_hotkey"],
+ "showAutosaveIndicator" => &["options.autosaveIndicator"],
+ "soundDevice" => &["options.audioDevice"],
+ "musicFrequency" => &["options.music_frequency"],
+ "music" => &["soundCategory.music", "options.music"],
+ "sound" => &["soundCategory.master", "options.sound"],
+ "realmsNotifications" => &["options.realmsNotifications.button"],
+ "telemetryOptInExtra" => &["options.telemetry.button"],
+ "saveChatDrafts" => &["options.chat.drafts"],
+ "chunkSectionFadeInTime" => &["options.chunkFade"],
+ "maxAnisotropyBit" => &["options.maxAnisotropy"],
+ "preferredGraphicsBackend" => &["options.graphicsApi"],
+ _ => &[],
+ };
+ aliases.iter().map(|key| (*key).to_owned())
+ .chain(std::iter::once(format!("options.{raw}"))).collect()
+}
+
+pub(super) fn choice_keys(option_id: &str) -> &'static [(&'static str, &'static str)] {
+ match option_id {
+ "graphics" => &[("fast", "options.graphics.fast"), ("fancy", "options.graphics.fancy"), ("fabulous", "options.graphics.fabulous"), ("custom", "options.graphics.custom")],
+ "clouds" => &[("false", "options.off"), ("fast", "options.clouds.fast"), ("true", "options.clouds.fancy")],
+ "particles" => &[("0", "options.particles.all"), ("1", "options.particles.decreased"), ("2", "options.particles.minimal")],
+ "chat_visibility" => &[("0", "options.chat.visibility.full"), ("1", "options.chat.visibility.system"), ("2", "options.chat.visibility.hidden")],
+ "main_hand" => &[("left", "options.mainHand.left"), ("right", "options.mainHand.right")],
+ "narrator" => &[
+ ("0", "options.narrator.off"),
+ ("1", "options.narrator.all"),
+ ("2", "options.narrator.chat"),
+ ("3", "options.narrator.system"),
+ ],
+ "ambient_occlusion" => &[
+ ("off", "options.ao.off"),
+ ("on", "options.on"),
+ ("minimum", "options.ao.min"),
+ ("maximum", "options.ao.max"),
+ ],
+ "music_toast" => &[
+ ("never", "options.musicToast.never"),
+ ("pause", "options.musicToast.pauseMenu"),
+ ("pause_and_toast", "options.musicToast.pauseMenuAndToast"),
+ ],
+ "legacy_view_distance" => &[
+ ("0", "options.renderDistance.far"),
+ ("1", "options.renderDistance.normal"),
+ ("2", "options.renderDistance.short"),
+ ("3", "options.renderDistance.tiny"),
+ ],
+ "legacy_framerate_limit" => &[
+ ("0", "options.framerateLimit.max"),
+ ("1", "options.framerateLimit.balanced"),
+ ("2", "options.framerateLimit.powersaver"),
+ ],
+ "inactivity_framerate_limit" => &[
+ ("afk", "options.inactivityFpsLimit.afk"),
+ ("minimized", "options.inactivityFpsLimit.minimized"),
+ ],
+ "prioritize_chunk_updates" => &[
+ ("0", "options.prioritizeChunkUpdates.none"),
+ ("1", "options.prioritizeChunkUpdates.byPlayer"),
+ ("2", "options.prioritizeChunkUpdates.nearby"),
+ ],
+ "attack_indicator" => &[
+ ("0", "options.off"),
+ ("1", "options.attack.crosshair"),
+ ("2", "options.attack.hotbar"),
+ ],
+ "chat_preview" => &[
+ ("0", "options.off"),
+ ("1", "options.chatPreview.live"),
+ ("2", "options.chatPreview.confirm"),
+ ],
+ "music_frequency" => &[
+ ("CONSTANT", "options.music_frequency.constant"),
+ ("DEFAULT", "options.music_frequency.default"),
+ ("FREQUENT", "options.music_frequency.frequent"),
+ ],
+ "share_presence" => &[
+ ("all", "options.sharePresence.all"),
+ ("limited", "options.sharePresence.limited"),
+ ("none", "options.sharePresence.none"),
+ ],
+ "graphics_backend" => &[
+ ("default", "options.graphicsApi.default"),
+ ("opengl", "options.graphicsApi.opengl"),
+ ("vulkan", "options.graphicsApi.vulkan"),
+ ],
+ "texture_filtering" => &[
+ ("0", "options.textureFiltering.none"),
+ ("1", "options.textureFiltering.rgss"),
+ ("2", "options.textureFiltering.anisotropic"),
+ ],
+ _ => &[],
+ }
+}
diff --git a/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs
new file mode 100644
index 0000000000..5d4e7011ee
--- /dev/null
+++ b/packages/app-lib/src/api/instance/synced_options/game_options/locales/mod.rs
@@ -0,0 +1,301 @@
+//! Caches game translations and remembers which JAR supplied each setting.
+
+mod archive;
+mod catalog;
+mod sources;
+mod storage;
+
+use super::catalog::setting_by_file_key;
+use super::options_file::{GameOptionsDocument, input_error, options_path, read_document};
+use crate::state::{InstanceMetadata, State};
+use archive::{ArchiveIndex, Translations};
+use serde::{Deserialize, Serialize};
+use sources::{ArchiveSource, AssetIndexSource, Snapshot};
+use std::collections::{BTreeMap, HashMap};
+use std::sync::{Arc, atomic::{AtomicBool, Ordering}};
+use tokio::sync::Notify;
+
+#[derive(Default)]
+pub(crate) struct GameLocaleIndexer {
+ notify: Notify,
+ started: AtomicBool,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct Observation {
+ snapshot: String,
+ raw_key: String,
+}
+
+#[derive(Clone, Serialize, Deserialize)]
+struct Origin {
+ instance_id: String,
+ game_version: String,
+ game_jar_hash: String,
+ asset_index: Option,
+ archive: ArchiveSource,
+ nested_path: String,
+ translation_key: String,
+ choices: BTreeMap,
+}
+
+#[derive(Default, Serialize)]
+pub struct GameSettingLocaleLabels {
+ pub settings: BTreeMap,
+}
+
+#[derive(Serialize)]
+pub struct GameSettingLocaleLabel {
+ pub label: String,
+ pub choices: BTreeMap,
+}
+
+struct Candidate {
+ snapshot_id: String,
+ snapshot: Snapshot,
+ keys: BTreeMap,
+}
+
+pub(crate) fn queue_game_locale_index() {
+ if let Some(state) = State::get_if_initialized() {
+ state.game_locale_indexer.notify.notify_one();
+ }
+}
+
+pub(crate) fn start_game_locale_indexer(state: Arc) {
+ if state.game_locale_indexer.started.swap(true, Ordering::AcqRel) { return; }
+ state.game_locale_indexer.notify.notify_one();
+ tokio::spawn(async move {
+ loop {
+ state.game_locale_indexer.notify.notified().await;
+ tokio::time::sleep(std::time::Duration::from_millis(300)).await;
+ if let Err(error) = index_installed_sources(&state).await {
+ tracing::warn!(%error, "Could not index game-setting translations");
+ }
+ #[cfg(feature = "tauri")]
+ {
+ use tauri::Emitter;
+ let _ = crate::EventState::get().app.emit("game-option-locales-updated", ());
+ }
+ }
+ });
+}
+
+fn document_keys(document: &GameOptionsDocument) -> BTreeMap {
+ document.effective_entries().keys().map(|key| {
+ let id = setting_by_file_key(key).map(|s| s.id.to_owned())
+ .unwrap_or_else(|| format!("external:{key}"));
+ (id, (*key).to_owned())
+ }).filter(|(id, key)| !catalog::translation_keys(key, !id.starts_with("external:")).is_empty()).collect()
+}
+
+/// Records Minecraft and mod JAR hashes before saving new options.
+pub(super) async fn capture_observation(metadata: &InstanceMetadata, state: &State) -> Option {
+ match sources::snapshot_instance(metadata, state).await {
+ Ok(id) => Some(id),
+ Err(error) => {
+ tracing::debug!(%error, "Game-setting locale source is not available yet");
+ None
+ }
+ }
+}
+
+pub(super) async fn record_observations(
+ tx: &mut sqlx::Transaction<'_, sqlx::Sqlite>, metadata: &InstanceMetadata,
+ document: &GameOptionsDocument, snapshot: Option<&str>,
+) {
+ let Some(snapshot) = snapshot else { return; };
+ for (id, raw_key) in document_keys(document) {
+ let observation = Observation { snapshot: snapshot.to_owned(), raw_key };
+ if let Err(error) = storage::observe(tx, "", &id, &metadata.instance.id,
+ &metadata.applied_content_set.game_version, &observation).await {
+ tracing::warn!(%error, "Could not record game-setting translation provenance");
+ }
+ }
+}
+
+async fn cached_archive(
+ state: &State, source: &ArchiveSource, cache: &mut HashMap>,
+) -> crate::Result> {
+ if let Some(index) = cache.get(&source.hash) { return Ok(index.clone()); }
+ let index = Arc::new(sources::archive_index(state, source).await?);
+ if cache.len() >= 8 { cache.clear(); }
+ cache.insert(source.hash.clone(), index.clone());
+ Ok(index)
+}
+
+async fn index_installed_sources(state: &State) -> crate::Result<()> {
+ let mut instances = crate::state::list_instances(&state.pool).await?;
+ instances.sort_by(|a, b| a.instance.id.cmp(&b.instance.id));
+ let mut candidates = Vec::new();
+ let mut archives = HashMap::new();
+ for metadata in instances {
+ if super::super::sync_files_are_protected(&metadata) { continue; }
+ let Some(snapshot_id) = capture_observation(&metadata, state).await else { continue; };
+ let snapshot = sources::load_snapshot(state, &snapshot_id).await?;
+ for source in std::iter::once(&snapshot.game_jar).chain(&snapshot.mods) {
+ if let Err(error) = cached_archive(state, source, &mut archives).await {
+ tracing::debug!(hash = source.hash, %error, "Could not index locale archive");
+ }
+ }
+ if let Some(index) = &snapshot.asset_index {
+ let _ = sources::language_assets(state, index).await;
+ }
+ let Ok((document, _)) = read_document(&options_path(&metadata, state)).await else { continue; };
+ let keys = document_keys(&document);
+ let mut tx = state.pool.begin().await?;
+ for (id, key) in &keys {
+ let observation = Observation { snapshot: snapshot_id.clone(), raw_key: key.clone() };
+ storage::observe(&mut tx, &metadata.instance.id, id, &metadata.instance.id,
+ &snapshot.game_version, &observation).await?;
+ }
+ tx.commit().await?;
+ candidates.push(Candidate { snapshot_id, snapshot, keys });
+ }
+ for row in storage::load(&state.pool, None).await? {
+ if row.origin.is_some() { continue; }
+ let mut observations = Vec::new();
+ if let Some(observation) = &row.observation {
+ observations.push(observation.clone());
+ } else {
+ if row.backfilled
+ && let Some(version) = &row.source_game_version
+ && let Some(definition) = super::catalog::setting_by_id(&row.option_id)
+ && let Ok(Some(snapshot)) = sources::historical_snapshot(
+ state, row.source_instance_id.as_deref().unwrap_or(""), version,
+ ).await {
+ observations.extend(definition.keys.iter().map(|key| Observation {
+ snapshot: snapshot.clone(), raw_key: (*key).to_owned(),
+ }));
+ }
+ let mut matching: Vec<_> = candidates.iter().filter(|c| {
+ (row.scope.is_empty() || row.scope == c.snapshot.instance_id)
+ && c.keys.contains_key(&row.option_id)
+ && (row.backfilled || row.source_instance_id.as_ref().is_none_or(|id| *id == c.snapshot.instance_id)
+ && row.source_game_version.as_ref().is_none_or(|v| *v == c.snapshot.game_version))
+ }).collect();
+ matching.sort_by_key(|c| (
+ row.source_instance_id.as_deref() != Some(c.snapshot.instance_id.as_str()),
+ row.source_game_version.as_deref() != Some(c.snapshot.game_version.as_str()),
+ c.snapshot.instance_id.clone(),
+ ));
+ observations.extend(matching.into_iter().map(|c| Observation {
+ snapshot: c.snapshot_id.clone(), raw_key: c.keys[&row.option_id].clone(),
+ }));
+ }
+ for observation in observations {
+ match resolve_origin(state, &row.option_id, &observation, &mut archives).await {
+ Ok(Some(origin)) => { storage::pin(&state.pool, &row, &origin).await?; break; }
+ Ok(None) => {}
+ Err(error) => tracing::debug!(%error, option_id = row.option_id, "Translation origin remains unresolved"),
+ }
+ }
+ }
+ Ok(())
+}
+
+async fn resolve_origin(
+ state: &State, option_id: &str, observation: &Observation,
+ archives: &mut HashMap>,
+) -> crate::Result