diff --git a/ui/v2.5/.vscode/settings.json b/ui/v2.5/.vscode/settings.json index ba34e28f041..5265cba166c 100644 --- a/ui/v2.5/.vscode/settings.json +++ b/ui/v2.5/.vscode/settings.json @@ -6,5 +6,12 @@ "javascript.preferences.importModuleSpecifier": "relative", "typescript.preferences.importModuleSpecifier": "relative", "editor.wordWrapColumn": 120, - "editor.rulers": [120] + "editor.rulers": [ + 120 + ], + "i18n-ally.localesPaths": [ + "src/locales" + ], + "i18n-ally.keystyle": "nested", + "i18n-ally.sourceLanguage": "en-GB" } \ No newline at end of file diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index 31fab528394..fa506466b8c 100755 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -1,13 +1,14 @@ import React, { useEffect } from "react"; import { Route, Switch, useRouteMatch } from "react-router-dom"; import { IntlProvider } from "react-intl"; +import { merge } from "lodash"; import { ToastProvider } from "src/hooks/Toast"; import LightboxProvider from "src/hooks/Lightbox/context"; import { library } from "@fortawesome/fontawesome-svg-core"; import { fas } from "@fortawesome/free-solid-svg-icons"; import { initPolyfills } from "src/polyfills"; -import locales from "src/locale"; +import locales from "src/locales"; import { useConfiguration, useSystemStatus } from "src/core/StashService"; import { flattenMessages } from "src/utils"; import Mousetrap from "mousetrap"; @@ -58,12 +59,12 @@ export const App: React.FC = () => { const messageLanguage = languageMessageString(language); // use en-GB as default messages if any messages aren't found in the chosen language - const mergedMessages = { + const mergedMessages = merge( // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(locales as any)[defaultMessageLanguage], + (locales as any)[defaultMessageLanguage], // eslint-disable-next-line @typescript-eslint/no-explicit-any - ...(locales as any)[messageLanguage], - }; + (locales as any)[messageLanguage] + ); const messages = flattenMessages(mergedMessages); const setupMatch = useRouteMatch(["/setup", "/migrate"]); diff --git a/ui/v2.5/src/components/Changelog/versions/v080.md b/ui/v2.5/src/components/Changelog/versions/v080.md index b79f4ca3dfe..7fa18c52469 100644 --- a/ui/v2.5/src/components/Changelog/versions/v080.md +++ b/ui/v2.5/src/components/Changelog/versions/v080.md @@ -9,6 +9,7 @@ * Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364)) ### 🎨 Improvements +* Added internationalisation for all UI pages and added zh-TW language option. ([#1471](https://github.com/stashapp/stash/pull/1471)) * Add option to disable audio for generated previews. ([#1454](https://github.com/stashapp/stash/pull/1454)) * Prompt when leaving scene edit page with unsaved changes. ([#1429](https://github.com/stashapp/stash/pull/1429)) * Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435)) diff --git a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx index 59e8eabedf9..8ff227e5c8b 100644 --- a/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/DeleteGalleriesDialog.tsx @@ -4,7 +4,7 @@ import { useGalleryDestroy } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; import { Modal } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { FormattedMessage } from "react-intl"; +import { useIntl } from "react-intl"; interface IDeleteGalleryDialogProps { selected: Pick[]; @@ -14,20 +14,22 @@ interface IDeleteGalleryDialogProps { export const DeleteGalleriesDialog: React.FC = ( props: IDeleteGalleryDialogProps ) => { - const plural = props.selected.length > 1; + const intl = useIntl(); + const singularEntity = intl.formatMessage({ id: "gallery" }); + const pluralEntity = intl.formatMessage({ id: "galleries" }); - const singleMessageId = "deleteGalleryText"; - const pluralMessageId = "deleteGallerysText"; - - const singleMessage = - "Are you sure you want to delete this gallery? Galleries for zip files will be re-added during the next scan unless the zip file is also deleted."; - const pluralMessage = - "Are you sure you want to delete these galleries? Galleries for zip files will be re-added during the next scan unless the zip files are also deleted."; - - const header = plural ? "Delete Galleries" : "Delete Gallery"; - const toastMessage = plural ? "Deleted galleries" : "Deleted gallery"; - const messageId = plural ? pluralMessageId : singleMessageId; - const message = plural ? pluralMessage : singleMessage; + const header = intl.formatMessage( + { id: "dialogs.delete_entity_title" }, + { count: props.selected.length, singularEntity, pluralEntity } + ); + const toastMessage = intl.formatMessage( + { id: "toast.delete_entity" }, + { count: props.selected.length, singularEntity, pluralEntity } + ); + const message = intl.formatMessage( + { id: "dialogs.delete_entity_desc" }, + { count: props.selected.length, singularEntity, pluralEntity } + ); const [deleteFile, setDeleteFile] = useState(false); const [deleteGenerated, setDeleteGenerated] = useState(true); @@ -63,17 +65,19 @@ export const DeleteGalleriesDialog: React.FC = ( show icon="trash-alt" header={header} - accept={{ variant: "danger", onClick: onDelete, text: "Delete" }} + accept={{ + variant: "danger", + onClick: onDelete, + text: intl.formatMessage({ id: "actions.delete" }), + }} cancel={{ onClick: () => props.onClose(false), - text: "Cancel", + text: intl.formatMessage({ id: "actions.cancel" }), variant: "secondary", }} isRunning={isDeleting} > -

- -

+

{message}

= ( setDeleteGenerated(!deleteGenerated)} /> diff --git a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx index 78ce68b6087..9e0d98f28bd 100644 --- a/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx +++ b/ui/v2.5/src/components/Galleries/EditGalleriesDialog.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useState } from "react"; import { Form, Col, Row } from "react-bootstrap"; +import { FormattedMessage, useIntl } from "react-intl"; import _ from "lodash"; import { useBulkGalleryUpdate } from "src/core/StashService"; import * as GQL from "src/core/generated-graphql"; @@ -17,6 +18,7 @@ interface IListOperationProps { export const EditGalleriesDialog: React.FC = ( props: IListOperationProps ) => { + const intl = useIntl(); const Toast = useToast(); const [rating, setRating] = useState(); const [studioId, setStudioId] = useState(); @@ -138,7 +140,14 @@ export const EditGalleriesDialog: React.FC = ( input: getGalleryInput(), }, }); - Toast.success({ content: "Updated galleries" }); + Toast.success({ + content: intl.formatMessage( + { id: "toast.updated_entity" }, + { + entity: intl.formatMessage({ id: "galleries" }).toLocaleLowerCase(), + } + ), + }); props.onClose(true); } catch (e) { Toast.error(e); @@ -347,11 +356,21 @@ export const EditGalleriesDialog: React.FC = ( props.onClose(false), - text: "Cancel", + text: intl.formatMessage({ id: "actions.cancel" }), variant: "secondary", }} isRunning={isUpdating} @@ -359,7 +378,7 @@ export const EditGalleriesDialog: React.FC = (
{FormUtils.renderLabel({ - title: "Rating", + title: intl.formatMessage({ id: "rating" }), })} = ( {FormUtils.renderLabel({ - title: "Studio", + title: intl.formatMessage({ id: "studio" }), })} = ( - Performers + + + {renderMultiSelect("performers", performerIds)} - Tags + + + {renderMultiSelect("tags", tagIds)} cycleOrganized()} diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index f947bd658ae..1cb43302825 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -1,6 +1,7 @@ import { Tab, Nav, Dropdown } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory, Link } from "react-router-dom"; +import { FormattedMessage, useIntl } from "react-intl"; import { mutateMetadataScan, useFindGallery, @@ -28,6 +29,7 @@ export const Gallery: React.FC = () => { const { tab = "images", id = "new" } = useParams(); const history = useHistory(); const Toast = useToast(); + const intl = useIntl(); const isNew = id === "new"; const { data, error, loading } = useFindGallery(id); @@ -73,7 +75,15 @@ export const Gallery: React.FC = () => { paths: [gallery.path], }); - Toast.success({ content: "Rescanning image" }); + Toast.success({ + content: intl.formatMessage( + { id: "toast.rescanning_entity" }, + { + count: 1, + singularEntity: intl.formatMessage({ id: "gallery" }), + } + ), + }); } const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); @@ -103,7 +113,7 @@ export const Gallery: React.FC = () => { variant="secondary" id="operation-menu" className="minimal" - title="Operations" + title={intl.formatMessage({ id: "operations" })} > @@ -114,7 +124,7 @@ export const Gallery: React.FC = () => { className="bg-secondary text-white" onClick={() => onRescan()} > - Rescan + ) : undefined} { className="bg-secondary text-white" onClick={() => setIsDeleteAlertOpen(true)} > - Delete Gallery + @@ -142,22 +155,28 @@ export const Gallery: React.FC = () => {