From 9c5b996b093d5ae2058ec97897dfda1b3f2143f4 Mon Sep 17 00:00:00 2001 From: Jan Cizmar Date: Wed, 4 Sep 2024 13:57:57 +0200 Subject: [PATCH] fix: Show plural all exact plural forms in the editor --- e2e/cypress/e2e/translations/plurals.cy.ts | 23 ++++++++- .../translations/TranslationEditor.tsx | 2 + .../TranslationsTable/TranslationWrite.tsx | 3 +- .../translationVisual/PluralEditor.tsx | 14 ++++++ .../translationVisual/TranslationPlurals.tsx | 47 +++++++++++++------ .../translations/useTranslationCell.ts | 30 ++++++++++-- 6 files changed, 97 insertions(+), 22 deletions(-) diff --git a/e2e/cypress/e2e/translations/plurals.cy.ts b/e2e/cypress/e2e/translations/plurals.cy.ts index a6eaff5036..94e8fda137 100644 --- a/e2e/cypress/e2e/translations/plurals.cy.ts +++ b/e2e/cypress/e2e/translations/plurals.cy.ts @@ -8,7 +8,7 @@ import { } from '../../common/translations'; import { waitForGlobalLoading } from '../../common/loading'; import { createKey, deleteProject } from '../../common/apiCalls/common'; -import { confirmStandard } from '../../common/shared'; +import { confirmStandard, gcyAdvanced } from '../../common/shared'; describe('Translations Base', () => { let project: ProjectDTO = null; @@ -60,6 +60,27 @@ describe('Translations Base', () => { .should('be.visible'); }); + it.only('shows base and existing exact forms', () => { + createKey( + project.id, + 'Test key', + { + en: 'You have {testValue, plural, one {# item} =2 {Two items} other {# items}}', + cs: 'Máte {testValue, plural, one {# položku} =4 {# položky } few {# položky} other {# položek}}', + }, + { isPlural: true } + ); + visitTranslations(project.id); + waitForGlobalLoading(); + getTranslationCell('Test key', 'cs').click(); + gcyAdvanced({ value: 'translation-editor', variant: '=2' }).should( + 'be.visible' + ); + gcyAdvanced({ value: 'translation-editor', variant: '=4' }).should( + 'be.visible' + ); + }); + it('will change plural parameter name for all translations', () => { createKey( project.id, diff --git a/webapp/src/views/projects/translations/TranslationEditor.tsx b/webapp/src/views/projects/translations/TranslationEditor.tsx index 5356e829df..8bde1efe13 100644 --- a/webapp/src/views/projects/translations/TranslationEditor.tsx +++ b/webapp/src/views/projects/translations/TranslationEditor.tsx @@ -18,6 +18,7 @@ export const TranslationEditor = ({ mode, tools, editorRef }: Props) => { handleSave, handleClose, handleInsertBase, + baseValue, } = tools; return ( @@ -30,6 +31,7 @@ export const TranslationEditor = ({ mode, tools, editorRef }: Props) => { autofocus={true} activeEditorRef={editorRef} mode={mode} + baseValue={baseValue} editorProps={{ shortcuts: [ { key: 'Escape', run: () => (handleClose(true), true) }, diff --git a/webapp/src/views/projects/translations/TranslationsTable/TranslationWrite.tsx b/webapp/src/views/projects/translations/TranslationsTable/TranslationWrite.tsx index b63776a10e..3449905969 100644 --- a/webapp/src/views/projects/translations/TranslationsTable/TranslationWrite.tsx +++ b/webapp/src/views/projects/translations/TranslationsTable/TranslationWrite.tsx @@ -56,6 +56,7 @@ export const TranslationWrite: React.FC = ({ tools }) => { handleInsertBase, editEnabled, disabled, + baseText, } = tools; const editVal = tools.editVal!; const state = translation?.state || 'UNTRANSLATED'; @@ -68,7 +69,7 @@ export const TranslationWrite: React.FC = ({ tools }) => { const baseTranslation = useBaseTranslation( activeVariant, - keyData.translations[baseLanguage]?.text, + baseText, keyData.keyIsPlural ); diff --git a/webapp/src/views/projects/translations/translationVisual/PluralEditor.tsx b/webapp/src/views/projects/translations/translationVisual/PluralEditor.tsx index 56478a06aa..e49a94e4fa 100644 --- a/webapp/src/views/projects/translations/translationVisual/PluralEditor.tsx +++ b/webapp/src/views/projects/translations/translationVisual/PluralEditor.tsx @@ -17,6 +17,7 @@ type Props = { autofocus?: boolean; activeEditorRef?: RefObject; mode: 'placeholders' | 'syntax'; + baseValue?: TolgeeFormat; }; export const PluralEditor = ({ @@ -29,6 +30,7 @@ export const PluralEditor = ({ activeEditorRef, editorProps, mode, + baseValue, }: Props) => { function handleChange(text: string, variant: string) { onChange?.({ ...value, variants: { ...value.variants, [variant]: text } }); @@ -38,6 +40,17 @@ export const PluralEditor = ({ const editorMode = project.icuPlaceholders ? mode : 'plain'; + function getExactForms() { + if (!baseValue) { + return []; + } + return Object.keys(baseValue.variants) + .filter((key) => /^=\d+(\.\d+)?$/.test(key)) + .map((key) => parseFloat(key.substring(1))); + } + + const exactForms = getExactForms(); + return ( { const variantOrOther = variant || 'other'; return ( diff --git a/webapp/src/views/projects/translations/translationVisual/TranslationPlurals.tsx b/webapp/src/views/projects/translations/translationVisual/TranslationPlurals.tsx index 0fce5348ed..94c35ddb35 100644 --- a/webapp/src/views/projects/translations/translationVisual/TranslationPlurals.tsx +++ b/webapp/src/views/projects/translations/translationVisual/TranslationPlurals.tsx @@ -1,10 +1,9 @@ -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { styled } from '@mui/material'; -import React from 'react'; import { - TolgeeFormat, getPluralVariants, getVariantExample, + TolgeeFormat, } from '@tginternal/editor'; const StyledContainer = styled('div')` @@ -67,6 +66,7 @@ type Props = { showEmpty?: boolean; activeVariant?: string; variantPaddingTop?: number | string; + exactForms?: number[]; }; export const TranslationPlurals = ({ @@ -76,19 +76,12 @@ export const TranslationPlurals = ({ showEmpty, activeVariant, variantPaddingTop, + exactForms, }: Props) => { - const variants = useMemo(() => { - const existing = new Set(Object.keys(value.variants)); - const required = getPluralVariants(locale); - required.forEach((val) => existing.delete(val)); - const result = Array.from(existing).map((value) => { - return [value, getVariantExample(locale, value)] as const; - }); - required.forEach((value) => { - result.push([value, getVariantExample(locale, value)]); - }); - return result; - }, [locale]); + const variants = useMemo( + () => getForms(locale, value, exactForms), + [locale, exactForms, value] + ); if (value.parameter) { return ( @@ -137,3 +130,27 @@ export const TranslationPlurals = ({ ); }; + +function getForms(locale: string, value: TolgeeFormat, exactForms?: number[]) { + const forms: Set = new Set(); + getPluralVariants(locale).forEach((value) => forms.add(value)); + Object.keys(value.variants).forEach((value) => forms.add(value)); + (exactForms || []) + .map((value) => `=${value.toString()}`) + .forEach((value) => forms.add(value)); + + const formsArray = sortExactForms(forms); + + return formsArray.map((value) => { + return [value, getVariantExample(locale, value)] as const; + }); +} + +function sortExactForms(forms: Set) { + return [...forms].sort((a, b) => { + if (a.startsWith('=') && b.startsWith('=')) { + return Number(a.substring(1)) - Number(b.substring(1)); + } + return 0; + }); +} diff --git a/webapp/src/views/projects/translations/useTranslationCell.ts b/webapp/src/views/projects/translations/useTranslationCell.ts index 4095ff49d5..ecd6260307 100644 --- a/webapp/src/views/projects/translations/useTranslationCell.ts +++ b/webapp/src/views/projects/translations/useTranslationCell.ts @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { getTolgeeFormat } from '@tginternal/editor'; import { TRANSLATION_STATES } from 'tg.constants/translationStates'; @@ -6,8 +6,8 @@ import { useProjectPermissions } from 'tg.hooks/useProjectPermissions'; import { components } from 'tg.service/apiSchema.generated'; import { - useTranslationsSelector, useTranslationsActions, + useTranslationsSelector, } from './context/TranslationsContext'; import { AfterCommand, @@ -88,13 +88,23 @@ export const useTranslationCell = ({ }); }; + const getBaseText = () => { + if (!baseLanguage) { + return undefined; + } + + return keyData.translations[baseLanguage.tag].text; + }; + + const baseText = getBaseText(); + const handleInsertBase = () => { - if (!baseLanguage?.tag) { + const baseText = getBaseText(); + + if (!baseText) { return; } - const baseText = keyData.translations[baseLanguage.tag].text; - let baseVariant: string | undefined; if (cursor?.activeVariant) { const variants = getTolgeeFormat( @@ -112,6 +122,14 @@ export const useTranslationCell = ({ } }; + const baseValue = useMemo(() => { + return getTolgeeFormat( + getBaseText() || '', + keyData.keyIsPlural, + !project.icuPlaceholders + ); + }, [baseText, keyData.keyIsPlural]); + const handleClose = (force = false) => { if (force) { setEditForce(undefined); @@ -171,5 +189,7 @@ export const useTranslationCell = ({ editEnabled, translation, disabled, + baseValue, + baseText, }; };