diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 58ba6d7a9..3861d0b22 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -62,6 +62,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult { showStudioAsText css cssEnabled + customLocales + customLocalesEnabled language imageLightbox { slideshowDelay diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 9a84f0cc6..35671a10c 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -259,6 +259,10 @@ input ConfigInterfaceInput { """Custom CSS""" css: String cssEnabled: Boolean + + """Custom Locales""" + customLocales: String + customLocalesEnabled: Boolean """Interface language""" language: String @@ -322,6 +326,10 @@ type ConfigInterfaceResult { css: String cssEnabled: Boolean + """Custom Locales""" + customLocales: String + customLocalesEnabled: Boolean + """Interface language""" language: String diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index f1e35239d..9881f46fb 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -356,6 +356,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI setBool(config.CSSEnabled, input.CSSEnabled) + if input.CustomLocales != nil { + c.SetCustomLocales(*input.CustomLocales) + } + + setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled) + if input.DisableDropdownCreate != nil { ddc := input.DisableDropdownCreate setBool(config.DisableDropdownCreatePerformer, ddc.Performer) diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index f3469de97..95411977c 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -141,6 +141,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { showStudioAsText := config.GetShowStudioAsText() css := config.GetCSS() cssEnabled := config.GetCSSEnabled() + customLocales := config.GetCustomLocales() + customLocalesEnabled := config.GetCustomLocalesEnabled() language := config.GetLanguage() handyKey := config.GetHandyKey() scriptOffset := config.GetFunscriptOffset() @@ -164,6 +166,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult { ContinuePlaylistDefault: &continuePlaylistDefault, CSS: &css, CSSEnabled: &cssEnabled, + CustomLocales: &customLocales, + CustomLocalesEnabled: &customLocalesEnabled, Language: &language, ImageLightbox: &imageLightboxOptions, diff --git a/internal/api/server.go b/internal/api/server.go index 5a3719c24..853044da9 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -179,6 +179,21 @@ func Start() error { http.ServeFile(w, r, fn) }) + r.HandleFunc("/customlocales", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + if !c.GetCustomLocalesEnabled() { + return + } + + // search for custom-locales.json in current directory, then $HOME/.stash + fn := c.GetCustomLocalesPath() + exists, _ := fsutil.FileExists(fn) + if !exists { + return + } + + http.ServeFile(w, r, fn) + }) r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) { ext := path.Ext(r.URL.Path) diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 3be7be32a..fdbb45bba 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -138,6 +138,7 @@ const ( ContinuePlaylistDefault = "continue_playlist_default" ShowStudioAsText = "show_studio_as_text" CSSEnabled = "cssEnabled" + CustomLocalesEnabled = "customLocalesEnabled" ShowScrubber = "show_scrubber" showScrubberDefault = true @@ -1056,6 +1057,49 @@ func (i *Instance) GetCSSEnabled() bool { return i.getBool(CSSEnabled) } +func (i *Instance) GetCustomLocalesPath() string { + // use custom-locales.json in the same directory as the config file + configFileUsed := i.GetConfigFile() + configDir := filepath.Dir(configFileUsed) + + fn := filepath.Join(configDir, "custom-locales.json") + + return fn +} + +func (i *Instance) GetCustomLocales() string { + fn := i.GetCustomLocalesPath() + + exists, _ := fsutil.FileExists(fn) + if !exists { + return "" + } + + buf, err := os.ReadFile(fn) + + if err != nil { + return "" + } + + return string(buf) +} + +func (i *Instance) SetCustomLocales(customLocales string) { + fn := i.GetCustomLocalesPath() + i.Lock() + defer i.Unlock() + + buf := []byte(customLocales) + + if err := os.WriteFile(fn, buf, 0777); err != nil { + logger.Warnf("error while writing %v bytes to %v: %v", len(buf), fn, err) + } +} + +func (i *Instance) GetCustomLocalesEnabled() bool { + return i.getBool(CustomLocalesEnabled) +} + func (i *Instance) GetHandyKey() string { return i.getString(HandyKey) } diff --git a/internal/manager/config/config_concurrency_test.go b/internal/manager/config/config_concurrency_test.go index 8af130419..5ed8deac3 100644 --- a/internal/manager/config/config_concurrency_test.go +++ b/internal/manager/config/config_concurrency_test.go @@ -85,7 +85,10 @@ func TestConcurrentConfigAccess(t *testing.T) { i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay) i.GetCSSPath() i.GetCSS() + i.GetCustomLocalesPath() + i.GetCustomLocales() i.Set(CSSEnabled, i.GetCSSEnabled()) + i.Set(CSSEnabled, i.GetCustomLocalesEnabled()) i.Set(HandyKey, i.GetHandyKey()) i.Set(DLNAServerName, i.GetDLNAServerName()) i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled()) diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 1ef2e306a..2950cd08b 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -29,6 +29,7 @@ import { InteractiveProvider } from "./hooks/Interactive/context"; import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog"; import { IUIConfig } from "./core/config"; import { releaseNotes } from "./docs/en/ReleaseNotes"; +import { getPlatformURL } from "./core/createClient"; const Performers = lazy(() => import("./components/Performers/Performers")); const FrontPage = lazy(() => import("./components/FrontPage/FrontPage")); @@ -87,11 +88,24 @@ export const App: React.FC = () => { const defaultMessages = (await locales[defaultMessageLanguage]()).default; const mergedMessages = cloneDeep(Object.assign({}, defaultMessages)); const chosenMessages = (await locales[messageLanguage]()).default; - mergeWith(mergedMessages, chosenMessages, (objVal, srcVal) => { - if (srcVal === "") { - return objVal; + const res = await fetch(getPlatformURL() + "customlocales"); + let customMessages = {}; + try { + customMessages = res.ok ? await res.json() : {}; + } catch (err) { + console.log(err); + } + + mergeWith( + mergedMessages, + chosenMessages, + customMessages, + (objVal, srcVal) => { + if (srcVal === "") { + return objVal; + } } - }); + ); setMessages(flattenMessages(mergedMessages)); }; diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index b3fde0c1a..f90121b1e 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -408,6 +408,36 @@ export const SettingsInterfacePanel: React.FC = () => { }} /> + + saveInterface({ customLocalesEnabled: v })} + /> + + + id="custom-locales" + headingID="config.ui.custom_locales.heading" + subHeadingID="config.ui.custom_locales.description" + value={iface.customLocales ?? undefined} + onChange={(v) => saveInterface({ customLocales: v })} + renderField={(value, setValue) => ( + ) => + setValue(e.currentTarget.value) + } + rows={16} + className="text-input code" + /> + )} + renderValue={() => { + return <>; + }} + /> +