mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Custom localization (#2837)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
@@ -62,6 +62,8 @@ fragment ConfigInterfaceData on ConfigInterfaceResult {
|
|||||||
showStudioAsText
|
showStudioAsText
|
||||||
css
|
css
|
||||||
cssEnabled
|
cssEnabled
|
||||||
|
customLocales
|
||||||
|
customLocalesEnabled
|
||||||
language
|
language
|
||||||
imageLightbox {
|
imageLightbox {
|
||||||
slideshowDelay
|
slideshowDelay
|
||||||
|
|||||||
@@ -259,6 +259,10 @@ input ConfigInterfaceInput {
|
|||||||
"""Custom CSS"""
|
"""Custom CSS"""
|
||||||
css: String
|
css: String
|
||||||
cssEnabled: Boolean
|
cssEnabled: Boolean
|
||||||
|
|
||||||
|
"""Custom Locales"""
|
||||||
|
customLocales: String
|
||||||
|
customLocalesEnabled: Boolean
|
||||||
|
|
||||||
"""Interface language"""
|
"""Interface language"""
|
||||||
language: String
|
language: String
|
||||||
@@ -322,6 +326,10 @@ type ConfigInterfaceResult {
|
|||||||
css: String
|
css: String
|
||||||
cssEnabled: Boolean
|
cssEnabled: Boolean
|
||||||
|
|
||||||
|
"""Custom Locales"""
|
||||||
|
customLocales: String
|
||||||
|
customLocalesEnabled: Boolean
|
||||||
|
|
||||||
"""Interface language"""
|
"""Interface language"""
|
||||||
language: String
|
language: String
|
||||||
|
|
||||||
|
|||||||
@@ -356,6 +356,12 @@ func (r *mutationResolver) ConfigureInterface(ctx context.Context, input ConfigI
|
|||||||
|
|
||||||
setBool(config.CSSEnabled, input.CSSEnabled)
|
setBool(config.CSSEnabled, input.CSSEnabled)
|
||||||
|
|
||||||
|
if input.CustomLocales != nil {
|
||||||
|
c.SetCustomLocales(*input.CustomLocales)
|
||||||
|
}
|
||||||
|
|
||||||
|
setBool(config.CustomLocalesEnabled, input.CustomLocalesEnabled)
|
||||||
|
|
||||||
if input.DisableDropdownCreate != nil {
|
if input.DisableDropdownCreate != nil {
|
||||||
ddc := input.DisableDropdownCreate
|
ddc := input.DisableDropdownCreate
|
||||||
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
setBool(config.DisableDropdownCreatePerformer, ddc.Performer)
|
||||||
|
|||||||
@@ -141,6 +141,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
|||||||
showStudioAsText := config.GetShowStudioAsText()
|
showStudioAsText := config.GetShowStudioAsText()
|
||||||
css := config.GetCSS()
|
css := config.GetCSS()
|
||||||
cssEnabled := config.GetCSSEnabled()
|
cssEnabled := config.GetCSSEnabled()
|
||||||
|
customLocales := config.GetCustomLocales()
|
||||||
|
customLocalesEnabled := config.GetCustomLocalesEnabled()
|
||||||
language := config.GetLanguage()
|
language := config.GetLanguage()
|
||||||
handyKey := config.GetHandyKey()
|
handyKey := config.GetHandyKey()
|
||||||
scriptOffset := config.GetFunscriptOffset()
|
scriptOffset := config.GetFunscriptOffset()
|
||||||
@@ -164,6 +166,8 @@ func makeConfigInterfaceResult() *ConfigInterfaceResult {
|
|||||||
ContinuePlaylistDefault: &continuePlaylistDefault,
|
ContinuePlaylistDefault: &continuePlaylistDefault,
|
||||||
CSS: &css,
|
CSS: &css,
|
||||||
CSSEnabled: &cssEnabled,
|
CSSEnabled: &cssEnabled,
|
||||||
|
CustomLocales: &customLocales,
|
||||||
|
CustomLocalesEnabled: &customLocalesEnabled,
|
||||||
Language: &language,
|
Language: &language,
|
||||||
|
|
||||||
ImageLightbox: &imageLightboxOptions,
|
ImageLightbox: &imageLightboxOptions,
|
||||||
|
|||||||
@@ -179,6 +179,21 @@ func Start() error {
|
|||||||
|
|
||||||
http.ServeFile(w, r, fn)
|
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) {
|
r.HandleFunc("/login*", func(w http.ResponseWriter, r *http.Request) {
|
||||||
ext := path.Ext(r.URL.Path)
|
ext := path.Ext(r.URL.Path)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ const (
|
|||||||
ContinuePlaylistDefault = "continue_playlist_default"
|
ContinuePlaylistDefault = "continue_playlist_default"
|
||||||
ShowStudioAsText = "show_studio_as_text"
|
ShowStudioAsText = "show_studio_as_text"
|
||||||
CSSEnabled = "cssEnabled"
|
CSSEnabled = "cssEnabled"
|
||||||
|
CustomLocalesEnabled = "customLocalesEnabled"
|
||||||
|
|
||||||
ShowScrubber = "show_scrubber"
|
ShowScrubber = "show_scrubber"
|
||||||
showScrubberDefault = true
|
showScrubberDefault = true
|
||||||
@@ -1056,6 +1057,49 @@ func (i *Instance) GetCSSEnabled() bool {
|
|||||||
return i.getBool(CSSEnabled)
|
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 {
|
func (i *Instance) GetHandyKey() string {
|
||||||
return i.getString(HandyKey)
|
return i.getString(HandyKey)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,7 +85,10 @@ func TestConcurrentConfigAccess(t *testing.T) {
|
|||||||
i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
i.Set(ImageLightboxSlideshowDelay, *i.GetImageLightboxOptions().SlideshowDelay)
|
||||||
i.GetCSSPath()
|
i.GetCSSPath()
|
||||||
i.GetCSS()
|
i.GetCSS()
|
||||||
|
i.GetCustomLocalesPath()
|
||||||
|
i.GetCustomLocales()
|
||||||
i.Set(CSSEnabled, i.GetCSSEnabled())
|
i.Set(CSSEnabled, i.GetCSSEnabled())
|
||||||
|
i.Set(CSSEnabled, i.GetCustomLocalesEnabled())
|
||||||
i.Set(HandyKey, i.GetHandyKey())
|
i.Set(HandyKey, i.GetHandyKey())
|
||||||
i.Set(DLNAServerName, i.GetDLNAServerName())
|
i.Set(DLNAServerName, i.GetDLNAServerName())
|
||||||
i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled())
|
i.Set(DLNADefaultEnabled, i.GetDLNADefaultEnabled())
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { InteractiveProvider } from "./hooks/Interactive/context";
|
|||||||
import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog";
|
import { ReleaseNotesDialog } from "./components/Dialogs/ReleaseNotesDialog";
|
||||||
import { IUIConfig } from "./core/config";
|
import { IUIConfig } from "./core/config";
|
||||||
import { releaseNotes } from "./docs/en/ReleaseNotes";
|
import { releaseNotes } from "./docs/en/ReleaseNotes";
|
||||||
|
import { getPlatformURL } from "./core/createClient";
|
||||||
|
|
||||||
const Performers = lazy(() => import("./components/Performers/Performers"));
|
const Performers = lazy(() => import("./components/Performers/Performers"));
|
||||||
const FrontPage = lazy(() => import("./components/FrontPage/FrontPage"));
|
const FrontPage = lazy(() => import("./components/FrontPage/FrontPage"));
|
||||||
@@ -87,11 +88,24 @@ export const App: React.FC = () => {
|
|||||||
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
const defaultMessages = (await locales[defaultMessageLanguage]()).default;
|
||||||
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
const mergedMessages = cloneDeep(Object.assign({}, defaultMessages));
|
||||||
const chosenMessages = (await locales[messageLanguage]()).default;
|
const chosenMessages = (await locales[messageLanguage]()).default;
|
||||||
mergeWith(mergedMessages, chosenMessages, (objVal, srcVal) => {
|
const res = await fetch(getPlatformURL() + "customlocales");
|
||||||
if (srcVal === "") {
|
let customMessages = {};
|
||||||
return objVal;
|
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));
|
setMessages(flattenMessages(mergedMessages));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -408,6 +408,36 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
<SettingSection headingID="config.ui.custom_locales.heading">
|
||||||
|
<BooleanSetting
|
||||||
|
id="custom-locales-enabled"
|
||||||
|
headingID="config.ui.custom_locales.option_label"
|
||||||
|
checked={iface.customLocalesEnabled ?? undefined}
|
||||||
|
onChange={(v) => saveInterface({ customLocalesEnabled: v })}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ModalSetting<string>
|
||||||
|
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) => (
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
value={value}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
setValue(e.currentTarget.value)
|
||||||
|
}
|
||||||
|
rows={16}
|
||||||
|
className="text-input code"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
renderValue={() => {
|
||||||
|
return <></>;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection headingID="config.ui.interactive_options">
|
<SettingSection headingID="config.ui.interactive_options">
|
||||||
<StringSetting
|
<StringSetting
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ After migrating, please run a scan on your entire library to populate missing da
|
|||||||
* Import/export schema has changed and is incompatible with the previous version.
|
* Import/export schema has changed and is incompatible with the previous version.
|
||||||
|
|
||||||
### ✨ New Features
|
### ✨ New Features
|
||||||
|
* Allow overriding UI localisation strings. ([#2837](https://github.com/stashapp/stash/pull/2837))
|
||||||
* Populate name from query field when creating new performer/studio/tag/gallery. ([#2701](https://github.com/stashapp/stash/pull/2701))
|
* Populate name from query field when creating new performer/studio/tag/gallery. ([#2701](https://github.com/stashapp/stash/pull/2701))
|
||||||
* Added support for identical files. Identical files are assigned to the same scene/gallery/image and can be viewed in File Info. ([#2676](https://github.com/stashapp/stash/pull/2676))
|
* Added support for identical files. Identical files are assigned to the same scene/gallery/image and can be viewed in File Info. ([#2676](https://github.com/stashapp/stash/pull/2676))
|
||||||
* Added support for filtering and sorting by file count. ([#2744](https://github.com/stashapp/stash/pull/2744))
|
* Added support for filtering and sorting by file count. ([#2744](https://github.com/stashapp/stash/pull/2744))
|
||||||
|
|||||||
@@ -26,6 +26,19 @@ The stash UI can be customised using custom CSS. See [here](https://github.com/s
|
|||||||
|
|
||||||
[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Theme-Plex) is a community created theme inspired by the popular Plex interface.
|
[Stash Plex Theme](https://github.com/stashapp/stash/wiki/Theme-Plex) is a community created theme inspired by the popular Plex interface.
|
||||||
|
|
||||||
|
## Custom Locales
|
||||||
|
|
||||||
|
The localisation strings can be customised. The master list of default (en-GB) locale strings can be found [here](https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json). The custom locale format is the same as this json file.
|
||||||
|
|
||||||
|
For example, to override the `actions.add_directory` label (which is `Add Directory` by default), you would have the following in the custom locale:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"actions": {
|
||||||
|
"add_directory": "Some other description"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Custom served folders
|
## Custom served folders
|
||||||
|
|
||||||
|
|||||||
@@ -427,6 +427,11 @@
|
|||||||
"heading": "Custom CSS",
|
"heading": "Custom CSS",
|
||||||
"option_label": "Custom CSS enabled"
|
"option_label": "Custom CSS enabled"
|
||||||
},
|
},
|
||||||
|
"custom_locales": {
|
||||||
|
"description": "Override individual locale strings. See https://github.com/stashapp/stash/blob/develop/ui/v2.5/src/locales/en-GB.json for the master list. Page must be reloaded for changes to take effect.",
|
||||||
|
"heading": "Custom localisation",
|
||||||
|
"option_label": "Custom localisation enabled"
|
||||||
|
},
|
||||||
"delete_options": {
|
"delete_options": {
|
||||||
"description": "Default settings when deleting images, galleries, and scenes.",
|
"description": "Default settings when deleting images, galleries, and scenes.",
|
||||||
"heading": "Delete Options",
|
"heading": "Delete Options",
|
||||||
|
|||||||
@@ -8,6 +8,12 @@
|
|||||||
"scene_filename_parser": {
|
"scene_filename_parser": {
|
||||||
"ignore_organized": "Ignore organized scenes"
|
"ignore_organized": "Ignore organized scenes"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"ui": {
|
||||||
|
"custom_locales": {
|
||||||
|
"heading": "Custom localization",
|
||||||
|
"option_label": "Custom localization enabled"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"performer_favorite": "Performer Favorited"
|
"performer_favorite": "Performer Favorited"
|
||||||
|
|||||||
Reference in New Issue
Block a user