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 <>>;
+ }}
+ />
+