From 3ae187e6f065328e37ef20b509e2c3e87feb4261 Mon Sep 17 00:00:00 2001 From: Still Hsu Date: Mon, 14 Jun 2021 14:48:59 +0900 Subject: [PATCH] Incorporate i18n into UI elements (#1471) * Update zh-tw string table (till 975343d2) * Prepare localization table * Implement i18n for Performers & Tags * Add "add" action strings * Use Lodash merge for deep merging language JSONs The original implementation does not properly merge language files, causing unexpected localization string fallback behavior. * Localize pagination strings * Use Field name value as null id fallback ...otherwise FormattedMessage is gonna throw when the ID is null * Use localized "Path" string for all instances * Localize the "Interface" tab under settings * Localize scene & performer cards * Rename locale folder for better compatibility with i18n-ally * Localize majority of the categories and features --- ui/v2.5/.vscode/settings.json | 9 +- ui/v2.5/src/App.tsx | 11 +- .../src/components/Changelog/versions/v080.md | 1 + .../Galleries/DeleteGalleriesDialog.tsx | 46 +- .../Galleries/EditGalleriesDialog.tsx | 41 +- .../Galleries/GalleryDetails/Gallery.tsx | 50 +- .../GalleryDetails/GalleryAddPanel.tsx | 17 +- .../GalleryDetails/GalleryEditPanel.tsx | 47 +- .../GalleryDetails/GalleryFileInfoPanel.tsx | 9 +- .../GalleryDetails/GalleryImagesPanel.tsx | 7 +- .../GalleryDetails/GalleryScrapeDialog.tsx | 46 +- .../src/components/Galleries/GalleryList.tsx | 14 +- .../components/Images/DeleteImagesDialog.tsx | 48 +- .../components/Images/EditImagesDialog.tsx | 39 +- .../components/Images/ImageDetails/Image.tsx | 31 +- .../Images/ImageDetails/ImageEditPanel.tsx | 23 +- .../ImageDetails/ImageFileInfoPanel.tsx | 10 +- ui/v2.5/src/components/Images/ImageList.tsx | 8 +- ui/v2.5/src/components/List/AddFilter.tsx | 16 +- ui/v2.5/src/components/List/ListFilter.tsx | 46 +- ui/v2.5/src/components/List/Pagination.tsx | 14 +- .../components/Movies/MovieDetails/Movie.tsx | 19 +- .../Movies/MovieDetails/MovieDetailsPanel.tsx | 20 +- .../Movies/MovieDetails/MovieEditPanel.tsx | 39 +- ui/v2.5/src/components/Movies/MovieList.tsx | 12 +- .../Performers/EditPerformersDialog.tsx | 26 +- .../components/Performers/PerformerCard.tsx | 16 +- .../Performers/PerformerDetails/Performer.tsx | 26 +- .../PerformerDetailsPanel.tsx | 46 +- .../PerformerDetails/PerformerEditPanel.tsx | 49 +- .../PerformerOperationsPanel.tsx | 7 +- .../PerformerScrapeDialog.tsx | 58 +- .../PerformerDetails/PerformerScrapeModal.tsx | 8 +- .../PerformerStashBoxModal.tsx | 8 +- .../components/Performers/PerformerList.tsx | 10 +- .../SceneDuplicateChecker.tsx | 67 +- .../SceneFilenameParser/ParserInput.tsx | 49 +- .../SceneFilenameParser.tsx | 53 +- .../SceneFilenameParser/ShowFields.tsx | 8 +- .../components/Scenes/DeleteScenesDialog.tsx | 48 +- .../components/Scenes/EditScenesDialog.tsx | 39 +- .../Scenes/SceneDetails/OCounterButton.tsx | 4 +- .../Scenes/SceneDetails/PrimaryTags.tsx | 3 +- .../components/Scenes/SceneDetails/Scene.tsx | 76 ++- .../Scenes/SceneDetails/SceneDetailPanel.tsx | 25 +- .../Scenes/SceneDetails/SceneEditPanel.tsx | 64 +- .../SceneDetails/SceneFileInfoPanel.tsx | 52 +- .../Scenes/SceneDetails/SceneMarkerForm.tsx | 5 +- .../Scenes/SceneDetails/SceneMarkersPanel.tsx | 5 +- .../Scenes/SceneDetails/SceneMovieTable.tsx | 6 +- .../Scenes/SceneDetails/SceneScrapeDialog.tsx | 30 +- .../SceneDetails/SceneVideoFilterPanel.tsx | 49 +- .../components/Scenes/SceneGenerateDialog.tsx | 91 ++- ui/v2.5/src/components/Scenes/SceneList.tsx | 12 +- .../src/components/Scenes/SceneListTable.tsx | 33 +- .../src/components/Scenes/SceneMarkerList.tsx | 4 +- ui/v2.5/src/components/Settings/Settings.tsx | 33 +- .../Settings/SettingsAboutPanel.tsx | 122 ++-- .../Settings/SettingsConfigurationPanel.tsx | 308 +++++++--- .../components/Settings/SettingsDLNAPanel.tsx | 95 ++- .../SettingsInterfacePanel.tsx | 96 ++- .../components/Settings/SettingsLogsPanel.tsx | 8 +- .../Settings/SettingsPluginsPanel.tsx | 19 +- .../Settings/SettingsScrapersPanel.tsx | 69 ++- .../DirectorySelectionDialog.tsx | 6 +- .../SettingsTasksPanel/GenerateButton.tsx | 26 +- .../SettingsTasksPanel/ImportDialog.tsx | 8 +- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 186 ++++-- .../Settings/SettingsToolsPanel.tsx | 13 +- .../Settings/StashBoxConfiguration.tsx | 35 +- .../Settings/StashConfiguration.tsx | 18 +- .../components/Shared/DeleteEntityDialog.tsx | 24 +- .../components/Shared/DetailsEditNavbar.tsx | 35 +- .../src/components/Shared/ExportDialog.tsx | 15 +- .../FolderSelect/FolderSelectDialog.tsx | 3 +- ui/v2.5/src/components/Shared/ImageInput.tsx | 14 +- ui/v2.5/src/components/Shared/Modal.tsx | 17 +- ui/v2.5/src/components/Shared/MultiSet.tsx | 14 +- .../src/components/Shared/ScrapeDialog.tsx | 10 +- ui/v2.5/src/components/Stats.tsx | 12 +- .../Studios/StudioDetails/Studio.tsx | 66 +- ui/v2.5/src/components/Studios/StudioList.tsx | 12 +- ui/v2.5/src/components/Tagger/Config.tsx | 96 ++- .../Tagger/PerformerFieldSelector.tsx | 4 +- .../src/components/Tagger/PerformerModal.tsx | 4 +- .../src/components/Tagger/PerformerResult.tsx | 11 +- .../components/Tagger/StashSearchResult.tsx | 30 +- .../src/components/Tagger/StudioResult.tsx | 29 +- ui/v2.5/src/components/Tagger/Tagger.tsx | 61 +- ui/v2.5/src/components/Tagger/constants.ts | 8 - .../Tagger/performers/PerformerTagger.tsx | 8 +- ui/v2.5/src/components/Tagger/utils.ts | 2 +- .../src/components/Tags/TagDetails/Tag.tsx | 38 +- .../Tags/TagDetails/TagDetailsPanel.tsx | 5 +- .../Tags/TagDetails/TagEditPanel.tsx | 15 +- ui/v2.5/src/components/Tags/TagList.tsx | 60 +- ui/v2.5/src/locale/en-GB.json | 86 --- ui/v2.5/src/locale/zh-TW.json | 20 - ui/v2.5/src/{locale => locales}/README.md | 0 ui/v2.5/src/locales/en-GB.json | 581 ++++++++++++++++++ ui/v2.5/src/{locale => locales}/en-US.json | 2 +- ui/v2.5/src/{locale => locales}/index.ts | 0 ui/v2.5/src/locales/zh-TW.json | 570 +++++++++++++++++ .../models/list-filter/criteria/criterion.ts | 11 +- ui/v2.5/src/utils/field.tsx | 20 +- 105 files changed, 3441 insertions(+), 1084 deletions(-) delete mode 100644 ui/v2.5/src/locale/en-GB.json delete mode 100644 ui/v2.5/src/locale/zh-TW.json rename ui/v2.5/src/{locale => locales}/README.md (100%) create mode 100644 ui/v2.5/src/locales/en-GB.json rename ui/v2.5/src/{locale => locales}/en-US.json (98%) rename ui/v2.5/src/{locale => locales}/index.ts (100%) create mode 100644 ui/v2.5/src/locales/zh-TW.json diff --git a/ui/v2.5/.vscode/settings.json b/ui/v2.5/.vscode/settings.json index ba34e28f0..5265cba16 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 31fab5283..fa506466b 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 b79f4ca3d..7fa18c524 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 59e8eabed..8ff227e5c 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 78ce68b60..9e0d98f28 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 f947bd658..1cb433028 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 = () => {