ENH - add configurator field descriptions

This commit is contained in:
Leon.Schmidt
2026-08-21 03:05:44 +02:00
parent 7bc4eb6993
commit 87fa234c49
7 changed files with 365 additions and 30 deletions
@@ -9,6 +9,7 @@ import {
import { computed, ref } from 'vue'
import type { AdminConfiguratorStructure } from '@/types/admin'
import type { AdminConfiguratorDescribe } from '@/utils/adminConfiguratorDescription'
export type AdminConfigEditorLabels = {
addField: string
@@ -19,6 +20,7 @@ export type AdminConfigEditorLabels = {
convertToTable: string
emptyList: string
emptyTable: string
entry: string
keyPlaceholder: string
list: string
remove: string
@@ -39,12 +41,14 @@ const props = withDefaults(
defineProps<{
ariaLabel?: string
depth?: number
describe: AdminConfiguratorDescribe
disabled?: boolean
labels: AdminConfigEditorLabels
modelValue: unknown
path?: string
structure?: AdminConfiguratorStructure
}>(),
{ ariaLabel: '', depth: 0, disabled: false },
{ ariaLabel: '', depth: 0, disabled: false, path: '' },
)
const emit = defineEmits<{ 'update:modelValue': [value: unknown] }>()
@@ -211,6 +215,18 @@ function toggleStructuredEntry(entry: string): void {
expandedEntry.value = expandedEntry.value === entry ? null : entry
}
function tableEntryPath(key: string): string {
return props.path ? `${props.path}.${key}` : key
}
function listEntryPath(index: number): string {
return `${props.path || props.ariaLabel}[${index + 1}]`
}
function mapValuePath(entry: SerializedMapEntry): string {
return props.path ? `${props.path}.${entry.key}` : String(entry.key)
}
function blankFromStructure(structure: AdminConfiguratorStructure): unknown {
if (structure.kind === 'optionalString') return ''
if (structure.kind === 'value') return blankValue(structure.valueType)
@@ -493,12 +509,33 @@ function mapEntryStructure(
:aria-expanded="expandedEntry === `list:${index}`"
@click="toggleStructuredEntry(`list:${index}`)"
>
<strong>{{ labels.list }} {{ index + 1 }}</strong>
<small>{{
structuredValueLabel(row, listStructure?.items[index])
}}</small>
<span class="config-structured-editor__field-copy">
<strong>{{ labels.entry }} {{ index + 1 }}</strong>
<small
:title="
describe(listEntryPath(index), row, listStructure?.items[index])
"
>
{{
describe(listEntryPath(index), row, listStructure?.items[index])
}}
</small>
</span>
<em>{{ structuredValueLabel(row, listStructure?.items[index]) }}</em>
<ChevronDown :size="14" />
</button>
<span v-else class="config-structured-editor__field-copy is-list-entry">
<strong>{{ labels.entry }} {{ index + 1 }}</strong>
<small
:title="
describe(listEntryPath(index), row, listStructure?.items[index])
"
>
{{
describe(listEntryPath(index), row, listStructure?.items[index])
}}
</small>
</span>
<AdminConfigValueEditor
v-if="
!isStructuredValue(row, listStructure?.items[index]) ||
@@ -507,9 +544,11 @@ function mapEntryStructure(
:model-value="row"
:structure="listStructure?.items[index]"
:aria-label="`${ariaLabel} ${index + 1}`"
:describe="describe"
:labels="labels"
:disabled="disabled"
:depth="depth + 1"
:path="listEntryPath(index)"
@update:model-value="updateListRow(index, $event)"
/>
<button
@@ -610,6 +649,23 @@ function mapEntryStructure(
:disabled="disabled || Boolean(mapEntryStructure(entry))"
@change="updateMapKey(index, $event)"
/>
<em
:title="
describe(
mapValuePath(entry),
entry.value,
mapEntryStructure(entry),
)
"
>
{{
describe(
mapValuePath(entry),
entry.value,
mapEntryStructure(entry),
)
}}
</em>
</span>
<button
v-if="isStructuredValue(entry.value, mapEntryStructure(entry))"
@@ -632,9 +688,11 @@ function mapEntryStructure(
:model-value="entry.value"
:structure="mapEntryStructure(entry)"
:aria-label="`${ariaLabel} ${entry.key}`"
:describe="describe"
:labels="labels"
:disabled="disabled"
:depth="depth + 1"
:path="mapValuePath(entry)"
@update:model-value="updateMapValue(index, $event)"
/>
<button
@@ -678,13 +736,31 @@ function mapEntryStructure(
:aria-expanded="expandedEntry === `table:${key}`"
@click="toggleStructuredEntry(`table:${key}`)"
>
<strong>{{ key }}</strong>
<small>{{
structuredValueLabel(value, tableFieldStructure(key))
}}</small>
<span class="config-structured-editor__field-copy">
<strong>{{ key }}</strong>
<small
:title="
describe(tableEntryPath(key), value, tableFieldStructure(key))
"
>
{{
describe(tableEntryPath(key), value, tableFieldStructure(key))
}}
</small>
</span>
<em>{{ structuredValueLabel(value, tableFieldStructure(key)) }}</em>
<ChevronDown :size="14" />
</button>
<strong v-else>{{ key }}</strong>
<span v-else class="config-structured-editor__field-copy">
<strong>{{ key }}</strong>
<small
:title="
describe(tableEntryPath(key), value, tableFieldStructure(key))
"
>
{{ describe(tableEntryPath(key), value, tableFieldStructure(key)) }}
</small>
</span>
<AdminConfigValueEditor
v-if="
!isStructuredValue(value, tableFieldStructure(key)) ||
@@ -693,9 +769,11 @@ function mapEntryStructure(
:model-value="value"
:structure="tableFieldStructure(key)"
:aria-label="`${ariaLabel} ${key}`"
:describe="describe"
:labels="labels"
:disabled="disabled"
:depth="depth + 1"
:path="tableEntryPath(key)"
@update:model-value="updateTableField(key, $event)"
/>
<button
@@ -927,7 +1005,7 @@ function mapEntryStructure(
.config-structured-editor__row,
.config-structured-editor__property {
display: grid;
grid-template-columns: 24px minmax(0, 1fr) 27px;
grid-template-columns: 24px minmax(105px, 0.42fr) minmax(140px, 1fr) 27px;
align-items: center;
gap: 6px;
padding: 5px 6px;
@@ -989,7 +1067,7 @@ function mapEntryStructure(
.config-structured-editor__row.has-structured-value
> .config-structured-editor__section-toggle {
grid-column: 2;
grid-column: 2 / 4;
grid-row: 1;
}
@@ -1007,7 +1085,7 @@ function mapEntryStructure(
.config-structured-editor__row.has-structured-value
> .config-structured-editor__remove {
grid-column: 3;
grid-column: 4;
grid-row: 1;
}
@@ -1033,20 +1111,44 @@ function mapEntryStructure(
background: rgba(255, 255, 255, 0.025);
}
.config-structured-editor__section-toggle strong {
.config-structured-editor__field-copy {
min-width: 0;
display: grid;
gap: 2px;
text-align: left;
}
.config-structured-editor__field-copy strong {
overflow: hidden;
min-width: 0;
color: #c9cec9;
font-size: 9px;
font-weight: 600;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-structured-editor__section-toggle small {
.config-structured-editor__field-copy small {
overflow: hidden;
color: var(--admin-muted);
font-size: 7.5px;
font-weight: 450;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-structured-editor__section-toggle
> .config-structured-editor__field-copy {
flex: 1 1 auto;
}
.config-structured-editor__section-toggle > em {
margin-left: auto;
color: var(--admin-muted);
font-size: 7px;
font-weight: 600;
font-style: normal;
letter-spacing: 0.05em;
text-transform: uppercase;
}
@@ -1082,13 +1184,23 @@ function mapEntryStructure(
gap: 3px;
}
.config-structured-editor__map-key small {
.config-structured-editor__map-key > small {
color: var(--admin-dim);
font-size: 7px;
letter-spacing: 0.04em;
text-transform: uppercase;
}
.config-structured-editor__map-key > em {
overflow: hidden;
color: var(--admin-muted);
font-size: 7px;
font-style: normal;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-structured-editor__map-key input {
width: 100%;
min-width: 0;
@@ -1096,15 +1208,6 @@ function mapEntryStructure(
padding: 0 7px;
}
.config-structured-editor__property > strong {
overflow: hidden;
color: #c9cec9;
font-size: 9px;
font-weight: 550;
text-overflow: ellipsis;
white-space: nowrap;
}
.config-structured-editor__index {
color: var(--admin-dim);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
+42 -3
View File
@@ -53,6 +53,7 @@ import type {
} from '@/types/admin'
import type { LaunchablePhoneAppDefinition } from '@/types/apps'
import { SkyButton } from '@/ui'
import { describeConfiguratorValue } from '@/utils/adminConfiguratorDescription'
import { copyText } from '@/utils/clipboard'
import { parseDatabaseDate } from '@/utils/date'
import { nuiCall } from '@/utils/nui'
@@ -336,6 +337,15 @@ function t(key: string, params?: Record<string, string>): string {
return phone.t('AdminPanel.' + key, params)
}
function configuratorDescription(
path: string,
value: unknown,
structure?: AdminConfiguratorStructure,
label?: string,
): string {
return describeConfiguratorValue(t, path, value, structure, label)
}
const configuratorEditorLabels = computed<AdminConfigEditorLabels>(() => ({
addField: t('configurator.table.addField'),
addRow: t('configurator.table.addRow'),
@@ -345,6 +355,7 @@ const configuratorEditorLabels = computed<AdminConfigEditorLabels>(() => ({
convertToTable: t('configurator.table.convertToTable'),
emptyList: t('configurator.table.emptyList'),
emptyTable: t('configurator.table.emptyTable'),
entry: t('configurator.table.entry'),
keyPlaceholder: t('configurator.table.keyPlaceholder'),
list: t('configurator.table.list'),
remove: t('configurator.table.remove'),
@@ -1328,7 +1339,26 @@ onBeforeUnmount(() => {
>
<span class="admin-panel-config-field__copy">
<strong>{{ field.label }}</strong>
<small>{{ field.path }}</small>
<small
:title="
configuratorDescription(
field.path,
configuratorFieldValue(field),
field.structure,
field.label,
)
"
>
{{
configuratorDescription(
field.path,
configuratorFieldValue(field),
field.structure,
field.label,
)
}}
</small>
<code>{{ field.path }}</code>
</span>
<span
@@ -1350,8 +1380,10 @@ onBeforeUnmount(() => {
:model-value="configuratorFieldValue(field)"
:structure="field.structure"
:aria-label="`${field.label} ${field.path}`"
:describe="configuratorDescription"
:labels="configuratorEditorLabels"
:disabled="!admin.configurator.enabled"
:path="field.path"
@update:model-value="
updateConfiguratorField(field, $event)
"
@@ -3598,7 +3630,8 @@ button:disabled {
}
.admin-panel-config-field__copy strong,
.admin-panel-config-field__copy small {
.admin-panel-config-field__copy small,
.admin-panel-config-field__copy code {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
@@ -3611,8 +3644,14 @@ button:disabled {
.admin-panel-config-field__copy small {
color: var(--admin-muted);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 8px;
line-height: 1.25;
}
.admin-panel-config-field__copy code {
color: var(--admin-dim);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 7px;
}
.admin-panel-config-field > input,
+35
View File
@@ -879,10 +879,45 @@ const adminPanelFallbackLocales = {
secretConfigured: 'Secret configured · enter a replacement',
invalidValue: 'Check the highlighted table or number value.',
saved: 'SQL configuration saved.',
descriptions: {
featureToggle: 'Turns {name} on or off.',
boolean: 'Controls whether {name} is allowed.',
number: 'Sets the numeric value for {name}.',
text: 'Sets the text value used for {name}.',
optionalText:
'Sets the optional value for {name}; switch it off to disable it.',
list: 'Manages all entries used for {name}.',
table: 'Groups the related settings for {name}.',
credential: 'Stores the protected credential used by {name}.',
url: 'Sets the URL or endpoint used by {name}.',
hosts: 'Defines which domains are allowed for {name}.',
milliseconds: 'Sets the timing for {name} in milliseconds.',
seconds: 'Sets the timing for {name} in seconds.',
rateLimit: 'Limits how many {name} actions are allowed per minute.',
byteLimit: 'Sets the maximum data size allowed for {name}.',
textLimit: 'Sets the maximum text length allowed for {name}.',
distance: 'Sets the world distance used for {name}.',
coordinates: 'Sets the world coordinates or orientation for {name}.',
gameAsset: 'Sets the GTA model or prop used for {name}.',
animation: 'Sets the animation asset used for {name}.',
access: 'Defines the jobs, groups or permission level for {name}.',
integration: 'Selects the connected framework or provider for {name}.',
path: 'Sets the storage or resource path used for {name}.',
color: 'Sets the interface color used for {name}.',
displayText: 'Sets the text shown to players for {name}.',
phoneNumber: 'Sets the phone or service number used for {name}.',
routing: 'Controls how incoming requests are routed for {name}.',
command: 'Sets the chat command used to open or run {name}.',
locale: 'Selects the language used for {name}.',
debug: 'Controls detailed diagnostic output for {name}.',
mediaQuality: 'Sets the media quality or volume used for {name}.',
amount: 'Sets the maximum or displayed amount for {name}.',
},
table: {
list: 'List',
table: 'Key table',
vector: 'Vector',
entry: 'Entry',
addRow: 'Add row',
addField: 'Add field',
remove: 'Remove',
@@ -0,0 +1,55 @@
import { describe, expect, it } from 'vitest'
import type { AdminConfiguratorStructure } from '@/types/admin'
import {
configuratorDescriptionKey,
describeConfiguratorValue,
} from './adminConfiguratorDescription'
describe('admin configurator descriptions', () => {
it('selects specific descriptions before generic value descriptions', () => {
expect(configuratorDescriptionKey('Bridge.CallbackTimeout', 15000)).toBe(
'milliseconds',
)
expect(configuratorDescriptionKey('Media.RequestTimeoutMs', 10000)).toBe(
'milliseconds',
)
expect(configuratorDescriptionKey('AdminPanel.AdminGroups', [])).toBe(
'access',
)
expect(configuratorDescriptionKey('FiveManage.ApiKey', '')).toBe(
'credential',
)
})
it('describes structured values from their schema', () => {
const vector: AdminConfiguratorStructure = {
kind: 'vector',
vectorType: 'vector3',
}
const table: AdminConfiguratorStructure = {
fields: {},
kind: 'table',
}
expect(configuratorDescriptionKey('Location', {}, vector)).toBe(
'coordinates',
)
expect(configuratorDescriptionKey('Settings', {}, table)).toBe('table')
})
it('passes a readable field name to the localized template', () => {
const translate = (key: string, params?: Record<string, string>) =>
`${key}:${params?.name}`
expect(
describeConfiguratorValue(
translate,
'CustomApps.MaximumStorageBytesPerApp',
262144,
),
).toBe('configurator.descriptions.byteLimit:Maximum Storage Bytes Per App')
expect(
describeConfiguratorValue(translate, 'Radio.AllowedJobs[2]', 'police'),
).toBe('configurator.descriptions.access:Allowed Jobs #2')
})
})
@@ -0,0 +1,103 @@
import type { AdminConfiguratorStructure } from '@/types/admin'
type ConfiguratorDescriptionTranslator = (
key: string,
params?: Record<string, string>,
) => string
export type AdminConfiguratorDescribe = (
path: string,
value: unknown,
structure?: AdminConfiguratorStructure,
label?: string,
) => string
const DESCRIPTION_RULES: Array<[RegExp, string]> = [
[/(?:^|\.)(?:apikey|token|password|secret)$/i, 'credential'],
[/(?:base|manifest|image|icon)?url$/i, 'url'],
[/(?:allowed)?(?:gif|media)?hosts?$/i, 'hosts'],
[
/(?:timeout|timeoutms|milliseconds|durationms|intervalms|pollms)$/i,
'milliseconds',
],
[/(?:timeoutseconds|seconds)$/i, 'seconds'],
[/perminute$/i, 'rateLimit'],
[/(?:maximum|max).*bytes/i, 'byteLimit'],
[/(?:maximum|max).*length$|length$/i, 'textLimit'],
[/(?:distance)$/i, 'distance'],
[/(?:location|position|rotation|coords|coordinates)$/i, 'coordinates'],
[/(?:model|prop|propmodel|customprop|replacementprop)$/i, 'gameAsset'],
[/(?:dictionary|dictionaries|clip|clips|pedclip|propclip)$/i, 'animation'],
[
/(?:permissions?|admingroups?|allowedjobs?|jobs?|minimumgrade|requiredace)$/i,
'access',
],
[/(?:framework|inventory|provider|voiceprovider|adapter)$/i, 'integration'],
[/(?:path)$/i, 'path'],
[/(?:color|colour|accent)$/i, 'color'],
[
/(?:label|name|title|description|address|district|locationlabel|devicename)$/i,
'displayText',
],
[/(?:number|callernumber|numberprefix)$/i, 'phoneNumber'],
[/(?:routing)$/i, 'routing'],
[/(?:command)$/i, 'command'],
[/(?:locale)$/i, 'locale'],
[/(?:debug)$/i, 'debug'],
[/(?:enabled|active|public|verified)$/i, 'featureToggle'],
[/(?:quality|bitratekbps|volume)$/i, 'mediaQuality'],
[
/(?:pagesize|batchsize|limit|count|maxselection|maximumplayers|samples|decimals)$/i,
'amount',
],
]
function pathName(path: string): string {
const listEntry = path.match(/^(.*)\[(\d+)\]$/)
const source = listEntry?.[1] ?? path
const segment = source.split('.').at(-1) ?? source
const name = segment
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
.replace(/([a-z\d])([A-Z])/g, '$1 $2')
.replace(/[_-]+/g, ' ')
.trim()
return listEntry ? `${name} #${listEntry[2]}` : name
}
export function configuratorDescriptionKey(
path: string,
value: unknown,
structure?: AdminConfiguratorStructure,
): string {
const segment =
path
.replace(/\[\d+\]$/, '')
.split('.')
.at(-1) ?? path
const semanticRule = DESCRIPTION_RULES.find(([pattern]) =>
pattern.test(segment),
)
if (semanticRule) return semanticRule[1]
if (structure?.kind === 'vector') return 'coordinates'
if (structure?.kind === 'list' || Array.isArray(value)) return 'list'
if (structure?.kind === 'map' || structure?.kind === 'table') return 'table'
if (structure?.kind === 'optionalString') return 'optionalText'
if (value !== null && typeof value === 'object') return 'table'
if (typeof value === 'boolean') return 'boolean'
if (typeof value === 'number') return 'number'
return 'text'
}
export function describeConfiguratorValue(
translate: ConfiguratorDescriptionTranslator,
path: string,
value: unknown,
structure?: AdminConfiguratorStructure,
label?: string,
): string {
return translate(
`configurator.descriptions.${configuratorDescriptionKey(path, value, structure)}`,
{ name: label?.trim() || pathName(path) },
)
}
+1 -1
View File
@@ -206,7 +206,7 @@ Locales["de"] = {
tabs = { overview = "Übersicht", players = "Spieler", devices = "Geräte", apps = "Apps", accounts = "Accounts", messages = "Nachrichten", calls = "Anrufe", moderation = "Moderation", audit = "Audit", configurator = "Phone Configurator" },
overview = { eyebrow = "Server", title = "Dashboard", body = "Spieler, Geräte, Apps und Handydaten.", stats = "Server-Handystatistik", online = "Online", devices = "Geräte", accounts = "Accounts", audit = "Audit-Einträge", control = "Navigation", features = "Module", featuresBody = "Öffne ein Verwaltungsmodul.", recent = "Letzte Aktivitäten", playerFeature = "Identität, Finanzen, Job und Dienst", deviceFeature = "IMEI, SIM, Nummer und Aktivität", appFeature = "Handy-Apps installieren oder entfernen", accountFeature = "Accountzugriff und geschützte Zugangsdaten", messageFeature = "Letzte SMS-Aktivitäten prüfen", callFeature = "Letzte Anrufaktivitäten prüfen", moderationFeature = "Zugriff, Nummer oder Gerätedaten zurücksetzen", auditFeature = "Sensible Admin-Aktionen prüfen", configuratorFeature = "config.lua und media.lua über SQL verwalten" },
appearance = { eyebrow = "Darstellung", title = "Akzentfarbe", body = "Ändere den Akzent im gesamten Admin-Arbeitsbereich.", colors = { emerald = "Smaragd", blue = "Blau", violet = "Violett", orange = "Orange", red = "Rot" } },
configurator = { context = "Runtime-Konfiguration", eyebrow = "Systemwerkzeug", sections = "Konfiguration", search = "Einstellungen oder Pfade suchen", configScope = "config.lua", mediaScope = "media.lua", noResults = "Keine passenden Einstellungen", loading = "SQL-Konfiguration wird geladen...", title = "Phone Configurator", body = "Verwalte Handy- und Media-Einstellungen im geschützten Admin-Bereich.", disabledTitle = "SQL-Konfiguration ist nicht aktiv", disabledBody = "Aktiviere den Configurator am Anfang der config.lua und starte sky_phone neu. Bis dahin bleiben die Dateiwerte aktiv und die Bearbeitung gesperrt.", manualSave = "Manuelles Speichern", restartNotice = "Nichts wird automatisch gespeichert. Der grüne Haken schreibt alle vorgemerkten Werte. Startgebundene Einstellungen werden nach einem Neustart von sky_phone vollständig aktiv.", fieldCount = "{count} Felder", secretConfigured = "Secret gesetzt · Ersatzwert eingeben", invalidValue = "Prüfe den markierten Tabellen- oder Zahlenwert.", saved = "SQL-Konfiguration gespeichert.", table = { list = "Liste", table = "Schlüsseltabelle", vector = "Vektor", addRow = "Zeile hinzufügen", addField = "Feld hinzufügen", remove = "Entfernen", emptyList = "Noch keine Zeilen. Füge die erste Zeile mit Plus hinzu.", emptyTable = "Noch keine Felder. Füge unten den ersten Schlüssel hinzu.", keyPlaceholder = "Neuer Schlüssel", convertToList = "Als Liste nutzen", convertToMap = "Als typisierte Schlüsseltabelle nutzen", convertToTable = "Als Schlüsseltabelle nutzen", types = { string = "Text", number = "Zahl", boolean = "Schalter", list = "Liste", table = "Tabelle" } } },
configurator = { context = "Runtime-Konfiguration", eyebrow = "Systemwerkzeug", sections = "Konfiguration", search = "Einstellungen oder Pfade suchen", configScope = "config.lua", mediaScope = "media.lua", noResults = "Keine passenden Einstellungen", loading = "SQL-Konfiguration wird geladen...", title = "Phone Configurator", body = "Verwalte Handy- und Media-Einstellungen im geschützten Admin-Bereich.", disabledTitle = "SQL-Konfiguration ist nicht aktiv", disabledBody = "Aktiviere den Configurator am Anfang der config.lua und starte sky_phone neu. Bis dahin bleiben die Dateiwerte aktiv und die Bearbeitung gesperrt.", manualSave = "Manuelles Speichern", restartNotice = "Nichts wird automatisch gespeichert. Der grüne Haken schreibt alle vorgemerkten Werte. Startgebundene Einstellungen werden nach einem Neustart von sky_phone vollständig aktiv.", fieldCount = "{count} Felder", secretConfigured = "Secret gesetzt · Ersatzwert eingeben", invalidValue = "Prüfe den markierten Tabellen- oder Zahlenwert.", saved = "SQL-Konfiguration gespeichert.", descriptions = { featureToggle = "Schaltet {name} ein oder aus.", boolean = "Legt fest, ob {name} erlaubt ist.", number = "Legt den Zahlenwert für {name} fest.", text = "Legt den Textwert für {name} fest.", optionalText = "Legt den optionalen Wert für {name} fest; der Schalter deaktiviert ihn.", list = "Verwaltet alle Einträge für {name}.", table = "Bündelt die zusammengehörigen Einstellungen für {name}.", credential = "Speichert den geschützten Zugangsschlüssel für {name}.", url = "Legt die URL oder den Endpunkt für {name} fest.", hosts = "Legt die erlaubten Domains für {name} fest.", milliseconds = "Legt die Zeit für {name} in Millisekunden fest.", seconds = "Legt die Zeit für {name} in Sekunden fest.", rateLimit = "Begrenzt die Aktionen für {name} pro Minute.", byteLimit = "Legt die maximal erlaubte Datengröße für {name} fest.", textLimit = "Legt die maximal erlaubte Textlänge für {name} fest.", distance = "Legt die Distanz in der Spielwelt für {name} fest.", coordinates = "Legt Weltkoordinaten oder Ausrichtung für {name} fest.", gameAsset = "Legt das GTA-Modell oder Prop für {name} fest.", animation = "Legt die Animationsdatei für {name} fest.", access = "Legt Jobs, Gruppen oder Berechtigungsstufen für {name} fest.", integration = "Wählt Framework oder Anbieter für {name} aus.", path = "Legt den Speicher- oder Ressourcenpfad für {name} fest.", color = "Legt die Oberflächenfarbe für {name} fest.", displayText = "Legt den Spielern angezeigten Text für {name} fest.", phoneNumber = "Legt die Telefon- oder Servicenummer für {name} fest.", routing = "Steuert die Verteilung eingehender Anfragen für {name}.", command = "Legt den Chat-Befehl zum Öffnen oder Ausführen von {name} fest.", locale = "Wählt die Sprache für {name} aus.", debug = "Steuert ausführliche Diagnoseausgaben für {name}.", mediaQuality = "Legt Medienqualität oder Lautstärke für {name} fest.", amount = "Legt die maximale oder angezeigte Menge für {name} fest." }, table = { list = "Liste", table = "Schlüsseltabelle", vector = "Vektor", entry = "Eintrag", addRow = "Zeile hinzufügen", addField = "Feld hinzufügen", remove = "Entfernen", emptyList = "Noch keine Zeilen. Füge die erste Zeile mit Plus hinzu.", emptyTable = "Noch keine Felder. Füge unten den ersten Schlüssel hinzu.", keyPlaceholder = "Neuer Schlüssel", convertToList = "Als Liste nutzen", convertToMap = "Als typisierte Schlüsseltabelle nutzen", convertToTable = "Als Schlüsseltabelle nutzen", types = { string = "Text", number = "Zahl", boolean = "Schalter", list = "Liste", table = "Tabelle" } } },
players = { eyebrow = "Aktive Sitzungen", title = "Online-Spieler", online = "Jetzt online", empty = "Keine Spieler gefunden", emptyBody = "Passe die Suche an oder aktualisiere die Spielerliste." },
search = { players = "Name, ID, Job oder Nummer suchen", apps = "Apps suchen", clear = "Suche leeren" },
detail = { character = "Charakterprofil", data = "Spielerdatenübersicht", cash = "Bargeld", bank = "Bank", job = "Job", duty = "Dienst", onDuty = "Im Dienst", offDuty = "Außer Dienst", identity = "Identität", playerData = "Spielerdaten", identifier = "Charakter-Identifier", birthdate = "Geburtsdatum", grade = "Job-Rang", unknown = "Unbekannt" },
+1 -1
View File
@@ -206,7 +206,7 @@ Locales["en"] = {
tabs = { overview = "Overview", players = "Players", devices = "Devices", apps = "Apps", accounts = "Accounts", messages = "Messages", calls = "Calls", moderation = "Moderation", audit = "Audit", configurator = "Phone configurator" },
overview = { eyebrow = "Server", title = "Dashboard", body = "Players, devices, apps, and phone data.", stats = "Server phone statistics", online = "Online", devices = "Devices", accounts = "Accounts", audit = "Audit entries", control = "Navigation", features = "Modules", featuresBody = "Open an administration module.", recent = "Recent activity", playerFeature = "Identity, finances, job, and duty", deviceFeature = "IMEI, SIM, number, and activity", appFeature = "Install or remove phone apps", accountFeature = "Account access and protected credentials", messageFeature = "Review recent SMS activity", callFeature = "Review recent call activity", moderationFeature = "Reset access, number, or device data", auditFeature = "Review sensitive admin actions", configuratorFeature = "Manage config.lua and media.lua through SQL" },
appearance = { eyebrow = "Appearance", title = "Accent color", body = "Change the accent across the complete admin workspace.", colors = { emerald = "Emerald", blue = "Blue", violet = "Violet", orange = "Orange", red = "Red" } },
configurator = { context = "Runtime configuration", eyebrow = "System tool", sections = "Configuration", search = "Search settings or paths", configScope = "config.lua", mediaScope = "media.lua", noResults = "No matching settings", loading = "Loading SQL configuration...", title = "Phone configurator", body = "Manage phone and media settings from the protected admin workspace.", disabledTitle = "SQL configuration is not active", disabledBody = "Enable the configurator at the beginning of config.lua and restart sky_phone. Until then, file values remain active and editing is locked.", manualSave = "Manual save", restartNotice = "Nothing is written automatically. Press the green check to persist all staged values. Startup-bound settings take full effect after restarting sky_phone.", fieldCount = "{count} fields", secretConfigured = "Secret configured · enter a replacement", invalidValue = "Check the highlighted table or number value.", saved = "SQL configuration saved.", table = { list = "List", table = "Key table", vector = "Vector", addRow = "Add row", addField = "Add field", remove = "Remove", emptyList = "No rows yet. Add the first row with plus.", emptyTable = "No fields yet. Add the first key below.", keyPlaceholder = "New key", convertToList = "Use list", convertToMap = "Use typed key table", convertToTable = "Use key table", types = { string = "Text", number = "Number", boolean = "Switch", list = "List", table = "Table" } } },
configurator = { context = "Runtime configuration", eyebrow = "System tool", sections = "Configuration", search = "Search settings or paths", configScope = "config.lua", mediaScope = "media.lua", noResults = "No matching settings", loading = "Loading SQL configuration...", title = "Phone configurator", body = "Manage phone and media settings from the protected admin workspace.", disabledTitle = "SQL configuration is not active", disabledBody = "Enable the configurator at the beginning of config.lua and restart sky_phone. Until then, file values remain active and editing is locked.", manualSave = "Manual save", restartNotice = "Nothing is written automatically. Press the green check to persist all staged values. Startup-bound settings take full effect after restarting sky_phone.", fieldCount = "{count} fields", secretConfigured = "Secret configured · enter a replacement", invalidValue = "Check the highlighted table or number value.", saved = "SQL configuration saved.", descriptions = { featureToggle = "Turns {name} on or off.", boolean = "Controls whether {name} is allowed.", number = "Sets the numeric value for {name}.", text = "Sets the text value used for {name}.", optionalText = "Sets the optional value for {name}; switch it off to disable it.", list = "Manages all entries used for {name}.", table = "Groups the related settings for {name}.", credential = "Stores the protected credential used by {name}.", url = "Sets the URL or endpoint used by {name}.", hosts = "Defines which domains are allowed for {name}.", milliseconds = "Sets the timing for {name} in milliseconds.", seconds = "Sets the timing for {name} in seconds.", rateLimit = "Limits how many {name} actions are allowed per minute.", byteLimit = "Sets the maximum data size allowed for {name}.", textLimit = "Sets the maximum text length allowed for {name}.", distance = "Sets the world distance used for {name}.", coordinates = "Sets the world coordinates or orientation for {name}.", gameAsset = "Sets the GTA model or prop used for {name}.", animation = "Sets the animation asset used for {name}.", access = "Defines the jobs, groups or permission level for {name}.", integration = "Selects the connected framework or provider for {name}.", path = "Sets the storage or resource path used for {name}.", color = "Sets the interface color used for {name}.", displayText = "Sets the text shown to players for {name}.", phoneNumber = "Sets the phone or service number used for {name}.", routing = "Controls how incoming requests are routed for {name}.", command = "Sets the chat command used to open or run {name}.", locale = "Selects the language used for {name}.", debug = "Controls detailed diagnostic output for {name}.", mediaQuality = "Sets the media quality or volume used for {name}.", amount = "Sets the maximum or displayed amount for {name}." }, table = { list = "List", table = "Key table", vector = "Vector", entry = "Entry", addRow = "Add row", addField = "Add field", remove = "Remove", emptyList = "No rows yet. Add the first row with plus.", emptyTable = "No fields yet. Add the first key below.", keyPlaceholder = "New key", convertToList = "Use list", convertToMap = "Use typed key table", convertToTable = "Use key table", types = { string = "Text", number = "Number", boolean = "Switch", list = "List", table = "Table" } } },
players = { eyebrow = "Active sessions", title = "Online players", online = "Online now", empty = "No players found", emptyBody = "Adjust the search or refresh the live player list." },
search = { players = "Search name, ID, job, or number", apps = "Search apps", clear = "Clear search" },
detail = { character = "Character profile", data = "Player data overview", cash = "Cash", bank = "Bank", job = "Job", duty = "Duty", onDuty = "On duty", offDuty = "Off duty", identity = "Identity", playerData = "Player data", identifier = "Character identifier", birthdate = "Birthdate", grade = "Job grade", unknown = "Unknown" },