Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
16 changes: 10 additions & 6 deletions client/modules/IDE/actions/project.js
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ export function deleteProject(id) {
});
};
}
export function changeVisibility(projectId, projectName, visibility) {
export function changeVisibility(projectId, projectName, visibility, t) {
return (dispatch, getState) => {
const state = getState();

Expand Down Expand Up @@ -443,11 +443,15 @@ export function changeVisibility(projectId, projectName, visibility) {
name: response.data.name
});

dispatch(
setToastText(
`${projectName} is now ${newVisibility.toLowerCase()}`
)
);
const visibilityToastText = t('Visibility.Changed', {
projectName,
newVisibility:
newVisibility === 'Public'
? t('Visibility.Public.Label').toLowerCase()
: t('Visibility.Private.Label').toLowerCase()
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could also be written as:

const visibilityToastText = t('Visibility.Changed', {
  projectName,
  newVisibility: t(`Visibility.${newVisibility}.Label`).toLowerCase()
});

but as a general rule of thumb introducing locale entries in the source code containing interpolated strings SHOULD BE AVOIDED, since this makes finding unused translations later much harder.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could see this being adjusted at some point, since we plan to add more visibility options in the future. Maybe there's a way to utilize some kind of dynamic key here?

Copy link
Contributor Author

@cassiano cassiano Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@raclim, I have updated the code to a much more flexible switch statement, as shown below:

let visibilityLabel;

switch (newVisibility) {
  case 'Public':
    visibilityLabel = t('Visibility.Public.Label');
    break;
  case 'Private':
    visibilityLabel = t('Visibility.Private.Label');
    break;
  default:
    visibilityLabel = newVisibility;
}

const visibilityToastText = t('Visibility.Changed', {
  projectName,
  newVisibility: visibilityLabel.toLowerCase()
});

It now becomes much easier to add more visibility options in the future, e.g. a 'Protected' one:

switch (newVisibility) {
  case 'Public':
    visibilityLabel = t('Visibility.Public.Label');
    break;
  case 'Private':
    visibilityLabel = t('Visibility.Private.Label');
    break;
  case 'Protected':
    visibilityLabel = t('Visibility.Protected.Label');
    break;
  default:
    visibilityLabel = newVisibility;
}

When using TypeScript (hopefully soon), my suggestion is to futurely create a TS union type containing all visibility options ('Public', 'Private', 'Protected' etc) and then use a technique known as "exhaustiveness checking" (https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking) in order to make sure all options were properly considered in the switch statement (you will get a compilation error if any options are missing, which is really cool). Something like:

// In some other module.
type VisibilityOptions = 'Public' | 'Private' | 'Protected'

switch (newVisibility) {  // `newVisibility` will be of type `VisibilityOptions`
  case 'Public':
    visibilityLabel = t('Visibility.Public.Label');
    break;
  case 'Private':
    visibilityLabel = t('Visibility.Private.Label');
    break;
  case 'Protected':
    visibilityLabel = t('Visibility.Protected.Label');
    break;
  default: {
    const exhaustiveCheck: never = newVisibility
    throw new Error(`Unhandled case: ${exhaustiveCheck}`);
  }
}

Copy link
Contributor Author

@cassiano cassiano Sep 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follows another good example where this technique could be used which is, according to my analysis, the only case of a "dynamic" locale key in your entire codebase:

export const SkipLink = ({ targetId, text }: SkipLinkProps) => {
  // ...
  return (
    <a
      href={`#${targetId}`}
      className={linkClasses}
      onFocus={handleFocus}
      onBlur={handleBlur}
    >
      {t(`SkipLink.${text}`)}
    </a>
  );
};

Because, like I mentioned before, interpolating text in order to produce locale keys makes finding them later in the source code an almost impossible task, be it manually or using some automated tool/script.

My recommendation would be to replace the above code with a switch statement, ideally using a union type and the "exhaustiveness checking" technique, as shown below:

type SkipLinkOptions = 'PlaySketch';  // More could be added later, e.g. 'PlaySketch' | 'StopSketch'

interface SkipLinkProps {
  targetId: string;
  text: SkipLinkOptions;
}

export const SkipLink = ({ targetId, text }: SkipLinkProps) => {
  const [focus, setFocus] = useState(false);
  const { t } = useTranslation();
  const handleFocus = () => {
    setFocus(true);
  };

  const handleBlur = () => {
    setFocus(false);
  };
  const linkClasses = classNames('skip_link', { focus });

  let translatedText: string;

  switch (text) {
    case 'PlaySketch':
      translatedText = t('SkipLink.PlaySketch');
      break;
    default: {
      const exhaustiveCheck: never = text;
      throw new Error(`Unhandled case: ${exhaustiveCheck}`);
    }
  }

  return (
    <a
      href={`#${targetId}`}
      className={linkClasses}
      onFocus={handleFocus}
      onBlur={handleBlur}
    >
      {translatedText}
    </a>
  );
};

So, it any new options are added later, but no translations are provided, TS will catch it at compile time:

image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PS: the above SkipLink component is already located in a .tsx file, so if desired I could easily include the proposed changes in this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks so much for the detailed explanation, I'm still relatively new to TypeScript myself, so this was really helpful! I agree that using union types with exhaustiveness checking would be a great improvement with i18n. I think it could make sense as a follow-up PR if that sounds good!


dispatch(setToastText(visibilityToastText));
dispatch(showToast(2000));
}
}
Expand Down
3 changes: 1 addition & 2 deletions client/modules/IDE/components/Header/Toolbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ const Toolbar = (props) => {
const dispatch = useDispatch();
const { t } = useTranslation();
const userIsOwner = user?.username === project.owner?.username;

const showVisibilityDropdown = project?.owner && userIsOwner;

const playButtonClass = classNames({
Expand All @@ -51,7 +50,7 @@ const Toolbar = (props) => {

const handleVisibilityChange = useCallback(
(sketchId, sketchName, newVisibility) => {
dispatch(changeVisibility(sketchId, sketchName, newVisibility));
dispatch(changeVisibility(sketchId, sketchName, newVisibility, t));
},
[changeVisibility]
);
Expand Down
2 changes: 1 addition & 1 deletion client/modules/IDE/components/SketchListRowBase.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ const SketchListRowBase = ({

const handleVisibilityChange = useCallback(
(sketchId, sketchName, newVisibility) => {
changeVisibility(sketchId, sketchName, newVisibility);
changeVisibility(sketchId, sketchName, newVisibility, t);
},
[changeVisibility]
);
Expand Down
7 changes: 4 additions & 3 deletions translations/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
"Reference": "Reference",
"About": "About",
"ReportBug": "Report a Bug",
"ChatOnDiscord":"Chat On Discord",
"PostOnTheForum":"Post on the Forum"
"ChatOnDiscord": "Chat On Discord",
"PostOnTheForum": "Post on the Forum"
Copy link
Contributor Author

@cassiano cassiano Sep 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice the missing spaces before the Object values, which were re-introduced automatically by Prettier.

},
"Lang": "Language",
"BackEditor": "Back to Editor",
Expand Down Expand Up @@ -690,6 +690,7 @@
"Private": {
"Description": "Only you can see this sketch.",
"Label": "Private"
}
},
"Changed": "'{{projectName}}' is now {{newVisibility}}..."
}
}
10 changes: 7 additions & 3 deletions translations/locales/pt-BR/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@
"Title": "Ajuda",
"KeyboardShortcuts": "Atalhos de Teclado",
"Reference": "Referência",
"About": "Sobre"
"About": "Sobre",
"ReportBug": "Reportar um Bug",
"ChatOnDiscord": "Conversar no Discord",
"PostOnTheForum": "Postar no Fórum"
},
"BackEditor": "Voltar ao Editor",
"WarningUnsavedChanges": "Realmente quer sair da página? Há mudanças não salvas.",
Expand Down Expand Up @@ -208,7 +211,7 @@
"AutocompleteHinterOnARIA": "Geração de dicas do Autocompleter ativada",
"AutocompleteHinterOffARIA": "Geração de dicas do Autocompleter desativada",
"LibraryVersion": "Versão do p5.js",
"LibraryVersionInfo": "Há uma [nova versão 2.0](https://github.com/processing/p5.js/releases/) do p5.js disponível! Ela se tornará padrão em agosto de 2026, então aproveite este tempo para testá-la e relatar bugs. Está interessado em transitar esboços de 1.x para 2.0? Confira os [recursos de compatibilidade e transição.](https://github.com/processing/p5.js-compatibility)",
"LibraryVersionInfo": "Há uma [nova versão 2.0](https://github.com/processing/p5.js/releases/) do p5.js disponível! Ela se tornará padrão em agosto de 2026, então aproveite este tempo para testá-la e relatar bugs. Está interessado em migrar esboços de 1.x para 2.0? Confira os [recursos de compatibilidade e migração.](https://github.com/processing/p5.js-compatibility)",
"CustomVersionTitle": "Gerenciando suas próprias bibliotecas? Legal!",
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have used the proper pt-BR verb here ("migrar" instead of "transitar").

"CustomVersionInfo": "A versão do p5.js está atualmente sendo gerenciada no código do index.html. Isso significa que não pode ser ajustada a partir desta aba.",
"CustomVersionReset": "Se você gostaria de usar as bibliotecas padrão, pode substituir as tags de script no index.html pelo seguinte:",
Expand Down Expand Up @@ -621,6 +624,7 @@
"Private": {
"Description": "Apenas você pode ver este esboço.",
"Label": "Privado"
}
},
"Changed": "'{{projectName}}' agora é {{newVisibility}}..."
}
}
Loading