Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/components/services/content/ServiceView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import TopBarProgress from 'react-topbar-progress-indicator';
import { CUSTOM_WEBSITE_RECIPE_ID } from '../../../config';
import WebControlsScreen from '../../../features/webControls/containers/WebControlsScreen';
import { getLocalizedRecipeName } from '../../../helpers/recipe-helpers';
import type ServiceModel from '../../../models/Service';
import type { RealStores } from '../../../stores';
import MediaSource from '../../MediaSource';
Expand Down Expand Up @@ -198,7 +199,15 @@ class ServiceView extends Component<IProps, IState> {
<>
{service.isActive && (
<ServiceDisabled
name={service.name === '' ? service.recipe.name : service.name}
name={
service.name === ''
? getLocalizedRecipeName(
service.recipe.id,
service.recipe.name,
intl,
)
: service.name
}
// webview={service.webview} // TODO: [TECH DEBT][PROPS NOT EXIST IN COMPONENT] check it
enable={enable}
/>
Expand Down
13 changes: 11 additions & 2 deletions src/components/services/tabs/TabItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import injectSheet, { type WithStylesProps } from 'react-jss';
import { SortableElement } from 'react-sortable-hoc';
import type { Stores } from '../../../@types/stores.types';
import { altKey, cmdOrCtrlShortcutKey, shiftKey } from '../../../environment';
import { getLocalizedRecipeName } from '../../../helpers/recipe-helpers';
import globalMessages from '../../../i18n/globalMessages';
import type Service from '../../../models/Service';
import Icon from '../../ui/icon';
Expand Down Expand Up @@ -261,7 +262,9 @@ class TabItem extends Component<IProps, IState> {

const menuTemplate: MenuItemConstructorOptions[] = [
{
label: service.name || service.recipe.name,
label:
service.name ||
getLocalizedRecipeName(service.recipe.id, service.recipe.name, intl),
enabled: false,
},
{
Expand Down Expand Up @@ -341,7 +344,13 @@ class TabItem extends Component<IProps, IState> {
type: 'question',
message: intl.formatMessage(messages.deleteService),
detail: intl.formatMessage(messages.confirmDeleteService, {
serviceName: service.name || service.recipe.name,
serviceName:
service.name ||
getLocalizedRecipeName(
service.recipe.id,
service.recipe.name,
intl,
),
}),
buttons: [
intl.formatMessage(globalMessages.yes),
Expand Down
15 changes: 11 additions & 4 deletions src/components/settings/recipes/RecipeItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { observer } from 'mobx-react';
import { Component, MouseEventHandler } from 'react';
import {
type WrappedComponentProps,
injectIntl,
} from 'react-intl';
import { getLocalizedRecipeName } from '../../../helpers/recipe-helpers';
import RecipePreview from '../../../models/RecipePreview';

interface IProps {
interface IProps extends WrappedComponentProps {
recipe: RecipePreview;
onClick: MouseEventHandler<HTMLButtonElement>;
}
Expand All @@ -14,15 +19,17 @@ class RecipeItem extends Component<IProps> {
}

render() {
const { recipe, onClick } = this.props;
const { recipe, onClick, intl } = this.props;

return (
<button type="button" className="recipe-teaser" onClick={onClick}>
{recipe.isDevRecipe && (
<span className="recipe-teaser__dev-badge">dev</span>
)}
<img src={recipe.icons?.svg} className="recipe-teaser__icon" alt="" />
<span className="recipe-teaser__label">{recipe.name}</span>
<span className="recipe-teaser__label">
{getLocalizedRecipeName(recipe.id, recipe.name, intl)}
</span>
{recipe.aliases && recipe.aliases.length > 0 && (
<span className="recipe-teaser__alias_label">
{`Aliases: ${recipe.aliases.join(', ')}`}
Expand All @@ -33,4 +40,4 @@ class RecipeItem extends Component<IProps> {
}
}

export default RecipeItem;
export default injectIntl(RecipeItem);
27 changes: 21 additions & 6 deletions src/components/settings/services/EditServiceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from 'react-intl';
import { Link } from 'react-router-dom';
import { isMac } from '../../../environment';
import { getLocalizedRecipeName } from '../../../helpers/recipe-helpers';
import { normalizedUrl } from '../../../helpers/url-helpers';
import globalMessages from '../../../i18n/globalMessages';
import type Form from '../../../lib/Form';
Expand Down Expand Up @@ -263,7 +264,9 @@ class EditServiceForm extends Component<IProps, IState> {
type: 'question',
message: intl.formatMessage(messages.deleteService),
detail: intl.formatMessage(messages.confirmDeleteService, {
serviceName: service?.name || recipe.name,
serviceName:
service?.name ||
getLocalizedRecipeName(recipe.id, recipe.name, intl),
}),
buttons: [
intl.formatMessage(globalMessages.yes),
Expand Down Expand Up @@ -314,11 +317,13 @@ class EditServiceForm extends Component<IProps, IState> {
<span className="settings__header-item">
{action === 'add'
? intl.formatMessage(messages.addServiceHeadline, {
name: recipe.name,
name: getLocalizedRecipeName(recipe.id, recipe.name, intl),
})
: intl.formatMessage(messages.editServiceHeadline, {
name:
service && service.name !== '' ? service.name : recipe.name,
service && service.name !== ''
? service.name
: getLocalizedRecipeName(recipe.id, recipe.name, intl),
})}
</span>
</div>
Expand All @@ -330,9 +335,15 @@ class EditServiceForm extends Component<IProps, IState> {
{(recipe.hasTeamId || recipe.hasCustomUrl) && (
<Tabs active={activeTabIndex}>
{recipe.hasHostedOption && (
<TabItem title={recipe.name}>
<TabItem
title={getLocalizedRecipeName(recipe.id, recipe.name, intl)}
>
{intl.formatMessage(messages.useHostedService, {
name: recipe.name,
name: getLocalizedRecipeName(
recipe.id,
recipe.name,
intl,
),
})}
</TabItem>
)}
Expand All @@ -351,7 +362,11 @@ class EditServiceForm extends Component<IProps, IState> {
{form.error === 'url-validation-error' && (
<p className="franz-form__error">
{intl.formatMessage(messages.customUrlValidationError, {
name: recipe.name,
name: getLocalizedRecipeName(
recipe.id,
recipe.name,
intl,
),
})}
</p>
)}
Expand Down
9 changes: 8 additions & 1 deletion src/components/settings/services/ServiceItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
injectIntl,
} from 'react-intl';
import { Tooltip as ReactTooltip } from 'react-tooltip';
import { getLocalizedRecipeName } from '../../../helpers/recipe-helpers';
import type ServiceModel from '../../../models/Service';
import Icon from '../../ui/icon';

Expand Down Expand Up @@ -59,7 +60,13 @@ class ServiceItem extends Component<IProps> {
/>
</td>
<td className="service-table__column-name" onClick={goToServiceForm}>
{service.name === '' ? service.recipe.name : service.name}
{service.name === ''
? getLocalizedRecipeName(
service.recipe.id,
service.recipe.name,
intl,
)
: service.name}
</td>
<td className="service-table__column-info" onClick={goToServiceForm}>
{service.isMuted && (
Expand Down
2 changes: 1 addition & 1 deletion src/components/settings/settings/EditSettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1361,7 +1361,7 @@ class EditSettingsForm extends Component<IProps, IState> {
</p>
{noUpdateAvailable && (
<p>
{intl.formatMessage(messages.updateStatusUpToDate)}.
{intl.formatMessage(messages.updateStatusUpToDate)}
</p>
)}
{updateFailed && (
Expand Down
5 changes: 4 additions & 1 deletion src/containers/settings/EditServiceScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import withParams from '../../components/util/WithParams';
import { DEFAULT_APP_SETTINGS, DEFAULT_SERVICE_SETTINGS } from '../../config';
import { config as proxyFeature } from '../../features/serviceProxy';
import { getSelectOptions } from '../../helpers/i18n-helpers';
import { getLocalizedRecipeName } from '../../helpers/recipe-helpers';
import { url, oneRequired, required } from '../../helpers/validation-helpers';
import globalMessages from '../../i18n/globalMessages';
import { SPELLCHECKER_LOCALES } from '../../i18n/languages';
Expand Down Expand Up @@ -197,7 +198,9 @@ class EditServiceScreen extends Component<IProps> {
name: {
label: intl.formatMessage(messages.name),
placeholder: intl.formatMessage(messages.name),
value: service?.id ? service.name : recipe.name,
value: service?.id
? service.name
: getLocalizedRecipeName(recipe.id, recipe.name, intl),
},
isEnabled: {
label: intl.formatMessage(messages.enableService),
Expand Down
40 changes: 35 additions & 5 deletions src/enforce-macos-app-location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,43 @@
import { api } from './electron-util';
import { isMac } from './environment';
import { isDevMode } from './environment-remote';
import { getTranslatedText } from './helpers/i18n-helpers';
import Settings from './electron/Settings';
import { DEFAULT_APP_SETTINGS } from './config';

export default function enforceMacOSAppLocation(): void {
if (isDevMode || !isMac || api.app.isInApplicationsFolder()) {
return;
}

// Get current locale from settings
const settings = new Settings('app', DEFAULT_APP_SETTINGS);
const locale = settings.get('locale') || 'en-US';

const clickedButtonIndex = api.dialog.showMessageBoxSync({
type: 'error',
message: 'Move to Applications folder?',
detail:
message: getTranslatedText(
locale,
'enforceMacOSAppLocation.message',
'Move to Applications folder?',
),
detail: getTranslatedText(
locale,
'enforceMacOSAppLocation.detail',
'Ferdium must live in the Applications folder to be able to run correctly.',
buttons: ['Move to Applications folder', 'Quit Ferdium'],
),
buttons: [
getTranslatedText(
locale,
'enforceMacOSAppLocation.moveButton',
'Move to Applications folder',
),
getTranslatedText(
locale,
'enforceMacOSAppLocation.quitButton',
'Quit Ferdium',
),
],
defaultId: 0,
cancelId: 1,
});
Expand All @@ -30,9 +55,14 @@ export default function enforceMacOSAppLocation(): void {
// Can't replace the active version of the app
api.dialog.showMessageBoxSync({
type: 'error',
message:
message: getTranslatedText(
locale,
'enforceMacOSAppLocation.conflictMessage',
'Another version of Ferdium is currently running. Quit it, then launch this version of the app again.',
buttons: ['OK'],
),
buttons: [
getTranslatedText(locale, 'enforceMacOSAppLocation.okButton', 'OK'),
],
});

api.app.quit();
Expand Down
44 changes: 44 additions & 0 deletions src/helpers/recipe-helpers.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable global-require */
import { parse } from 'node:path';
import type { IntlShape } from 'react-intl';
import { userDataRecipesPath } from '../environment-remote';
import { getTranslatedText } from './i18n-helpers';

export const getRecipeDirectory = (id: string = ''): string => {
return userDataRecipesPath(id);
Expand Down Expand Up @@ -31,4 +33,46 @@ export const loadRecipeConfig = (recipeId: string) => {
}
};

/**
* Get localized recipe name for display
* Supports both React components (using IntlShape) and non-React components (using locale string)
*
* Translation key naming convention: `recipe.{recipeId}.name`
* - The function automatically constructs the translation key from the recipeId
* - If a translation exists, it will be used; otherwise, falls back to recipeName
* - No source code changes needed to add i18n support for new recipes
*
* Example: To add i18n support for 'whatsapp' recipe:
* 1. Add to en-US.json: "recipe.whatsapp.name": "WhatsApp"
* 2. Add to zh-HANS.json: "recipe.whatsapp.name": "WhatsApp"
* 3. No code changes required!
*
* @param recipeId - The recipe ID (e.g., 'franz-custom-website', 'whatsapp')
* @param recipeName - The fallback recipe name from package.json
* @param localeOrIntl - Either a locale string (e.g., 'en-US') or an IntlShape object from react-intl
* @returns Localized recipe name if translation exists, otherwise returns recipeName
*/
export const getLocalizedRecipeName = (
recipeId: string,
recipeName: string,
localeOrIntl: string | IntlShape,
): string => {
// Construct translation key following the convention: recipe.{recipeId}.name
const translationKey = `recipe.${recipeId}.name`;

// If localeOrIntl is an IntlShape object (has formatMessage method)
if (typeof localeOrIntl === 'object' && 'formatMessage' in localeOrIntl) {
return localeOrIntl.formatMessage(
{
id: translationKey,
defaultMessage: recipeName,
},
{},
);
}

// If localeOrIntl is a locale string
return getTranslatedText(localeOrIntl as string, translationKey, recipeName);
};

module.paths.unshift(getDevRecipeDirectory(), getRecipeDirectory());
Loading