diff --git a/cli/src/commands/stats.ts b/cli/src/commands/stats.ts index c22480337..21f6d54c9 100644 --- a/cli/src/commands/stats.ts +++ b/cli/src/commands/stats.ts @@ -23,6 +23,9 @@ export default class Stats extends Command { 'check-reviewed': flags.boolean({ description: 'Exit 1 when reviewed percentage is not 100%', }), + 'check-translated': flags.boolean({ + description: 'Exit 1 when translated percentage is not 100%', + }), config: configFlag, }; @@ -65,5 +68,26 @@ export default class Stats extends Command { ); } } + + if (flags['check-translated']) { + const translatedCount = this.project!.revisions.reduce( + (memo, revision: Revision) => memo + revision.translatedCount, + 0 + ); + const translationsCount = this.project!.revisions.reduce( + (memo, revision: Revision) => memo + revision.translationsCount, + 0 + ); + + if (translationsCount - translatedCount !== 0) { + const versionFormat = flags.version ? ` ${flags.version}` : ''; + throw new CLIError( + chalk.red( + `Project${versionFormat} has ${translatedCount} strings to be translated` + ), + {exit: 1} + ); + } + } } } diff --git a/cli/src/services/formatters/project-stats.ts b/cli/src/services/formatters/project-stats.ts index a3f83d29c..4c122c66e 100644 --- a/cli/src/services/formatters/project-stats.ts +++ b/cli/src/services/formatters/project-stats.ts @@ -62,6 +62,10 @@ export default class ProjectStatsFormatter extends Base { (memo, revision: Revision) => memo + revision.translationsCount, 0 ); + const translatedCount = this.project.revisions.reduce( + (memo, revision: Revision) => memo + revision.translatedCount, + 0 + ); const conflictsCount = this.project.revisions.reduce( (memo, revision: Revision) => memo + revision.conflictsCount, 0 @@ -79,10 +83,8 @@ export default class ProjectStatsFormatter extends Base { ); console.log( - this.project.logo - ? this.project.logo - : chalk.bgGreenBright.black.bold(' ^ '), - chalk.white.bold(this.project.name), + this.project.logo ? this.project.logo : chalk.bgGreen.black.bold(' ^ '), + chalk.whiteBright.bold(this.project.name), chalk.dim(' • '), percentageReviewedFormat ); @@ -167,6 +169,10 @@ export default class ProjectStatsFormatter extends Base { console.log(chalk.magenta(`Strings (${translationsCount})`)); console.log(chalk.green('✓ Reviewed:'), chalk.green(`${reviewedCount}`)); console.log(chalk.red('× In review:'), chalk.red(`${conflictsCount}`)); + console.log( + chalk.white('✎ Translated:'), + chalk.white(`${translatedCount}`) + ); console.log(''); const owners = this.project.collaborators.filter( diff --git a/cli/src/services/project-fetcher.ts b/cli/src/services/project-fetcher.ts index a63c6b71e..d01bdff40 100644 --- a/cli/src/services/project-fetcher.ts +++ b/cli/src/services/project-fetcher.ts @@ -88,9 +88,10 @@ export default class ProjectFetcher { } } - revisions(versionId: $versionId) { + revisions(versionId: $version_id) { id isMaster + translatedCount translationsCount conflictsCount reviewedCount diff --git a/cli/src/types/project.ts b/cli/src/types/project.ts index 98dd2c252..cf19ca9a9 100644 --- a/cli/src/types/project.ts +++ b/cli/src/types/project.ts @@ -10,6 +10,7 @@ export interface Revision { slug: string | null; language: Language; isMaster: boolean; + translatedCount: number; translationsCount: number; conflictsCount: number; reviewedCount: number; diff --git a/config/config.exs b/config/config.exs index 28cd71d68..f1df5bd9a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -6,7 +6,7 @@ config :accent, ecto_repos: [Accent.Repo], version: version -config :accent, Accent.Repo, start_apps_before_migration: [:ssl] +config :accent, Accent.Repo, start_apps_before_migration: [:ssl], stacktrace: true if config_env() == :dev do config :accent, Accent.Repo, log: false diff --git a/lib/accent/auth/role_abilities.ex b/lib/accent/auth/role_abilities.ex index 5e2f5d8fc..d2da8bf88 100644 --- a/lib/accent/auth/role_abilities.ex +++ b/lib/accent/auth/role_abilities.ex @@ -7,6 +7,7 @@ defmodule Accent.RoleAbilities do @bot_role "bot" @developer_role "developer" @reviewer_role "reviewer" + @translator_role "translator" @read_actions ~w( lint @@ -36,10 +37,6 @@ defmodule Accent.RoleAbilities do create_comment delete_comment update_comment - correct_all_revision - uncorrect_all_revision - correct_translation - uncorrect_translation update_translation create_translation_comments_subscription delete_translation_comments_subscription @@ -91,6 +88,13 @@ defmodule Accent.RoleAbilities do delete_project )a ++ @developer_actions + @reviewer_actions ~w( + correct_all_revision + uncorrect_all_revision + correct_translation + uncorrect_translation + )a ++ @any_actions + @actions_with_target ~w(machine_translations_translate use_prompt_improve_text)a def actions_for(role, target) @@ -102,6 +106,7 @@ defmodule Accent.RoleAbilities do def actions_for(@bot_role, target), do: add_actions_with_target(@bot_actions, @bot_role, target) def actions_for(@developer_role, target), do: add_actions_with_target(@developer_actions, @developer_role, target) def actions_for(@reviewer_role, target), do: add_actions_with_target(@any_actions, @reviewer_role, target) + def actions_for(@translator_role, target), do: add_actions_with_target(@any_actions, @translator_role, target) defp add_actions_with_target(actions, role, target) do Enum.reduce(@actions_with_target, actions, fn action, actions -> @@ -141,10 +146,14 @@ defmodule Accent.RoleAbilities do def can?(@developer_role, unquote(action), _), do: true end - for action <- @any_actions do + for action <- @reviewer_actions do def can?(@reviewer_role, unquote(action), _), do: true end + for action <- @any_actions do + def can?(@translator_role, unquote(action), _), do: true + end + # Fallback if no permission has been found for the user on the project def can?(_role, _action, _), do: false end diff --git a/lib/accent/badge_generator.ex b/lib/accent/badge_generator.ex index 43da29d4b..32812cc61 100644 --- a/lib/accent/badge_generator.ex +++ b/lib/accent/badge_generator.ex @@ -37,15 +37,19 @@ defmodule Accent.BadgeGenerator do defp label(value, :percentage_reviewed_count), do: "#{value}%25" defp label(value, :translations_count), do: "#{value}%20strings" defp label(value, :reviewed_count), do: "#{value}%20reviewed" + defp label(value, :translated_count), do: "#{value}%20translated" defp label(value, :conflicts_count), do: "#{value}%20conflicts" defp merge_project_stats(revisions) do + initial_state = %{translations_count: 0, conflicts_count: 0, reviewed_count: 0, translated_count: 0} + revisions - |> Enum.reduce(%{translations_count: 0, conflicts_count: 0, reviewed_count: 0}, fn revision, acc -> + |> Enum.reduce(initial_state, fn revision, acc -> acc |> Map.put(:translations_count, acc[:translations_count] + revision.translations_count) |> Map.put(:conflicts_count, acc[:conflicts_count] + revision.conflicts_count) |> Map.put(:reviewed_count, acc[:reviewed_count] + revision.reviewed_count) + |> Map.put(:translated_count, acc[:translated_count] + revision.reviewed_count) end) |> then(fn %{translations_count: 0} = stats -> diff --git a/lib/accent/schemas/document.ex b/lib/accent/schemas/document.ex index 5d71c57aa..8f19de7b3 100644 --- a/lib/accent/schemas/document.ex +++ b/lib/accent/schemas/document.ex @@ -17,6 +17,7 @@ defmodule Accent.Document do has_many(:translations, Accent.Translation) field(:translations_count, :any, virtual: true, default: :not_loaded) + field(:translated_count, :any, virtual: true, default: :not_loaded) field(:reviewed_count, :any, virtual: true, default: :not_loaded) field(:conflicts_count, :any, virtual: true, default: :not_loaded) diff --git a/lib/accent/schemas/previous_translation.ex b/lib/accent/schemas/previous_translation.ex index 3c333ef05..b3deaa1ff 100644 --- a/lib/accent/schemas/previous_translation.ex +++ b/lib/accent/schemas/previous_translation.ex @@ -19,6 +19,7 @@ defmodule Accent.PreviousTranslation do field(:conflicted_text, :string, default: "") field(:conflicted, :boolean, default: false) field(:removed, :boolean, default: false) + field(:translated, :boolean, default: false) field(:value_type, :string) field(:placeholders, {:array, :string}, default: []) end @@ -28,8 +29,8 @@ defmodule Accent.PreviousTranslation do iex> Accent.PreviousTranslation.from_translation(nil) %Accent.PreviousTranslation{} - iex> Accent.PreviousTranslation.from_translation(%Accent.Translation{proposed_text: "a", corrected_text: "b", conflicted_text: "c", conflicted: true, removed: false, value_type: "string", placeholders: ["foo"]}) - %Accent.PreviousTranslation{proposed_text: "a", corrected_text: "b", conflicted_text: "c", conflicted: true, removed: false, value_type: "string", placeholders: ["foo"]} + iex> Accent.PreviousTranslation.from_translation(%Accent.Translation{translated: true, proposed_text: "a", corrected_text: "b", conflicted_text: "c", conflicted: true, removed: false, value_type: "string", placeholders: ["foo"]}) + %Accent.PreviousTranslation{translated: false, proposed_text: "a", corrected_text: "b", conflicted_text: "c", conflicted: true, removed: false, value_type: "string", placeholders: ["foo"]} """ def from_translation(nil), do: from_translation(%{}) diff --git a/lib/accent/schemas/project.ex b/lib/accent/schemas/project.ex index 6ee7a9d34..2d9b42227 100644 --- a/lib/accent/schemas/project.ex +++ b/lib/accent/schemas/project.ex @@ -20,6 +20,7 @@ defmodule Accent.Project do field(:locked_file_operations, :boolean, default: false) field(:translations_count, :any, virtual: true, default: :not_loaded) + field(:translated_count, :any, virtual: true, default: :not_loaded) field(:reviewed_count, :any, virtual: true, default: :not_loaded) field(:conflicts_count, :any, virtual: true, default: :not_loaded) @@ -30,7 +31,9 @@ defmodule Accent.Project do has_many(:operations, Accent.Operation) has_many(:prompts, Accent.Prompt) - has_many(:collaborators, Accent.Collaborator, where: [role: {:in, ["reviewer", "admin", "developer", "owner"]}]) + has_many(:collaborators, Accent.Collaborator, + where: [role: {:in, ["reviewer", "admin", "developer", "owner", "translator"]}] + ) has_many(:all_collaborators, Accent.Collaborator) belongs_to(:language, Accent.Language) diff --git a/lib/accent/schemas/revision.ex b/lib/accent/schemas/revision.ex index 586058a7c..9688bb092 100644 --- a/lib/accent/schemas/revision.ex +++ b/lib/accent/schemas/revision.ex @@ -27,6 +27,7 @@ defmodule Accent.Revision do field(:translations_count, :any, virtual: true, default: :not_loaded) field(:reviewed_count, :any, virtual: true, default: :not_loaded) + field(:translated_count, :any, virtual: true, default: :not_loaded) field(:conflicts_count, :any, virtual: true, default: :not_loaded) field(:translation_ids, {:array, :string}, virtual: true) diff --git a/lib/accent/schemas/role.ex b/lib/accent/schemas/role.ex index 2f847855f..484ad361d 100644 --- a/lib/accent/schemas/role.ex +++ b/lib/accent/schemas/role.ex @@ -8,14 +8,15 @@ defmodule Accent.Role do %{slug: "owner"}, %{slug: "admin"}, %{slug: "developer"}, - %{slug: "reviewer"} + %{slug: "reviewer"}, + %{slug: "translator"} ] @doc """ ## Examples iex> Accent.Role.slugs() - ["owner", "admin", "developer", "reviewer"] + ["owner", "admin", "developer", "reviewer", "translator"] """ defmacro slugs, do: Enum.map(@all, &Map.get(&1, :slug)) @@ -27,7 +28,8 @@ defmodule Accent.Role do %Accent.Role{slug: "owner"}, %Accent.Role{slug: "admin"}, %Accent.Role{slug: "developer"}, - %Accent.Role{slug: "reviewer"} + %Accent.Role{slug: "reviewer"}, + %Accent.Role{slug: "translator"} ] """ def all, do: Enum.map(@all, &struct(__MODULE__, &1)) diff --git a/lib/accent/schemas/translation.ex b/lib/accent/schemas/translation.ex index ebcabc5f4..6e7a9a6e4 100644 --- a/lib/accent/schemas/translation.ex +++ b/lib/accent/schemas/translation.ex @@ -17,6 +17,7 @@ defmodule Accent.Translation do field(:value_type, :string, default: "string") field(:plural, :boolean, default: false) field(:locked, :boolean, default: false) + field(:translated, :boolean, default: false) field(:placeholders, {:array, :string}, default: []) belongs_to(:document, Accent.Document) @@ -42,6 +43,7 @@ defmodule Accent.Translation do conflicted removed comments_count + translated file_index file_comment value_type diff --git a/lib/accent/scopes/document.ex b/lib/accent/scopes/document.ex index c888b1d2b..a3e1c583c 100644 --- a/lib/accent/scopes/document.ex +++ b/lib/accent/scopes/document.ex @@ -25,7 +25,7 @@ defmodule Accent.Scopes.Document do end @doc """ - Fill `translations_count`, `conflicts_count` and `reviewed_count` for documents. + Fill `translations_count`, `conflicts_count`, `translated_count` and `reviewed_count` for documents. """ @spec with_stats(Ecto.Queryable.t(), Keyword.t()) :: Ecto.Queryable.t() def with_stats(query, opts) do diff --git a/lib/accent/scopes/project.ex b/lib/accent/scopes/project.ex index 90cb67326..2d2244b27 100644 --- a/lib/accent/scopes/project.ex +++ b/lib/accent/scopes/project.ex @@ -37,7 +37,7 @@ defmodule Accent.Scopes.Project do end @doc """ - Fill `translations_count`, `conflicts_count` and `reviewed_count` for projects. + Fill `translations_count`, `conflicts_count`, `translated_count` and `reviewed_count` for projects. """ @spec with_stats(Ecto.Queryable.t()) :: Ecto.Queryable.t() def with_stats(query) do @@ -52,6 +52,7 @@ defmodule Accent.Scopes.Project do ) reviewed = from(translations, where: [conflicted: false]) + translated = from(translations, where: [translated: true]) from( projects in query, @@ -59,8 +60,11 @@ defmodule Accent.Scopes.Project do on: translations.field_id == projects.id, left_join: reviewed in subquery(reviewed), on: reviewed.field_id == projects.id, + left_join: translated in subquery(translated), + on: translated.field_id == projects.id, select_merge: %{ translations_count: coalesce(translations.count, 0), + translated_count: coalesce(translated.count, 0), reviewed_count: coalesce(reviewed.count, 0), conflicts_count: coalesce(translations.count, 0) - coalesce(reviewed.count, 0) } diff --git a/lib/accent/scopes/revision.ex b/lib/accent/scopes/revision.ex index 2660f892a..4fdb3474e 100644 --- a/lib/accent/scopes/revision.ex +++ b/lib/accent/scopes/revision.ex @@ -63,7 +63,7 @@ defmodule Accent.Scopes.Revision do end @doc """ - Fill `translations_count`, `conflicts_count` and `reviewed_count` for revisions. + Fill `translations_count`, `conflicts_count`, `translated_count` and `reviewed_count` for revisions. """ @spec with_stats(Ecto.Queryable.t(), Keyword.t() | nil) :: Ecto.Queryable.t() def with_stats(query, options \\ []) do diff --git a/lib/accent/scopes/translations_count.ex b/lib/accent/scopes/translations_count.ex index a5633193d..689d74c58 100644 --- a/lib/accent/scopes/translations_count.ex +++ b/lib/accent/scopes/translations_count.ex @@ -28,11 +28,13 @@ defmodule Accent.Scopes.TranslationsCount do query |> count_translations(translations, exclude_empty_translations) |> count_reviewed(translations) + |> count_translated(translations) from( - [translations: t, reviewed: r] in query, + [translations: t, reviewed: r, translated: td] in query, select_merge: %{ translations_count: coalesce(t.count, 0), + translated_count: coalesce(td.count, 0), reviewed_count: coalesce(r.count, 0), conflicts_count: coalesce(t.count, 0) - coalesce(r.count, 0) } @@ -59,4 +61,14 @@ defmodule Accent.Scopes.TranslationsCount do reviewed = from(translations, where: [conflicted: false]) from(q in query, left_join: translations in subquery(reviewed), as: :reviewed, on: translations.field_id == q.id) end + + defp count_translated(query, translations) do + translated = from(translations, where: [translated: true]) + + from(q in query, + left_join: translations in subquery(translated), + as: :translated, + on: translations.field_id == q.id + ) + end end diff --git a/lib/graphql/mutations/translation.ex b/lib/graphql/mutations/translation.ex index 37a875d71..590901c75 100644 --- a/lib/graphql/mutations/translation.ex +++ b/lib/graphql/mutations/translation.ex @@ -16,13 +16,14 @@ defmodule Accent.GraphQL.Mutations.Translation do field :uncorrect_translation, :mutated_translation do arg(:id, non_null(:id)) + arg(:text, non_null(:string)) resolve(translation_authorize(:uncorrect_translation, &TranslationResolver.uncorrect/3)) end field :update_translation, :mutated_translation do arg(:id, non_null(:id)) - arg(:text, :string) + arg(:text, non_null(:string)) resolve(translation_authorize(:update_translation, &TranslationResolver.update/3)) end diff --git a/lib/graphql/resolvers/translation.ex b/lib/graphql/resolvers/translation.ex index a78682676..9f052e1d5 100644 --- a/lib/graphql/resolvers/translation.ex +++ b/lib/graphql/resolvers/translation.ex @@ -52,9 +52,10 @@ defmodule Accent.GraphQL.Resolvers.Translation do end @spec uncorrect(Translation.t(), map(), GraphQLContext.t()) :: translation_operation - def uncorrect(translation, _, info) do + def uncorrect(translation, %{text: text}, info) do %Context{} |> Context.assign(:translation, translation) + |> Context.assign(:text, text) |> Context.assign(:user_id, info.context[:conn].assigns[:current_user].id) |> TranslationUncorrectConflictBuilder.build() |> then(&fn -> BasePersister.execute(&1) end) diff --git a/lib/graphql/types/document.ex b/lib/graphql/types/document.ex index d7e6f0468..3d0fb035f 100644 --- a/lib/graphql/types/document.ex +++ b/lib/graphql/types/document.ex @@ -12,6 +12,7 @@ defmodule Accent.GraphQL.Types.Document do field(:path, non_null(:string)) field(:format, non_null(:document_format)) field(:translations_count, non_null(:integer)) + field(:translated_count, non_null(:integer)) field(:conflicts_count, non_null(:integer)) field(:reviewed_count, non_null(:integer)) end diff --git a/lib/graphql/types/project.ex b/lib/graphql/types/project.ex index c2b029932..4184647d3 100644 --- a/lib/graphql/types/project.ex +++ b/lib/graphql/types/project.ex @@ -37,6 +37,7 @@ defmodule Accent.GraphQL.Types.Project do field(:logo, :string) field(:translations_count, non_null(:integer)) + field(:translated_count, non_null(:integer)) field(:conflicts_count, non_null(:integer)) field(:reviewed_count, non_null(:integer)) diff --git a/lib/graphql/types/revision.ex b/lib/graphql/types/revision.ex index 29fa7b195..3781838e6 100644 --- a/lib/graphql/types/revision.ex +++ b/lib/graphql/types/revision.ex @@ -10,6 +10,7 @@ defmodule Accent.GraphQL.Types.Revision do field(:id, :id) field(:is_master, non_null(:boolean), resolve: field_alias(:master)) field(:translations_count, non_null(:integer)) + field(:translated_count, non_null(:integer)) field(:conflicts_count, non_null(:integer)) field(:reviewed_count, non_null(:integer)) field(:inserted_at, non_null(:datetime)) diff --git a/lib/graphql/types/role.ex b/lib/graphql/types/role.ex index 260097406..328fd86b4 100644 --- a/lib/graphql/types/role.ex +++ b/lib/graphql/types/role.ex @@ -7,6 +7,7 @@ defmodule Accent.GraphQL.Types.Role do value(:owner, as: "owner") value(:admin, as: "admin") value(:developer, as: "developer") + value(:translator, as: "translator") value(:reviewer, as: "reviewer") end diff --git a/lib/graphql/types/viewer.ex b/lib/graphql/types/viewer.ex index 85dd587cc..d3b5c0f51 100644 --- a/lib/graphql/types/viewer.ex +++ b/lib/graphql/types/viewer.ex @@ -25,7 +25,7 @@ defmodule Accent.GraphQL.Types.Viewer do end field :project, :project do - arg(:id, non_null(:id)) + arg(:id, :id, default_value: nil) resolve(project_authorize(:show_project, &Accent.GraphQL.Resolvers.Project.show_viewer/3)) end diff --git a/lib/movement/builders/translation_uncorrect_conflict.ex b/lib/movement/builders/translation_uncorrect_conflict.ex index 067a443b0..fa4364024 100644 --- a/lib/movement/builders/translation_uncorrect_conflict.ex +++ b/lib/movement/builders/translation_uncorrect_conflict.ex @@ -6,8 +6,9 @@ defmodule Movement.Builders.TranslationUncorrectConflict do @action "uncorrect_conflict" - def build(%Movement.Context{assigns: %{translation: translation}, operations: operations} = context) do - operation = OperationMapper.map(@action, translation, %{text: nil}) + def build(%Movement.Context{assigns: %{translation: translation, text: text}, operations: operations} = context) do + value_type = Movement.Mappers.ValueType.from_translation_new_value(translation, text) + operation = OperationMapper.map(@action, translation, %{text: text, value_type: value_type}) %{context | operations: Enum.concat(operations, [operation])} end diff --git a/lib/movement/builders/translation_update.ex b/lib/movement/builders/translation_update.ex index 817ec4c16..e78874007 100644 --- a/lib/movement/builders/translation_update.ex +++ b/lib/movement/builders/translation_update.ex @@ -6,7 +6,10 @@ defmodule Movement.Builders.TranslationUpdate do @action "update" - def build(%Movement.Context{assigns: %{text: text, translation: %{corrected_text: corrected_text}}} = context) + def build( + %Movement.Context{assigns: %{text: text, translation: %{corrected_text: corrected_text, translated: true}}} = + context + ) when text === corrected_text, do: context diff --git a/lib/movement/migration/conflict.ex b/lib/movement/migration/conflict.ex index b8f29480c..677b0b268 100644 --- a/lib/movement/migration/conflict.ex +++ b/lib/movement/migration/conflict.ex @@ -9,20 +9,31 @@ defmodule Movement.Migration.Conflict do update_all_dynamic( operation.translation, - [:text, :text, :boolean], - [:corrected_text, :value_type, :conflicted], - [operation.text, operation.value_type, false] + [:text, :text, :boolean, :boolean], + [:corrected_text, :value_type, :conflicted, :translated], + [operation.text, operation.value_type, false, true] ) end def call(:uncorrect, operation) do + conflicted_text = + if operation.previous_translation do + if operation.text === operation.previous_translation.corrected_text do + operation.previous_translation.conflicted_text + else + operation.previous_translation.corrected_text + end + end + update_all_dynamic( operation.translation, - [:text, :text, :boolean], - [:conflicted_text, :value_type, :conflicted], + [:text, :text, :text, :boolean, :boolean], + [:corrected_text, :conflicted_text, :value_type, :conflicted, :translated], [ - operation.previous_translation && operation.previous_translation.conflicted_text, - operation.previous_translation && operation.previous_translation.value_type, + operation.text, + conflicted_text, + operation.value_type, + true, true ] ) diff --git a/lib/movement/migration/translation.ex b/lib/movement/migration/translation.ex index eb064bae9..a152685fa 100644 --- a/lib/movement/migration/translation.ex +++ b/lib/movement/migration/translation.ex @@ -19,6 +19,7 @@ defmodule Movement.Migration.Translation do update(operation.translation, %{ value_type: operation.value_type, + translated: true, corrected_text: operation.text, conflicted_text: operation.previous_translation && operation.previous_translation.corrected_text, placeholders: operation.placeholders @@ -56,6 +57,7 @@ defmodule Movement.Migration.Translation do file_index: operation.file_index, file_comment: operation.file_comment, removed: operation.previous_translation && operation.previous_translation.removed, + translated: (operation.previous_translation && operation.previous_translation.translated) || false, revision_id: operation.revision_id, document_id: operation.document_id, version_id: operation.version_id, @@ -79,6 +81,7 @@ defmodule Movement.Migration.Translation do key: operation.key, proposed_text: operation.text, corrected_text: operation.text, + translated: (operation.previous_translation && operation.previous_translation.translated) || false, conflicted: operation.previous_translation && operation.previous_translation.conflicted, value_type: operation.value_type, file_index: operation.file_index, diff --git a/lib/movement/persisters/operations_update_all_dynamic.ex b/lib/movement/persisters/operations_update_all_dynamic.ex index 7f5ce163f..11eb40b3b 100644 --- a/lib/movement/persisters/operations_update_all_dynamic.ex +++ b/lib/movement/persisters/operations_update_all_dynamic.ex @@ -6,7 +6,7 @@ defmodule Movement.Persisters.OperationsUpdateAllDynamic do @uuid_fragment "SELECT * FROM unnest(?::uuid[], ?::uuid[]) AS t(a, b)" @text_text_bool_bool_fragment "SELECT * FROM unnest(?::uuid[], ?::text[], ?::text[], ?::boolean[], ?::boolean[]) AS t(a, b, c, d, e)" - @text_text_bool_fragment "SELECT * FROM unnest(?::uuid[], ?::text[], ?::text[], ?::boolean[]) AS t(a, b, c, d)" + @text_text_text_bool_bool_fragment "SELECT * FROM unnest(?::uuid[], ?::text[], ?::text[], ?::text[], ?::boolean[], ?::boolean[]) AS t(a, b, c, d, e, f)" def update({{schema, [:text, :text, :boolean, :boolean], fields}, records}) do [bind_1, bind_2, bind_3, bind_4] = values_binding(records, fields) @@ -28,7 +28,8 @@ defmodule Movement.Persisters.OperationsUpdateAllDynamic do {^Enum.at(fields, 0), values_list.b}, {^Enum.at(fields, 1), values_list.c}, {^Enum.at(fields, 2), values_list.d}, - {^Enum.at(fields, 3), values_list.e} + {^Enum.at(fields, 3), values_list.e}, + updated_at: ^DateTime.utc_now(:second) ] ] ) @@ -47,30 +48,35 @@ defmodule Movement.Persisters.OperationsUpdateAllDynamic do ^bind_1 ), on: values_list.a == entries.id, - update: [set: [{^Enum.at(fields, 0), values_list.b}]] + update: [set: [{^Enum.at(fields, 0), values_list.b}, updated_at: ^DateTime.utc_now(:second)]] ) ) end - def update({{schema, [:text, :text, :boolean], fields}, records}) do - [bind_1, bind_2, bind_3] = values_binding(records, fields) + def update({{schema, [:text, :text, :text, :boolean, :boolean], fields}, records}) do + [bind_1, bind_2, bind_3, bind_4, bind_5] = values_binding(records, fields) update_all( from(entries in schema, join: values_list in fragment( - @text_text_bool_fragment, + @text_text_text_bool_bool_fragment, ^ids_binding(records), ^bind_1, ^bind_2, - ^bind_3 + ^bind_3, + ^bind_4, + ^bind_5 ), on: values_list.a == entries.id, update: [ set: [ {^Enum.at(fields, 0), values_list.b}, {^Enum.at(fields, 1), values_list.c}, - {^Enum.at(fields, 2), values_list.d} + {^Enum.at(fields, 2), values_list.d}, + {^Enum.at(fields, 3), values_list.e}, + {^Enum.at(fields, 4), values_list.f}, + updated_at: ^DateTime.utc_now(:second) ] ] ) diff --git a/lib/web/controllers/badge_controller.ex b/lib/web/controllers/badge_controller.ex index 5fb1f9066..9bb2246e6 100644 --- a/lib/web/controllers/badge_controller.ex +++ b/lib/web/controllers/badge_controller.ex @@ -24,6 +24,7 @@ defmodule Accent.BadgeController do def conflicts_count(conn, _params), do: send_badge_resp(conn) def reviewed_count(conn, _params), do: send_badge_resp(conn) def translations_count(conn, _params), do: send_badge_resp(conn) + def translated_count(conn, _params), do: send_badge_resp(conn) defp send_badge_resp(conn) do conn diff --git a/priv/repo/migrations/20231215150950_add_translated_column_for_translations.exs b/priv/repo/migrations/20231215150950_add_translated_column_for_translations.exs new file mode 100644 index 000000000..36a468fd9 --- /dev/null +++ b/priv/repo/migrations/20231215150950_add_translated_column_for_translations.exs @@ -0,0 +1,12 @@ +defmodule Accent.Repo.Migrations.AddTranslatedColumnForTranslations do + @moduledoc false + use Ecto.Migration + + def change do + alter table(:translations) do + add(:translated, :boolean, null: false, default: false) + end + + execute("UPDATE translations SET translated = true") + end +end diff --git a/test/graphql/resolvers/translation_test.exs b/test/graphql/resolvers/translation_test.exs index d66758fc6..347815723 100644 --- a/test/graphql/resolvers/translation_test.exs +++ b/test/graphql/resolvers/translation_test.exs @@ -68,11 +68,12 @@ defmodule AccentTest.GraphQL.Resolvers.Translation do proposed_text: "bar" }) - {:ok, result} = Resolver.uncorrect(translation, %{}, context) + {:ok, result} = Resolver.uncorrect(translation, %{text: "baz"}, context) assert get_in(result, [:errors]) == nil assert get_in(result, [:translation, Access.key(:id)]) == translation.id - assert get_in(Repo.all(Translation), [Access.all(), Access.key(:corrected_text)]) == ["bar"] + assert get_in(Repo.all(Translation), [Access.all(), Access.key(:corrected_text)]) == ["baz"] + assert get_in(Repo.all(Translation), [Access.all(), Access.key(:conflicted_text)]) == ["bar"] assert get_in(Repo.all(Translation), [Access.all(), Access.key(:conflicted)]) == [true] end diff --git a/test/movement/builders/translation_uncorrect_conflict_test.exs b/test/movement/builders/translation_uncorrect_conflict_test.exs index 73b72ef12..9e60dd8db 100644 --- a/test/movement/builders/translation_uncorrect_conflict_test.exs +++ b/test/movement/builders/translation_uncorrect_conflict_test.exs @@ -13,13 +13,15 @@ defmodule AccentTest.Movement.Builders.TranslationUncorrectConflict do context = %Movement.Context{} |> Movement.Context.assign(:translation, translation) + |> Movement.Context.assign(:text, "B") |> TranslationUncorrectConflictBuilder.build() - operations = Enum.map(context.operations, &Map.take(&1, [:key, :action])) + operations = Enum.map(context.operations, &Map.take(&1, [:key, :action, :text])) assert operations === [ %{ key: "a", + text: "B", action: "uncorrect_conflict" } ] diff --git a/test/movement/builders/translation_update_test.exs b/test/movement/builders/translation_update_test.exs index a39e44a3b..70c44e155 100644 --- a/test/movement/builders/translation_update_test.exs +++ b/test/movement/builders/translation_update_test.exs @@ -30,11 +30,12 @@ defmodule AccentTest.Movement.Builders.TranslationUpdate do ] end - test "builder same text" do + test "builder same text translated" do translation = %Translation{ key: "a", proposed_text: "A", - corrected_text: "A" + corrected_text: "A", + translated: true } context = @@ -46,6 +47,23 @@ defmodule AccentTest.Movement.Builders.TranslationUpdate do assert context.operations == [] end + test "builder same text not translated" do + translation = %Translation{ + key: "a", + proposed_text: "A", + corrected_text: "A", + translated: false + } + + context = + %Context{} + |> Context.assign(:text, "A") + |> Context.assign(:translation, translation) + |> TranslationUpdateBuilder.build() + + assert length(context.operations) === 1 + end + test "builder value type null to nothing" do translation = %Translation{ key: "a", diff --git a/test/movement/up_test.exs b/test/movement/up_test.exs index 733dd9ef4..0d306884b 100644 --- a/test/movement/up_test.exs +++ b/test/movement/up_test.exs @@ -127,11 +127,14 @@ defmodule AccentTest.Movement.Migrator.Up do proposed_text: "proposed_text", conflicted_text: "foo", conflicted: false, - removed: false + removed: false, + value_type: "string" }) Migrator.up(%Operation{ action: "uncorrect_conflict", + value_type: "string", + text: "new proposed text", translation: translation, previous_translation: PreviousTranslation.from_translation(translation) }) @@ -154,12 +157,16 @@ defmodule AccentTest.Movement.Migrator.Up do corrected_text: "proposed_text", proposed_text: "proposed_text", conflicted_text: "previous conflicted", + value_type: "string", conflicted: false, + translated: true, removed: false }) Migrator.up(%Operation{ action: "uncorrect_conflict", + value_type: "string", + text: "proposed_text", translation: translation, previous_translation: PreviousTranslation.from_translation(translation) }) diff --git a/test/services/collaborator_creator_test.exs b/test/services/collaborator_creator_test.exs index 6a0676347..2121782f0 100644 --- a/test/services/collaborator_creator_test.exs +++ b/test/services/collaborator_creator_test.exs @@ -63,7 +63,8 @@ defmodule AccentTest.CollaboratorCreator do }) assert collaborator.errors === [ - role: {"is invalid", [validation: :inclusion, enum: ["owner", "admin", "developer", "reviewer"]]} + role: + {"is invalid", [validation: :inclusion, enum: ["owner", "admin", "developer", "reviewer", "translator"]]} ] end diff --git a/webapp/app/locales/en-us.json b/webapp/app/locales/en-us.json index 774d370d7..a30d19522 100644 --- a/webapp/app/locales/en-us.json +++ b/webapp/app/locales/en-us.json @@ -943,7 +943,8 @@ "DEVELOPER": "Developer", "OWNER": "Owner", "BOT": "API Token", - "REVIEWER": "Reviewer" + "REVIEWER": "Reviewer", + "TRANSLATOR": "Translator" }, "machine_translations_providers": { "google_translate": "Google Translate", diff --git a/webapp/app/locales/fr-ca.json b/webapp/app/locales/fr-ca.json index dc03c0605..5a0c66795 100644 --- a/webapp/app/locales/fr-ca.json +++ b/webapp/app/locales/fr-ca.json @@ -946,7 +946,8 @@ "DEVELOPER": "Développeur", "OWNER": "Propriétaire", "BOT": "Jeton d’API", - "REVIEWER": "Traducteur" + "REVIEWER": "Correcteur", + "TRANSLATOR": "Traducteur" }, "machine_translations_providers": { "google_translate": "Google Translate", diff --git a/webapp/app/pods/components/conflicts-list/item/related-translation/styles.scss b/webapp/app/pods/components/conflicts-list/item/related-translation/styles.scss index b0f2890ba..a1eb07f87 100644 --- a/webapp/app/pods/components/conflicts-list/item/related-translation/styles.scss +++ b/webapp/app/pods/components/conflicts-list/item/related-translation/styles.scss @@ -1,6 +1,6 @@ .item { display: flex; - align-items: start; + align-items: flex-start; flex-direction: row; padding: 5px 0 3px; color: var(--color-grey); @@ -31,7 +31,7 @@ .item-text { width: 100%; display: flex; - align-items: start; + align-items: flex-start; } .item-text-content { diff --git a/webapp/app/pods/components/dashboard-revisions/item/template.hbs b/webapp/app/pods/components/dashboard-revisions/item/template.hbs index f64093aee..e21020625 100644 --- a/webapp/app/pods/components/dashboard-revisions/item/template.hbs +++ b/webapp/app/pods/components/dashboard-revisions/item/template.hbs @@ -44,29 +44,33 @@ {{#if this.showActions}}
- {{#if this.showCorrectAllAction}} - - {{inline-svg '/assets/check.svg' class='button-icon'}} - {{t 'components.dashboard_revisions.item.correct_all_button'}} - + {{#if (get @permissions 'correct_all_revision')}} + {{#if this.showCorrectAllAction}} + + {{inline-svg '/assets/check.svg' class='button-icon'}} + {{t 'components.dashboard_revisions.item.correct_all_button'}} + + {{/if}} {{/if}} - {{#if this.showUncorrectAllAction}} - - {{inline-svg '/assets/revert.svg' class='button-icon'}} - {{t 'components.dashboard_revisions.item.uncorrect_all_button'}} - + {{#if (get @permissions 'uncorrect_all_revision')}} + {{#if this.showUncorrectAllAction}} + + {{inline-svg '/assets/revert.svg' class='button-icon'}} + {{t 'components.dashboard_revisions.item.uncorrect_all_button'}} + + {{/if}} {{/if}}
{{/if}} diff --git a/webapp/app/pods/components/project-navigation/list/template.hbs b/webapp/app/pods/components/project-navigation/list/template.hbs index 3c40a8621..c7e588777 100644 --- a/webapp/app/pods/components/project-navigation/list/template.hbs +++ b/webapp/app/pods/components/project-navigation/list/template.hbs @@ -24,7 +24,7 @@ {{/if}} - {{#if (get @permissions 'index_translations')}} + {{#if (get @permissions 'correct_translation')}}
  • {{inline-svg '/assets/check.svg' local-class='list-item-link-icon'}} diff --git a/webapp/app/pods/components/related-translations-list/item/component.ts b/webapp/app/pods/components/related-translations-list/item/component.ts index 5d68822ec..5b5e07584 100644 --- a/webapp/app/pods/components/related-translations-list/item/component.ts +++ b/webapp/app/pods/components/related-translations-list/item/component.ts @@ -20,12 +20,6 @@ export default class RelatedTranslationsListItem extends Component { @tracked editText = this.args.translation.correctedText; - get showSaveButton() { - if (this.args.translation.isRemoved) return false; - - return this.args.translation.correctedText !== this.editText; - } - get revisionName() { return ( this.args.translation.revision.name || diff --git a/webapp/app/pods/components/related-translations-list/item/template.hbs b/webapp/app/pods/components/related-translations-list/item/template.hbs index d2f07145c..57a224baf 100644 --- a/webapp/app/pods/components/related-translations-list/item/template.hbs +++ b/webapp/app/pods/components/related-translations-list/item/template.hbs @@ -67,7 +67,7 @@ /> {{/unless}} - + {{t 'components.related_translations_list.save_button'}} diff --git a/webapp/app/pods/components/translation-edit/component.ts b/webapp/app/pods/components/translation-edit/component.ts index 2ed1807e7..bd6b16be2 100644 --- a/webapp/app/pods/components/translation-edit/component.ts +++ b/webapp/app/pods/components/translation-edit/component.ts @@ -11,7 +11,7 @@ interface Args { onChangeText?: (text: string) => void; onUpdateText: (text: string) => Promise; onCorrectConflict: (text: string) => Promise; - onUncorrectConflict: () => Promise; + onUncorrectConflict: (text: string) => Promise; } export default class TranslationEdit extends Component { @@ -92,7 +92,7 @@ export default class TranslationEdit extends Component { async uncorrectConflict() { this.isUncorrectingConflict = true; - await this.args.onUncorrectConflict(); + await this.args.onUncorrectConflict(this.text); this.isUncorrectingConflict = false; } diff --git a/webapp/app/pods/components/translation-edit/template.hbs b/webapp/app/pods/components/translation-edit/template.hbs index b2adb9495..93564b1c2 100644 --- a/webapp/app/pods/components/translation-edit/template.hbs +++ b/webapp/app/pods/components/translation-edit/template.hbs @@ -33,59 +33,35 @@ {{/if}} {{#if @translation}} - {{#if @translation.isConflicted}} - {{#unless @translation.isRemoved}} -
    -
    - {{#if @translation.sourceTranslation}} - - {{t 'components.translation_edit.source_translation'}} - - {{/if}} -
    -
    - {{#unless this.hasTextNotChanged}} - - {{inline-svg '/assets/revert.svg' class='button-icon'}} - - {{/unless}} + {{#unless @translation.isRemoved}} +
    +
    + {{#if @translation.sourceTranslation}} + + {{t 'components.translation_edit.source_translation'}} + + {{/if}} +
    +
    + {{#unless this.hasTextNotChanged}} + + {{inline-svg '/assets/revert.svg' class='button-icon'}} + + {{/unless}} - {{#if (get @permissions 'update_translation')}} - - {{t 'components.translation_edit.update_text'}} - - {{/if}} + {{#if (get @permissions 'update_translation')}} + + {{t 'components.translation_edit.update_text'}} + + {{/if}} + {{#if @translation.isConflicted}} {{#if (get @permissions 'correct_translation')}} {{inline-svg '/assets/check.svg' class='button-icon'}} {{t 'components.translation_edit.correct_button'}} {{/if}} -
    -
    - {{/unless}} - {{else}} - {{#unless @translation.isRemoved}} -
    -
    - {{#if @translation.sourceTranslation}} - - {{t 'components.translation_edit.source_translation'}} - - {{/if}} -
    -
    - {{#unless this.hasTextNotChanged}} - - {{inline-svg '/assets/revert.svg' class='button-icon'}} - - {{/unless}} - - {{#if (get @permissions 'update_translation')}} - - {{t 'components.translation_edit.update_text'}} - - {{/if}} + {{else}} {{#if (get @permissions 'uncorrect_translation')}} @@ -93,9 +69,9 @@ {{t 'components.translation_edit.uncorrect_button'}} {{/if}} -
    + {{/if}}
    - {{/unless}} - {{/if}} +
    + {{/unless}} {{/if}}
    \ No newline at end of file diff --git a/webapp/app/pods/logged-in/project/translation/index/controller.ts b/webapp/app/pods/logged-in/project/translation/index/controller.ts index 295b502a5..33e84a4cb 100644 --- a/webapp/app/pods/logged-in/project/translation/index/controller.ts +++ b/webapp/app/pods/logged-in/project/translation/index/controller.ts @@ -65,13 +65,14 @@ export default class IndexController extends Controller { } @action - async uncorrectConflict() { + async uncorrectConflict(text: string) { const conflict = this.model.translation; const response = await this.apolloMutate.mutate({ mutation: translationUncorrectQuery, variables: { translationId: conflict.id, + text, }, }); diff --git a/webapp/app/queries/uncorrect-translation.ts b/webapp/app/queries/uncorrect-translation.ts index 79ef69e35..b89d47213 100644 --- a/webapp/app/queries/uncorrect-translation.ts +++ b/webapp/app/queries/uncorrect-translation.ts @@ -1,8 +1,8 @@ import gql from 'graphql-tag'; export default gql` - mutation TranslationUncorrect($translationId: ID!) { - uncorrectTranslation(id: $translationId) { + mutation TranslationUncorrect($translationId: ID!, $text: String!) { + uncorrectTranslation(id: $translationId, text: $text) { translation { id correctedText