mirror of
https://github.com/mirego/accent.git
synced 2024-09-11 10:55:28 +03:00
Add machine translations config
This commit is contained in:
parent
bb8979d325
commit
a37b1831fe
@ -29,7 +29,7 @@
|
||||
{Credo.Check.Readability.TrailingBlankLine},
|
||||
{Credo.Check.Readability.TrailingWhiteSpace},
|
||||
{Credo.Check.Readability.VariableNames},
|
||||
{Credo.Check.Refactor.ABCSize, max_size: 50},
|
||||
{Credo.Check.Refactor.ABCSize, max_size: 60},
|
||||
{Credo.Check.Refactor.CaseTrivialMatches},
|
||||
{Credo.Check.Refactor.CondStatements},
|
||||
{Credo.Check.Refactor.FunctionArity},
|
||||
@ -39,7 +39,7 @@
|
||||
{Credo.Check.Refactor.MapInto, false},
|
||||
{Credo.Check.Refactor.NegatedConditionsInUnless},
|
||||
{Credo.Check.Refactor.NegatedConditionsWithElse},
|
||||
{Credo.Check.Refactor.Nesting},
|
||||
{Credo.Check.Refactor.Nesting, max_nesting: 4},
|
||||
{Credo.Check.Refactor.UnlessWithElse},
|
||||
{Credo.Check.Warning.IExPry},
|
||||
{Credo.Check.Warning.IoInspect},
|
||||
|
@ -129,6 +129,7 @@ Accent provides a default value for every required environment variable. This me
|
||||
| `SENTRY_DSN` | _none_ | The _secret_ Sentry DSN used to collect API runtime errors |
|
||||
| `WEBAPP_SENTRY_DSN` | _none_ | The _public_ Sentry DSN used to collect Webapp runtime errors |
|
||||
| `CANONICAL_URL` | _none_ | The URL of the app. Used in sent emails and to redirect from external services to the app in the authentication flow. |
|
||||
| `STATIC_URL` | _none_ | The URL of the app. Default to the CANONICAL_URL value. |
|
||||
| `WEBAPP_SKIP_SUBRESOURCE_INTEGRITY` | _none_ | Remove integrity attributes on link and script tag. Useful when using a proxy that compress resources before serving them. |
|
||||
| `DATABASE_SSL` | _false_ | If SSL should be used to connect to the database |
|
||||
| `DATABASE_POOL_SIZE` | _10_ | The size of the pool used by the database connection module |
|
||||
|
@ -40,20 +40,10 @@ config :accent, Accent.Repo,
|
||||
url: get_env("DATABASE_URL") || "postgres://localhost/accent_development",
|
||||
socket_options: if(ecto_ipv6?, do: [:inet6], else: [])
|
||||
|
||||
google_translate_provider = {Accent.MachineTranslations.Adapter.GoogleTranslations, [key: get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY")]}
|
||||
|
||||
translate_list_provider = if get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY"), do: google_translate_provider, else: nil
|
||||
|
||||
translate_text_providers = []
|
||||
|
||||
translate_text_providers =
|
||||
if get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY"),
|
||||
do: [google_translate_provider | translate_text_providers],
|
||||
else: translate_text_providers
|
||||
|
||||
config :accent, Accent.MachineTranslations,
|
||||
translate_list: translate_list_provider,
|
||||
translate_text: translate_text_providers
|
||||
default_providers_config: %{
|
||||
"google_translate" => %{"key" => get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY")}
|
||||
}
|
||||
|
||||
providers = []
|
||||
providers = if get_env("GOOGLE_API_CLIENT_ID"), do: [{:google, {Ueberauth.Strategy.Google, [scope: "email openid"]}} | providers], else: providers
|
||||
|
@ -8,6 +8,7 @@ defmodule Accent do
|
||||
Accent.Endpoint,
|
||||
Accent.Repo,
|
||||
{Oban, oban_config()},
|
||||
Accent.Vault,
|
||||
{TelemetryUI, Accent.TelemetryUI.config()},
|
||||
{Phoenix.PubSub, [name: Accent.PubSub, adapter: Phoenix.PubSub.PG2]}
|
||||
]
|
||||
|
@ -20,7 +20,7 @@ defmodule Accent.APITokenManager do
|
||||
token: Accent.Utils.SecureRandom.urlsafe_base64(70)
|
||||
}
|
||||
|> cast(params, [:custom_permissions])
|
||||
|> validate_subset(:custom_permissions, Enum.map(RoleAbilities.actions_for(:all), &to_string/1))
|
||||
|> validate_subset(:custom_permissions, Enum.map(RoleAbilities.actions_for(:all, project), &to_string/1))
|
||||
|> cast_assoc(:user,
|
||||
with: fn changeset, params ->
|
||||
changeset
|
||||
|
@ -1,16 +1,12 @@
|
||||
defimpl Canada.Can, for: Accent.User do
|
||||
alias Accent.{User, Project, Revision, Comment}
|
||||
alias Accent.{User, Project, Comment}
|
||||
|
||||
def can?(%User{permissions: permissions}, action, project_id) when is_binary(project_id) do
|
||||
validate_role(permissions, action, project_id)
|
||||
end
|
||||
|
||||
def can?(%User{permissions: permissions}, action, %Project{id: project_id}) when is_binary(project_id) do
|
||||
validate_role(permissions, action, project_id)
|
||||
end
|
||||
|
||||
def can?(%User{permissions: permissions}, action, %Revision{project_id: project_id}) when is_binary(project_id) do
|
||||
validate_role(permissions, action, project_id)
|
||||
def can?(%User{permissions: permissions}, action, project = %Project{}) do
|
||||
validate_role(permissions, action, project)
|
||||
end
|
||||
|
||||
def can?(%User{id: user_id}, _action, %Comment{user_id: comment_user_id}) do
|
||||
@ -25,6 +21,12 @@ defimpl Canada.Can, for: Accent.User do
|
||||
|
||||
def validate_role(nil, _action, _project_id), do: false
|
||||
|
||||
def validate_role(permissions, action, project = %Project{}) do
|
||||
permissions
|
||||
|> Map.get(project.id)
|
||||
|> Accent.RoleAbilities.can?(action, project)
|
||||
end
|
||||
|
||||
def validate_role(permissions, action, project_id) do
|
||||
permissions
|
||||
|> Map.get(project_id)
|
||||
|
@ -1,8 +1,6 @@
|
||||
defmodule Accent.RoleAbilities do
|
||||
require Logger
|
||||
|
||||
alias Accent.MachineTranslations
|
||||
|
||||
@owner_role "owner"
|
||||
@admin_role "admin"
|
||||
@bot_role "bot"
|
||||
@ -71,6 +69,8 @@ defmodule Accent.RoleAbilities do
|
||||
delete_project_integration
|
||||
create_version
|
||||
update_version
|
||||
save_project_machine_translations_config
|
||||
delete_project_machine_translations_config
|
||||
)a ++ @any_actions
|
||||
|
||||
@admin_actions ~w(
|
||||
@ -87,68 +87,65 @@ defmodule Accent.RoleAbilities do
|
||||
delete_project
|
||||
)a ++ @developer_actions
|
||||
|
||||
@configurable_actions ~w(machine_translations_translate_document machine_translations_translate_file machine_translations_translate_text)a
|
||||
@actions_with_target ~w(machine_translations_translate)a
|
||||
|
||||
def actions_for(:all), do: add_configurable_actions(@admin_actions, @owner_role)
|
||||
def actions_for({:custom, permissions}), do: permissions
|
||||
def actions_for(@owner_role), do: add_configurable_actions(@admin_actions, @owner_role)
|
||||
def actions_for(@admin_role), do: add_configurable_actions(@admin_actions, @admin_role)
|
||||
def actions_for(@bot_role), do: add_configurable_actions(@bot_actions, @bot_role)
|
||||
def actions_for(@developer_role), do: add_configurable_actions(@developer_actions, @developer_role)
|
||||
def actions_for(@reviewer_role), do: add_configurable_actions(@any_actions, @reviewer_role)
|
||||
def actions_for(role, target)
|
||||
|
||||
def add_configurable_actions(actions, role) do
|
||||
Enum.reduce(@configurable_actions, actions, fn action, actions ->
|
||||
if can?(role, action), do: [action | actions], else: actions
|
||||
def actions_for(:all, target), do: add_actions_with_target(@admin_actions, @owner_role, target)
|
||||
def actions_for({:custom, permissions}, _target), do: permissions
|
||||
def actions_for(@owner_role, target), do: add_actions_with_target(@admin_actions, @owner_role, target)
|
||||
def actions_for(@admin_role, target), do: add_actions_with_target(@admin_actions, @admin_role, target)
|
||||
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)
|
||||
|
||||
defp add_actions_with_target(actions, role, target) do
|
||||
Enum.reduce(@actions_with_target, actions, fn action, actions ->
|
||||
if can?(role, action, target), do: [action | actions], else: actions
|
||||
end)
|
||||
end
|
||||
|
||||
def can?(role, :machine_translations_translate_document) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
MachineTranslations.translate_list_enabled?()
|
||||
def can?(role, action, target \\ nil)
|
||||
|
||||
def can?(_role, :machine_translations_translate, nil), do: false
|
||||
|
||||
def can?(_role, :machine_translations_translate, project) do
|
||||
Accent.MachineTranslations.enabled?(project.machine_translations_config)
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_document), do: false
|
||||
|
||||
def can?(role, :machine_translations_translate_file) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
MachineTranslations.translate_list_enabled?()
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_file), do: false
|
||||
|
||||
def can?(role, :machine_translations_translate_text) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
MachineTranslations.translate_text_enabled?()
|
||||
end
|
||||
|
||||
def can?(_role, :machine_translations_translate_text), do: false
|
||||
|
||||
# Define abilities function at compile time to remove list lookup at runtime
|
||||
def can?(@owner_role, _action), do: true
|
||||
def can?(@owner_role, _action, _), do: true
|
||||
|
||||
def can?({:custom, permissions}, action) do
|
||||
def can?({:custom, permissions}, action, _) do
|
||||
to_string(action) in permissions
|
||||
end
|
||||
|
||||
for action <- @admin_actions do
|
||||
def can?(@admin_role, unquote(action)), do: true
|
||||
def can?(@admin_role, unquote(action), _), do: true
|
||||
end
|
||||
|
||||
for action <- @bot_actions do
|
||||
def can?(@bot_role, unquote(action)), do: true
|
||||
def can?(@bot_role, unquote(action), _), do: true
|
||||
end
|
||||
|
||||
for action <- @developer_actions do
|
||||
def can?(@developer_role, unquote(action)), do: true
|
||||
def can?(@developer_role, unquote(action), _), do: true
|
||||
end
|
||||
|
||||
for action <- @any_actions do
|
||||
def can?(@reviewer_role, unquote(action)), do: true
|
||||
def can?(@reviewer_role, unquote(action), _), do: true
|
||||
end
|
||||
|
||||
# Fallback if no permission has been found for the user on the project
|
||||
def can?(nil, _action), do: false
|
||||
def can?(nil, _action, _), do: false
|
||||
|
||||
def can?(role, action) do
|
||||
def can?(role, action, nil) do
|
||||
Logger.warn("Unauthorized action: #{action} for #{role}")
|
||||
false
|
||||
end
|
||||
|
||||
def can?(role, action, target) do
|
||||
Logger.warn("Unauthorized action: #{action} for #{role} on #{inspect(target)}")
|
||||
false
|
||||
end
|
||||
end
|
||||
|
@ -0,0 +1,32 @@
|
||||
defmodule Accent.MachineTranslationsConfigManager do
|
||||
alias Accent.Repo
|
||||
alias Ecto.Multi
|
||||
|
||||
import Ecto.Changeset
|
||||
|
||||
def save(project, params) do
|
||||
params = %{
|
||||
"machine_translations_config" => %{
|
||||
"config" => %{
|
||||
"key" => params[:config_key]
|
||||
},
|
||||
"provider" => params[:provider],
|
||||
"use_platform" => params[:use_platform]
|
||||
}
|
||||
}
|
||||
|
||||
changeset = cast(project, params, [:machine_translations_config])
|
||||
|
||||
Multi.new()
|
||||
|> Multi.update(:project, changeset)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
|
||||
def delete(project) do
|
||||
changeset = put_change(cast(project, %{}, []), :machine_translations_config, nil)
|
||||
|
||||
Multi.new()
|
||||
|> Multi.update(:project, changeset)
|
||||
|> Repo.transaction()
|
||||
end
|
||||
end
|
@ -1,6 +1,10 @@
|
||||
defmodule Accent.Project do
|
||||
use Accent.Schema
|
||||
|
||||
defmodule MachineTranslationsConfig do
|
||||
use Cloak.Ecto.Map, vault: Accent.Vault
|
||||
end
|
||||
|
||||
schema "projects" do
|
||||
field(:name, :string)
|
||||
field(:main_color, :string)
|
||||
@ -21,6 +25,8 @@ defmodule Accent.Project do
|
||||
has_many(:all_collaborators, Accent.Collaborator)
|
||||
belongs_to(:language, Accent.Language)
|
||||
|
||||
field :machine_translations_config, MachineTranslationsConfig
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
@ -236,7 +236,7 @@ defmodule Accent.TelemetryUI do
|
||||
header_color: "#28cb87",
|
||||
primary_color: "#28cb87",
|
||||
title: "Accent metrics",
|
||||
share_key: "0123456789123456",
|
||||
share_key: "012345678912345",
|
||||
logo: """
|
||||
<svg
|
||||
viewBox="0 0 480 480"
|
||||
|
21
lib/accent/vault.ex
Normal file
21
lib/accent/vault.ex
Normal file
@ -0,0 +1,21 @@
|
||||
defmodule Accent.Vault do
|
||||
use Cloak.Vault, otp_app: :accent, json_library: Jason
|
||||
|
||||
@impl GenServer
|
||||
def init(config) do
|
||||
config =
|
||||
Keyword.put(
|
||||
config,
|
||||
:ciphers,
|
||||
default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("MACHINE_TRANSLATIONS_VAULT_KEY")}
|
||||
)
|
||||
|
||||
{:ok, config}
|
||||
end
|
||||
|
||||
defp decode_env!(key_name) do
|
||||
key_name
|
||||
|> System.get_env()
|
||||
|> Base.decode64!()
|
||||
end
|
||||
end
|
@ -4,8 +4,12 @@ defmodule Accent.GraphQL.ErrorReporting do
|
||||
def run(%{result: %{errors: errors}, source: source} = blueprint, _) when not is_nil(errors) do
|
||||
Logger.error("""
|
||||
#{operation_name(Absinthe.Blueprint.current_operation(blueprint))}
|
||||
Errors: #{inspect(errors)}
|
||||
Source: #{inspect(source)}
|
||||
|
||||
Errors:
|
||||
#{inspect(errors)}
|
||||
|
||||
Source:
|
||||
#{inspect(source)}
|
||||
""")
|
||||
|
||||
{:ok, blueprint}
|
||||
|
@ -30,12 +30,12 @@ defmodule Accent.GraphQL.Helpers.Authorization do
|
||||
def project_authorize(action, func, id \\ :id) do
|
||||
fn
|
||||
project = %Project{}, args, info ->
|
||||
authorize(action, project.id, info, do: func.(project, args, info))
|
||||
authorize(action, project, info, do: func.(project, args, info))
|
||||
|
||||
_, args, info ->
|
||||
project = Repo.get(Project, args[id]) || %{id: nil}
|
||||
|
||||
authorize(action, project.id, info, do: func.(project, args, info))
|
||||
authorize(action, project, info, do: func.(project, args, info))
|
||||
end
|
||||
end
|
||||
|
||||
|
24
lib/graphql/mutations/machine_translations_config.ex
Normal file
24
lib/graphql/mutations/machine_translations_config.ex
Normal file
@ -0,0 +1,24 @@
|
||||
defmodule Accent.GraphQL.Mutations.MachineTranslationsConfig do
|
||||
use Absinthe.Schema.Notation
|
||||
|
||||
import Accent.GraphQL.Helpers.Authorization
|
||||
|
||||
alias Accent.GraphQL.Resolvers.MachineTranslationsConfig, as: Resolver
|
||||
|
||||
object :machine_translations_config_mutations do
|
||||
field :save_project_machine_translations_config, :mutated_project do
|
||||
arg(:project_id, non_null(:id))
|
||||
arg(:provider, non_null(:string))
|
||||
arg(:use_platform, non_null(:boolean))
|
||||
arg(:config_key, :string)
|
||||
|
||||
resolve(project_authorize(:save_project_machine_translations_config, &Resolver.save/3, :project_id))
|
||||
end
|
||||
|
||||
field :delete_project_machine_translations_config, :mutated_project do
|
||||
arg(:project_id, non_null(:id))
|
||||
|
||||
resolve(project_authorize(:delete_project_machine_translations_config, &Resolver.delete/3, :project_id))
|
||||
end
|
||||
end
|
||||
end
|
@ -1,8 +1,8 @@
|
||||
defmodule Accent.GraphQL.Plugins.Authorization do
|
||||
defmacro authorize(action, id, info, do: do_clause) do
|
||||
defmacro authorize(action, target, info, do: do_clause) do
|
||||
quote do
|
||||
with current_user when not is_nil(current_user) <- unquote(info).context[:conn].assigns[:current_user],
|
||||
true <- Canada.Can.can?(current_user, unquote(action), unquote(id)) do
|
||||
true <- Canada.Can.can?(current_user, unquote(action), unquote(target)) do
|
||||
unquote(do_clause)
|
||||
else
|
||||
_ -> {:ok, nil}
|
||||
|
@ -17,7 +17,10 @@ defmodule Accent.GraphQL.Resolvers.MachineTranslation do
|
||||
source_language = slug_language(project.id, args.source_language_slug)
|
||||
target_language = slug_language(project.id, args.target_language_slug)
|
||||
|
||||
{:ok, MachineTranslations.translate_text(args.text, source_language, target_language)}
|
||||
case MachineTranslations.translate([%{value: args.text}], source_language, target_language, project.machine_translations_config) do
|
||||
[%{value: text}] -> {:ok, %{text: text}}
|
||||
_ -> {:ok, nil}
|
||||
end
|
||||
end
|
||||
|
||||
defp slug_language(project_id, slug) do
|
||||
|
29
lib/graphql/resolvers/machine_translations_config.ex
Normal file
29
lib/graphql/resolvers/machine_translations_config.ex
Normal file
@ -0,0 +1,29 @@
|
||||
defmodule Accent.GraphQL.Resolvers.MachineTranslationsConfig do
|
||||
alias Accent.{
|
||||
MachineTranslationsConfigManager,
|
||||
Plugs.GraphQLContext,
|
||||
Project
|
||||
}
|
||||
|
||||
@spec save(Project.t(), any(), GraphQLContext.t()) :: {:ok, Project.t() | nil}
|
||||
def save(project, args, _info) do
|
||||
case MachineTranslationsConfigManager.save(project, args) do
|
||||
{:ok, %{project: project}} ->
|
||||
{:ok, %{project: project, errors: nil}}
|
||||
|
||||
{:error, _reason, _, _} ->
|
||||
{:ok, %{project: nil, errors: ["unprocessable_entity"]}}
|
||||
end
|
||||
end
|
||||
|
||||
@spec delete(Project.t(), any(), GraphQLContext.t()) :: {:ok, Project.t() | nil}
|
||||
def delete(project, _args, _info) do
|
||||
case MachineTranslationsConfigManager.delete(project) do
|
||||
{:ok, %{project: project}} ->
|
||||
{:ok, %{project: project, errors: nil}}
|
||||
|
||||
{:error, _reason, _, _} ->
|
||||
{:ok, %{project: nil, errors: ["unprocessable_entity"]}}
|
||||
end
|
||||
end
|
||||
end
|
@ -20,7 +20,7 @@ defmodule Accent.GraphQL.Resolvers.Permission do
|
||||
permissions =
|
||||
context[:conn].assigns[:current_user].permissions
|
||||
|> Map.get(project.id)
|
||||
|> Accent.RoleAbilities.actions_for()
|
||||
|> Accent.RoleAbilities.actions_for(project)
|
||||
|
||||
{:ok, permissions}
|
||||
end
|
||||
|
@ -56,6 +56,7 @@ defmodule Accent.GraphQL.Schema do
|
||||
mutation do
|
||||
# Mutation types
|
||||
import_types(Accent.GraphQL.Mutations.APIToken)
|
||||
import_types(Accent.GraphQL.Mutations.MachineTranslationsConfig)
|
||||
import_types(Accent.GraphQL.Mutations.Translation)
|
||||
import_types(Accent.GraphQL.Mutations.Comment)
|
||||
import_types(Accent.GraphQL.Mutations.Collaborator)
|
||||
@ -67,6 +68,7 @@ defmodule Accent.GraphQL.Schema do
|
||||
import_types(Accent.GraphQL.Mutations.Version)
|
||||
|
||||
import_fields(:api_token_mutations)
|
||||
import_fields(:machine_translations_config_mutations)
|
||||
import_fields(:comment_mutations)
|
||||
import_fields(:translation_mutations)
|
||||
import_fields(:collaborator_mutations)
|
||||
|
@ -12,10 +12,15 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
end
|
||||
|
||||
object :project_translated_text do
|
||||
field(:id, non_null(:id))
|
||||
field(:text, :string)
|
||||
end
|
||||
|
||||
object :machine_translations_config do
|
||||
field(:provider, non_null(:string))
|
||||
field(:use_platform, non_null(:boolean))
|
||||
field(:use_config_key, non_null(:boolean))
|
||||
end
|
||||
|
||||
object :project do
|
||||
field(:id, :id)
|
||||
field(:name, :string)
|
||||
@ -27,6 +32,21 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
field(:conflicts_count, non_null(:integer))
|
||||
field(:reviewed_count, non_null(:integer))
|
||||
|
||||
field(:machine_translations_config, :machine_translations_config,
|
||||
resolve: fn project, _, _ ->
|
||||
if project.machine_translations_config do
|
||||
{:ok,
|
||||
%{
|
||||
provider: project.machine_translations_config["provider"],
|
||||
use_platform: project.machine_translations_config["use_platform"],
|
||||
use_config_key: not is_nil(project.machine_translations_config["config"]["key"])
|
||||
}}
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
field :last_activity, :activity do
|
||||
arg(:action, :string)
|
||||
resolve(&Accent.GraphQL.Resolvers.Project.last_activity/3)
|
||||
@ -34,11 +54,11 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
|
||||
field(:is_file_operations_locked, non_null(:boolean), resolve: field_alias(:locked_file_operations))
|
||||
|
||||
field :translated_text, list_of(non_null(:project_translated_text)) do
|
||||
field :translated_text, :project_translated_text do
|
||||
arg(:text, non_null(:string))
|
||||
arg(:source_language_slug, non_null(:string))
|
||||
arg(:target_language_slug, non_null(:string))
|
||||
resolve(project_authorize(:machine_translations_translate_text, &Accent.GraphQL.Resolvers.MachineTranslation.translate_text/3))
|
||||
resolve(project_authorize(:machine_translations_translate, &Accent.GraphQL.Resolvers.MachineTranslation.translate_text/3))
|
||||
end
|
||||
|
||||
field :lint_translations, list_of(non_null(:lint_translation)) do
|
||||
|
@ -1,4 +0,0 @@
|
||||
defmodule Accent.MachineTranslations.Adapter do
|
||||
@callback translate_text(String.t(), String.t(), String.t(), Keyword.t()) :: {:ok, Accent.MachineTranslations.TranslatedText.t()} | any()
|
||||
@callback translate_list([String.t()], String.t(), String.t(), Keyword.t()) :: {:ok, [Accent.MachineTranslations.TranslatedText.t()]} | any()
|
||||
end
|
@ -1,198 +0,0 @@
|
||||
defmodule Accent.MachineTranslations.Adapter.GoogleTranslations do
|
||||
@behaviour Accent.MachineTranslations.Adapter
|
||||
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
alias Tesla.Middleware
|
||||
|
||||
@supported_languages ~w(
|
||||
af
|
||||
sq
|
||||
am
|
||||
ar
|
||||
hy
|
||||
az
|
||||
eu
|
||||
be
|
||||
bn
|
||||
bs
|
||||
bg
|
||||
ca
|
||||
ceb
|
||||
zh-CN
|
||||
zh
|
||||
zh-TW
|
||||
co
|
||||
hr
|
||||
cs
|
||||
da
|
||||
nl
|
||||
en
|
||||
eo
|
||||
et
|
||||
fi
|
||||
fr
|
||||
fy
|
||||
gl
|
||||
ka
|
||||
de
|
||||
el
|
||||
gu
|
||||
ht
|
||||
ha
|
||||
haw
|
||||
he
|
||||
iw
|
||||
hi
|
||||
hmn
|
||||
hu
|
||||
is
|
||||
ig
|
||||
id
|
||||
ga
|
||||
it
|
||||
ja
|
||||
jv
|
||||
kn
|
||||
kk
|
||||
km
|
||||
rw
|
||||
ko
|
||||
ku
|
||||
ky
|
||||
lo
|
||||
lv
|
||||
lt
|
||||
lb
|
||||
mk
|
||||
mg
|
||||
ms
|
||||
ml
|
||||
mt
|
||||
mi
|
||||
mr
|
||||
mn
|
||||
my
|
||||
ne
|
||||
no
|
||||
ny
|
||||
or
|
||||
ps
|
||||
fa
|
||||
pl
|
||||
pt
|
||||
pa
|
||||
ro
|
||||
ru
|
||||
sm
|
||||
gd
|
||||
sr
|
||||
st
|
||||
sn
|
||||
sd
|
||||
si
|
||||
sk
|
||||
sl
|
||||
so
|
||||
es
|
||||
su
|
||||
sw
|
||||
sv
|
||||
tl
|
||||
tg
|
||||
ta
|
||||
tt
|
||||
te
|
||||
th
|
||||
tr
|
||||
tk
|
||||
uk
|
||||
ur
|
||||
ug
|
||||
uz
|
||||
vi
|
||||
cy
|
||||
xh
|
||||
yi
|
||||
yo
|
||||
zu
|
||||
)
|
||||
|
||||
@impl Accent.MachineTranslations.Adapter
|
||||
def translate_text(content, source, target, config) do
|
||||
case translate_list([content], source, target, config) do
|
||||
{:ok, [translation]} -> {:ok, translation}
|
||||
error -> error
|
||||
end
|
||||
end
|
||||
|
||||
@impl Accent.MachineTranslations.Adapter
|
||||
def translate_list(contents, source, target, config) do
|
||||
target = to_language_code(target)
|
||||
source = to_language_code(source)
|
||||
|
||||
case Tesla.post(client(config), ":translateText", %{contents: contents, mimeType: "text/plain", sourceLanguageCode: source, targetLanguageCode: target}) do
|
||||
{:ok, %{body: %{"translations" => translations}}} ->
|
||||
{:ok, Enum.map(translations, &%TranslatedText{text: &1["translatedText"]})}
|
||||
|
||||
{:ok, %{status: status, body: body}} when status > 201 ->
|
||||
{:error, body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Auth do
|
||||
@behaviour Tesla.Middleware
|
||||
|
||||
@impl Tesla.Middleware
|
||||
def call(env, next, opts) do
|
||||
case auth_enabled?() && Goth.Token.for_scope(opts[:scope]) do
|
||||
{:ok, %{token: token, type: type}} ->
|
||||
env
|
||||
|> Tesla.put_header("authorization", type <> " " <> token)
|
||||
|> Tesla.run(next)
|
||||
|
||||
_ ->
|
||||
Tesla.run(env, next)
|
||||
end
|
||||
end
|
||||
|
||||
defp auth_enabled? do
|
||||
!Application.get_env(:goth, :disabled)
|
||||
end
|
||||
end
|
||||
|
||||
defp client(config) do
|
||||
project_id = project_id_from_config(config)
|
||||
|
||||
middlewares =
|
||||
List.flatten([
|
||||
{Middleware.Timeout, [timeout: :infinity]},
|
||||
{Middleware.BaseUrl, "https://translation.googleapis.com/v3/projects/#{project_id}"},
|
||||
{Auth, [scope: "https://www.googleapis.com/auth/cloud-translation"]},
|
||||
Middleware.DecodeJson,
|
||||
Middleware.EncodeJson,
|
||||
Middleware.Logger,
|
||||
Middleware.Telemetry
|
||||
])
|
||||
|
||||
Tesla.client(middlewares)
|
||||
end
|
||||
|
||||
defp project_id_from_config(config) do
|
||||
key = Keyword.fetch!(config, :key)
|
||||
Jason.decode!(key)["project_id"]
|
||||
end
|
||||
|
||||
defp to_language_code(language) when language in @supported_languages do
|
||||
language
|
||||
end
|
||||
|
||||
defp to_language_code(language) do
|
||||
case String.split(language, "-", parts: 2) do
|
||||
[prefix, _] when prefix in @supported_languages -> prefix
|
||||
_ -> :unsupported
|
||||
end
|
||||
end
|
||||
end
|
@ -1,53 +1,61 @@
|
||||
defmodule Accent.MachineTranslations do
|
||||
alias Accent.MachineTranslations.Provider
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
|
||||
@translation_chunk_size 500
|
||||
@translation_chunk_size 100
|
||||
@untranslatable_string_length_limit 2000
|
||||
@untranslatable_placeholder "_"
|
||||
@untranslatable_placeholder "__^__"
|
||||
|
||||
def translate_list_enabled? do
|
||||
not is_nil(provider_from_service(:translate_list))
|
||||
def enabled?(config) do
|
||||
provider = provider_from_config(config)
|
||||
Provider.enabled?(provider)
|
||||
end
|
||||
|
||||
def translate_text_enabled? do
|
||||
not Enum.empty?(provider_from_service(:translate_text))
|
||||
end
|
||||
@spec translate([map()], String.t(), String.t(), struct()) :: [map()]
|
||||
def translate(entries, source_language, target_language, config) do
|
||||
provider = provider_from_config(config)
|
||||
|
||||
def translate_text(text, source_language, target_language) do
|
||||
:translate_text
|
||||
|> provider_from_service()
|
||||
|> Enum.map(fn {module, config} ->
|
||||
case module.translate_text(text, source_language.slug, target_language.slug, config) do
|
||||
{:ok, %TranslatedText{} = translated} -> %{translated | id: to_id(text)}
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
def translate_entries(entries, source_language, target_language) do
|
||||
entries
|
||||
|> Enum.map(&filter_long_value/1)
|
||||
|> Enum.chunk_every(@translation_chunk_size)
|
||||
|> Enum.flat_map(fn entries ->
|
||||
values = Enum.map(entries, &filter_long_value/1)
|
||||
|> Enum.flat_map(fn values ->
|
||||
case Provider.translate(provider, values, source_language.slug, target_language.slug) do
|
||||
{:ok, translated_values} ->
|
||||
entries
|
||||
|> Enum.zip(translated_values)
|
||||
|> Enum.map(fn {entry, translated_text} ->
|
||||
case translated_text do
|
||||
%TranslatedText{text: @untranslatable_placeholder} -> entry
|
||||
%TranslatedText{text: text} -> %{entry | value: text}
|
||||
_ -> entry
|
||||
end
|
||||
end)
|
||||
|
||||
with {module, config} <- provider_from_service(:translate_list),
|
||||
{:ok, translated_values} <- module.translate_list(values, source_language.slug, target_language.slug, config) do
|
||||
entries
|
||||
|> Enum.zip(translated_values)
|
||||
|> Enum.map(fn {entry, translated_text} ->
|
||||
case translated_text do
|
||||
%TranslatedText{text: @untranslatable_placeholder} -> entry
|
||||
%TranslatedText{text: text} -> %{entry | value: text}
|
||||
_ -> entry
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> entries
|
||||
_ ->
|
||||
entries
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp provider_from_config(nil), do: %Provider.NotImplemented{}
|
||||
|
||||
defp provider_from_config(machine_translations_config) do
|
||||
struct_module =
|
||||
case machine_translations_config["provider"] do
|
||||
"google_translate" -> Provider.GoogleTranslate
|
||||
"deepl" -> Provider.Deepl
|
||||
_ -> Provider.NotImplemented
|
||||
end
|
||||
|
||||
struct!(struct_module, config: fetch_config(machine_translations_config))
|
||||
end
|
||||
|
||||
defp fetch_config(%{"provider" => provider, "use_platform" => true}) do
|
||||
Map.get(Application.get_env(:accent, __MODULE__)[:default_providers_config], provider)
|
||||
end
|
||||
|
||||
defp fetch_config(%{"config" => config}), do: config
|
||||
|
||||
defp filter_long_value(%{value: value}) when value in ["", nil], do: @untranslatable_placeholder
|
||||
|
||||
defp filter_long_value(entry) do
|
||||
@ -57,15 +65,4 @@ defmodule Accent.MachineTranslations do
|
||||
entry.value
|
||||
end
|
||||
end
|
||||
|
||||
defp to_id(text) do
|
||||
:md5
|
||||
|> :crypto.hash(text)
|
||||
|> :base64.encode_to_string()
|
||||
|> to_string()
|
||||
end
|
||||
|
||||
defp provider_from_service(service) do
|
||||
Application.get_env(:accent, __MODULE__)[service]
|
||||
end
|
||||
end
|
||||
|
96
lib/machine_translations/provider/deepl.ex
Normal file
96
lib/machine_translations/provider/deepl.ex
Normal file
@ -0,0 +1,96 @@
|
||||
defmodule Accent.MachineTranslations.Provider.Deepl do
|
||||
defstruct config: nil
|
||||
|
||||
defimpl Accent.MachineTranslations.Provider do
|
||||
@supported_languages ~w(
|
||||
bg
|
||||
cs
|
||||
da
|
||||
de
|
||||
el
|
||||
en
|
||||
es
|
||||
et
|
||||
fi
|
||||
fr
|
||||
hu
|
||||
id
|
||||
it
|
||||
ja
|
||||
ko
|
||||
lt
|
||||
lv
|
||||
nb
|
||||
nl
|
||||
pl
|
||||
pt
|
||||
ro
|
||||
ru
|
||||
sk
|
||||
sl
|
||||
sv
|
||||
tr
|
||||
uk
|
||||
zh
|
||||
)
|
||||
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
alias Tesla.Middleware
|
||||
|
||||
def enabled?(%{config: %{"key" => key}}), do: not is_nil(key)
|
||||
def enabled?(_), do: false
|
||||
|
||||
def translate(provider, contents, source, target) do
|
||||
target = String.upcase(to_language_code(target))
|
||||
source = String.upcase(to_language_code(source))
|
||||
|
||||
case Tesla.post(client(provider.config["key"]), "translate", %{text: contents, source_lang: source, target_lang: target}) do
|
||||
{:ok, %{body: %{"translations" => translations}}} ->
|
||||
{:ok, Enum.map(translations, &%TranslatedText{text: &1["text"]})}
|
||||
|
||||
{:ok, %{status: status, body: body}} when status > 201 ->
|
||||
{:error, body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Auth do
|
||||
@behaviour Tesla.Middleware
|
||||
|
||||
@impl Tesla.Middleware
|
||||
def call(env, next, opts) do
|
||||
env
|
||||
|> Tesla.put_header("authorization", "DeepL-Auth-Key #{opts[:key]}")
|
||||
|> Tesla.run(next)
|
||||
end
|
||||
end
|
||||
|
||||
defp client(key) do
|
||||
middlewares =
|
||||
List.flatten([
|
||||
{Middleware.Timeout, [timeout: :infinity]},
|
||||
{Middleware.BaseUrl, "https://api-free.deepl.com/v2/"},
|
||||
{Auth, [key: key]},
|
||||
Middleware.DecodeJson,
|
||||
Middleware.EncodeJson,
|
||||
Middleware.Logger,
|
||||
Middleware.Telemetry
|
||||
])
|
||||
|
||||
Tesla.client(middlewares)
|
||||
end
|
||||
|
||||
defp to_language_code(language) when language in @supported_languages do
|
||||
language
|
||||
end
|
||||
|
||||
defp to_language_code(language) do
|
||||
case String.split(language, "-", parts: 2) do
|
||||
[prefix, _] when prefix in @supported_languages -> prefix
|
||||
_ -> :unsupported
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
196
lib/machine_translations/provider/google_translate.ex
Normal file
196
lib/machine_translations/provider/google_translate.ex
Normal file
@ -0,0 +1,196 @@
|
||||
defmodule Accent.MachineTranslations.Provider.GoogleTranslate do
|
||||
defstruct config: nil
|
||||
|
||||
defimpl Accent.MachineTranslations.Provider do
|
||||
@supported_languages ~w(
|
||||
af
|
||||
sq
|
||||
am
|
||||
ar
|
||||
hy
|
||||
az
|
||||
eu
|
||||
be
|
||||
bn
|
||||
bs
|
||||
bg
|
||||
ca
|
||||
ceb
|
||||
zh-CN
|
||||
zh
|
||||
zh-TW
|
||||
co
|
||||
hr
|
||||
cs
|
||||
da
|
||||
nl
|
||||
en
|
||||
eo
|
||||
et
|
||||
fi
|
||||
fr
|
||||
fy
|
||||
gl
|
||||
ka
|
||||
de
|
||||
el
|
||||
gu
|
||||
ht
|
||||
ha
|
||||
haw
|
||||
he
|
||||
iw
|
||||
hi
|
||||
hmn
|
||||
hu
|
||||
is
|
||||
ig
|
||||
id
|
||||
ga
|
||||
it
|
||||
ja
|
||||
jv
|
||||
kn
|
||||
kk
|
||||
km
|
||||
rw
|
||||
ko
|
||||
ku
|
||||
ky
|
||||
lo
|
||||
lv
|
||||
lt
|
||||
lb
|
||||
mk
|
||||
mg
|
||||
ms
|
||||
ml
|
||||
mt
|
||||
mi
|
||||
mr
|
||||
mn
|
||||
my
|
||||
ne
|
||||
no
|
||||
ny
|
||||
or
|
||||
ps
|
||||
fa
|
||||
pl
|
||||
pt
|
||||
pa
|
||||
ro
|
||||
ru
|
||||
sm
|
||||
gd
|
||||
sr
|
||||
st
|
||||
sn
|
||||
sd
|
||||
si
|
||||
sk
|
||||
sl
|
||||
so
|
||||
es
|
||||
su
|
||||
sw
|
||||
sv
|
||||
tl
|
||||
tg
|
||||
ta
|
||||
tt
|
||||
te
|
||||
th
|
||||
tr
|
||||
tk
|
||||
uk
|
||||
ur
|
||||
ug
|
||||
uz
|
||||
vi
|
||||
cy
|
||||
xh
|
||||
yi
|
||||
yo
|
||||
zu
|
||||
)
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
alias Tesla.Middleware
|
||||
|
||||
def enabled?(%{config: %{"key" => key}}) do
|
||||
not is_nil(key) and match?({:ok, %{"project_id" => _}}, Jason.decode(key))
|
||||
end
|
||||
|
||||
def enabled?(_), do: false
|
||||
|
||||
def translate(provider, contents, source, target) do
|
||||
target = to_language_code(target)
|
||||
source = to_language_code(source)
|
||||
|
||||
case Tesla.post(client(provider.config), ":translateText", %{contents: contents, mimeType: "text/plain", sourceLanguageCode: source, targetLanguageCode: target}) do
|
||||
{:ok, %{body: %{"translations" => translations}}} ->
|
||||
{:ok, Enum.map(translations, &%TranslatedText{text: &1["translatedText"]})}
|
||||
|
||||
{:ok, %{status: status, body: body}} when status > 201 ->
|
||||
{:error, body}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Auth do
|
||||
@behaviour Tesla.Middleware
|
||||
|
||||
@impl Tesla.Middleware
|
||||
def call(env, next, opts) do
|
||||
case auth_enabled?() && Goth.Token.for_scope(opts[:scope]) do
|
||||
{:ok, %{token: token, type: type}} ->
|
||||
env
|
||||
|> Tesla.put_header("authorization", type <> " " <> token)
|
||||
|> Tesla.run(next)
|
||||
|
||||
_ ->
|
||||
Tesla.run(env, next)
|
||||
end
|
||||
end
|
||||
|
||||
defp auth_enabled? do
|
||||
!Application.get_env(:goth, :disabled)
|
||||
end
|
||||
end
|
||||
|
||||
defp client(config) do
|
||||
project_id = project_id_from_config(config)
|
||||
|
||||
middlewares =
|
||||
List.flatten([
|
||||
{Middleware.Timeout, [timeout: :infinity]},
|
||||
{Middleware.BaseUrl, "https://translation.googleapis.com/v3/projects/#{project_id}"},
|
||||
{Auth, [scope: "https://www.googleapis.com/auth/cloud-translation"]},
|
||||
Middleware.DecodeJson,
|
||||
Middleware.EncodeJson,
|
||||
Middleware.Logger,
|
||||
Middleware.Telemetry
|
||||
])
|
||||
|
||||
Tesla.client(middlewares)
|
||||
end
|
||||
|
||||
defp project_id_from_config(config) do
|
||||
key = Map.fetch!(config, "key")
|
||||
Jason.decode!(key)["project_id"]
|
||||
end
|
||||
|
||||
defp to_language_code(language) when language in @supported_languages do
|
||||
language
|
||||
end
|
||||
|
||||
defp to_language_code(language) do
|
||||
case String.split(language, "-", parts: 2) do
|
||||
[prefix, _] when prefix in @supported_languages -> prefix
|
||||
_ -> :unsupported
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
8
lib/machine_translations/provider/not_implemented.ex
Normal file
8
lib/machine_translations/provider/not_implemented.ex
Normal file
@ -0,0 +1,8 @@
|
||||
defmodule Accent.MachineTranslations.Provider.NotImplemented do
|
||||
defstruct config: nil
|
||||
|
||||
defimpl Accent.MachineTranslations.Provider do
|
||||
def enabled?(_provider), do: false
|
||||
def translate(_provider, entries, _source_language, _target_language), do: entries
|
||||
end
|
||||
end
|
4
lib/machine_translations/provider/provider.ex
Normal file
4
lib/machine_translations/provider/provider.ex
Normal file
@ -0,0 +1,4 @@
|
||||
defprotocol Accent.MachineTranslations.Provider do
|
||||
def enabled?(provider)
|
||||
def translate(provider, entries, source_language, target_language)
|
||||
end
|
@ -1,3 +1,3 @@
|
||||
defmodule Accent.MachineTranslations.TranslatedText do
|
||||
defstruct id: nil, text: nil
|
||||
defstruct text: nil
|
||||
end
|
||||
|
@ -7,13 +7,12 @@ defmodule Accent.MachineTranslationsController do
|
||||
alias Accent.Scopes.Revision, as: RevisionScope
|
||||
alias Accent.Scopes.Translation, as: TranslationScope
|
||||
|
||||
plug(Plug.Assign, %{canary_action: :machine_translations_translate_file} when action === :translate_file)
|
||||
plug(Plug.Assign, %{canary_action: :machine_translations_translate_document} when action === :translate_document)
|
||||
plug(Plug.Assign, %{canary_action: :machine_translations_translate})
|
||||
plug(:load_and_authorize_resource, model: Project, id_name: "project_id")
|
||||
plug(:fetch_master_revision)
|
||||
plug(:load_resource, model: Language, id_name: "language", as: :source_language)
|
||||
plug(:load_resource, model: Language, id_name: "to_language_id", as: :target_language)
|
||||
plug(:load_resource, model: Document, id_name: "document_id", as: :document, only: [:machine_translations_translate_document])
|
||||
plug(:load_resource, model: Document, id_name: "document_id", as: :document, only: [:machine_translations_translate])
|
||||
plug(:fetch_order when action === :translate_document)
|
||||
plug(:fetch_format when action === :translate_document)
|
||||
plug(Accent.Plugs.MovementContextParser when action === :translate_file)
|
||||
@ -54,10 +53,11 @@ defmodule Accent.MachineTranslationsController do
|
||||
|> Enum.map(&Translation.to_langue_entry(&1, nil, true, conn.assigns[:source_language].slug))
|
||||
|
||||
entries =
|
||||
Accent.MachineTranslations.translate_entries(
|
||||
Accent.MachineTranslations.translate(
|
||||
entries,
|
||||
conn.assigns[:source_language],
|
||||
conn.assigns[:target_language]
|
||||
conn.assigns[:target_language],
|
||||
conn.assigns[:project].machine_translations_config
|
||||
)
|
||||
|
||||
%{render: render} =
|
||||
@ -97,10 +97,11 @@ defmodule Accent.MachineTranslationsController do
|
||||
"""
|
||||
def translate_file(conn, _params) do
|
||||
entries =
|
||||
Accent.MachineTranslations.translate_entries(
|
||||
Accent.MachineTranslations.translate(
|
||||
conn.assigns[:movement_context].entries,
|
||||
conn.assigns[:source_language],
|
||||
conn.assigns[:target_language]
|
||||
conn.assigns[:target_language],
|
||||
conn.assigns[:project].machine_translations_config
|
||||
)
|
||||
|
||||
%{render: render} =
|
||||
|
3
mix.exs
3
mix.exs
@ -54,6 +54,7 @@ defmodule Accent.Mixfile do
|
||||
{:ecto_sql, "~> 3.2"},
|
||||
{:ecto_dev_logger, "~> 0.4"},
|
||||
{:postgrex, "~> 0.14"},
|
||||
{:cloak_ecto, "~> 1.2"},
|
||||
|
||||
# Phoenix data helpers
|
||||
{:phoenix_ecto, "~> 4.0"},
|
||||
@ -100,7 +101,7 @@ defmodule Accent.Mixfile do
|
||||
{:new_relic_agent, "~> 1.27"},
|
||||
{:new_relic_absinthe, "~> 0.0"},
|
||||
{:telemetry, "~> 1.0", override: true},
|
||||
{:telemetry_ui, path: "../mirego/telemetry_ui"},
|
||||
{:telemetry_ui, "~> 2.0"},
|
||||
|
||||
# Mock testing
|
||||
{:mox, "~> 0.3", only: :test},
|
||||
|
4
mix.lock
4
mix.lock
@ -9,6 +9,8 @@
|
||||
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f348d9848693c830a65b707bba9e4dfdd6434e8c356a8d4477e4535afb0d653b"},
|
||||
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
|
||||
"cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"corsica": {:hex, :corsica, "1.3.0", "bbec02ccbeca1fdf44ee23b25a8ae32f7c6c28fc127ef8836dd8420e8f65bd9b", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8847ec817554047e9aa6d9933539cacb10c4ee60b58e0c15c3b380c5b737b35f"},
|
||||
@ -61,7 +63,7 @@
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.18.11", "c50eac83dae6b5488859180422dfb27b2c609de87f4aa5b9c926ecd0501cd44f", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "76c99a0ffb47cd95bf06a917e74f282a603f3e77b00375f3c2dd95110971b102"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.0", "c57bc5044f25f007dc86ab21895688c098a9f846a8dda6bc40e2d0ddc146e38f", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "1b066f99a26fd22064c12b2600a9a6e56700f591bf7b20b418054ea38b4d4357"},
|
||||
"phoenix_template": {:hex, :phoenix_template, "1.0.1", "85f79e3ad1b0180abb43f9725973e3b8c2c3354a87245f91431eec60553ed3ef", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "157dc078f6226334c91cb32c1865bf3911686f8bcd6bcff86736f6253e6993ee"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "2.0.2", "6bd4d2fd595ef80d33b439ede6a19326b78f0f1d8d62b9a318e3d9c1af351098", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}], "hexpm", "a929e7230ea5c7ee0e149ffcf44ce7cf7f4b6d2bfe1752dd7c084cdff152d36f"},
|
||||
"php_assoc_map": {:hex, :php_assoc_map, "0.5.2", "8b55283c2ffa762f8703cb30ef40085bf5c06ee08d5a82e38405fa4b949b2a6b", [:mix], [], "hexpm", "c95f27f74075cdd5908e4217db96887709334a9fe1da30fc98706c225f3ceafd"},
|
||||
"plug": {:hex, :plug, "1.10.4", "41eba7d1a2d671faaf531fa867645bd5a3dce0957d8e2a3f398ccff7d2ef017f", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "ad1e233fe73d2eec56616568d260777b67f53148a999dc2d048f4eb9778fe4a0"},
|
||||
|
@ -0,0 +1,9 @@
|
||||
defmodule Accent.Repo.Migrations.AddMachineTranslationsConfigOnProject do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:projects) do
|
||||
add(:machine_translations_config, :binary)
|
||||
end
|
||||
end
|
||||
end
|
@ -368,7 +368,17 @@
|
||||
"manage_languages": "Manage languages",
|
||||
"manage_languages_text": "Add/remove languages translated in the project",
|
||||
"jipt": "Just In Place Translations",
|
||||
"jipt_text": "Live translation on any applications, with builtin Javascript library"
|
||||
"jipt_text": "Live translation on any applications, with builtin Javascript library",
|
||||
"machine_translations": "Machine translations",
|
||||
"machine_translations_text": "Configure an external service to translate languages automatically"
|
||||
},
|
||||
"machine_translations": {
|
||||
"title": "Machine translations",
|
||||
"text": "Add a valid configuration to enable translating directly in the Review page. You can also translate your project’s file as well as arbirtary files that you upload on Accent.",
|
||||
"use_platform_label": "Use the platform config if available",
|
||||
"config_key_help_present": "Config key has been set but the value will never be exposed via the API.",
|
||||
"config_key_help_not_present": "Once saved, the config key will never be shown.",
|
||||
"config_key_placeholder": "Paste the config key here…"
|
||||
},
|
||||
"delete_form": {
|
||||
"title": "Danger zone",
|
||||
@ -868,6 +878,10 @@
|
||||
"BOT": "API Token",
|
||||
"REVIEWER": "Reviewer"
|
||||
},
|
||||
"machine_translations_providers": {
|
||||
"google_translate": "Google Translate",
|
||||
"deepl": "DeepL"
|
||||
},
|
||||
"integration_services": {
|
||||
"DISCORD": "Discord",
|
||||
"GITHUB": "GitHub",
|
||||
@ -976,6 +990,8 @@
|
||||
"integration_update_success": "The integration has been updated with success",
|
||||
"integration_remove_error": "The integration could not be removed",
|
||||
"integration_remove_success": "The integration has been removed with success",
|
||||
"machine_translations_config_error": "The machine translations config could not be saved",
|
||||
"machine_translations_config_success": "The machine translations config has been saved with success",
|
||||
"update_error": "The project could not be updated",
|
||||
"update_success": "The project has been updated with success",
|
||||
"delete_error": "The project could not be deleted",
|
||||
@ -994,6 +1010,9 @@
|
||||
"jipt": {
|
||||
"loading_content": "Fetching project’s Just In Place Translations settings…"
|
||||
},
|
||||
"machine_translations": {
|
||||
"loading_content": "Fetching project’s machine translations settings…"
|
||||
},
|
||||
"service_integrations": {
|
||||
"loading_content": "Fetching project’s service & integrations…"
|
||||
}
|
||||
|
@ -22,7 +22,7 @@
|
||||
{{inline-svg "assets/eye.svg" local-class="item-actions-icon"}}
|
||||
</LinkTo>
|
||||
|
||||
{{#if (get @permissions "machine_translations_translate_text")}}
|
||||
{{#if (get @permissions "machine_translations_translate")}}
|
||||
<button
|
||||
title={{t "components.conflict_item.translate_tooltip"}}
|
||||
class="tooltip tooltip--top"
|
||||
|
@ -14,6 +14,7 @@
|
||||
{{else}}
|
||||
<div local-class="form">
|
||||
{{inline-svg this.logoService local-class="logo"}}
|
||||
|
||||
<AccSelect
|
||||
@searchEnabled={{false}}
|
||||
@selected={{this.serviceValue}}
|
||||
|
@ -60,6 +60,7 @@
|
||||
opacity: 0.6;
|
||||
stroke: var(--color-black);
|
||||
|
||||
&.link-icon--machine-translations path:nth-child(2),
|
||||
&.link-icon--jipt path:nth-child(2),
|
||||
&.link-icon--integrations circle:nth-child(3),
|
||||
&.link-icon--badges path:nth-child(2) {
|
||||
|
@ -23,5 +23,11 @@
|
||||
<strong>{{t "components.project_settings.links_list.jipt"}}</strong>
|
||||
<p>{{t "components.project_settings.links_list.jipt_text"}}</p>
|
||||
</LinkTo>
|
||||
|
||||
<LinkTo @route="logged-in.project.edit.machine-translations" local-class="link">
|
||||
{{inline-svg "assets/machine-translations.svg" local-class="link-icon link-icon--machine-translations"}}
|
||||
<strong>{{t "components.project_settings.links_list.machine_translations"}}</strong>
|
||||
<p>{{t "components.project_settings.links_list.machine_translations_text"}}</p>
|
||||
</LinkTo>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -0,0 +1,112 @@
|
||||
import {action} from '@ember/object';
|
||||
import {inject as service} from '@ember/service';
|
||||
import Component from '@glimmer/component';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import GlobalState from 'accent-webapp/services/global-state';
|
||||
import FlashMessages from 'ember-cli-flash/services/flash-messages';
|
||||
import {dropTask} from 'ember-concurrency-decorators';
|
||||
import {taskFor} from 'ember-concurrency-ts';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
|
||||
interface Args {
|
||||
project: any;
|
||||
onDelete: () => void;
|
||||
onSave: ({
|
||||
provider,
|
||||
usePlatform,
|
||||
configKey,
|
||||
}: {
|
||||
provider: string;
|
||||
usePlatform: boolean;
|
||||
configKey: string | null;
|
||||
}) => Promise<any>;
|
||||
}
|
||||
|
||||
const PROVIDERS = ['google_translate', 'deepl'];
|
||||
|
||||
const LOGOS = {
|
||||
deepl: 'assets/machine_translations_providers/deepl.svg',
|
||||
google_translate:
|
||||
'assets/machine_translations_providers/google_translate.svg',
|
||||
};
|
||||
|
||||
export default class ProjectSettingsMachineTranslations extends Component<Args> {
|
||||
@service('global-state')
|
||||
globalState: GlobalState;
|
||||
|
||||
@service('flash-messages')
|
||||
flashMessages: FlashMessages;
|
||||
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@tracked
|
||||
provider =
|
||||
this.args.project.machineTranslationsConfig?.provider || 'google_translate';
|
||||
|
||||
@tracked
|
||||
usePlatform =
|
||||
this.args.project.machineTranslationsConfig?.usePlatform || false;
|
||||
|
||||
@tracked
|
||||
configKey: any;
|
||||
|
||||
get providerValue() {
|
||||
return this.mappedProviders.find(({value}) => value === this.provider);
|
||||
}
|
||||
|
||||
get isSubmitting() {
|
||||
return taskFor(this.submit).isRunning;
|
||||
}
|
||||
|
||||
get isRemoving() {
|
||||
return taskFor(this.remove).isRunning;
|
||||
}
|
||||
|
||||
get mappedProviders() {
|
||||
return PROVIDERS.map((value) => {
|
||||
return {
|
||||
label: this.intl.t(`general.machine_translations_providers.${value}`),
|
||||
value,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
get logoProvider() {
|
||||
const provider: keyof typeof LOGOS = this.provider as any;
|
||||
|
||||
return LOGOS[provider];
|
||||
}
|
||||
|
||||
@action
|
||||
setProvider({value}: {value: string}) {
|
||||
this.provider = value;
|
||||
}
|
||||
|
||||
@action
|
||||
onUsePlatformChange(event: InputEvent) {
|
||||
const checked = (event.target as HTMLInputElement).checked;
|
||||
|
||||
if (checked) this.configKey = null;
|
||||
this.usePlatform = checked;
|
||||
}
|
||||
|
||||
@action
|
||||
onConfigKeyChange(event: InputEvent) {
|
||||
this.configKey = (event.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*submit() {
|
||||
yield this.args.onSave({
|
||||
provider: this.provider,
|
||||
usePlatform: this.usePlatform,
|
||||
configKey: this.configKey,
|
||||
});
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*remove() {
|
||||
yield this.args.onDelete();
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
.project-settings-form {
|
||||
margin-top: 25px;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-top: 25px;
|
||||
padding: 12px;
|
||||
border-radius: 3px;
|
||||
background: var(--background-light);
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-grow: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.select select {
|
||||
background: var(--body-background);
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
padding: 7px 10px;
|
||||
font-weight: bold;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.text {
|
||||
max-width: 490px;
|
||||
margin: 10px 0 15px;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 25px;
|
||||
}
|
||||
|
||||
.config-key-help {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: var(--color-warning);
|
||||
padding: 6px 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.provider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.textInput {
|
||||
@extend %textInput;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 0;
|
||||
padding: 8px 10px;
|
||||
margin-right: 10px;
|
||||
width: 100%;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.platform-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<div local-class="project-settings-form">
|
||||
<ProjectSettings::Title
|
||||
@title={{t "components.project_settings.machine_translations.title"}}
|
||||
/>
|
||||
|
||||
<p local-class="text">
|
||||
{{t "components.project_settings.machine_translations.text"}}
|
||||
</p>
|
||||
|
||||
<div local-class="form">
|
||||
<div local-class="provider">
|
||||
{{inline-svg this.logoProvider local-class="logo"}}
|
||||
|
||||
<AccSelect
|
||||
local-class="select"
|
||||
@searchEnabled={{false}}
|
||||
@selected={{this.providerValue}}
|
||||
@renderInPlace={{true}}
|
||||
@options={{this.mappedProviders}}
|
||||
@onchange={{fn this.setProvider}}
|
||||
/>
|
||||
<label local-class="platform-checkbox">
|
||||
|
||||
<input
|
||||
value={{this.usePlatform}}
|
||||
type="checkbox"
|
||||
checked={{this.usePlatform}}
|
||||
{{on "change" (fn this.onUsePlatformChange)}}
|
||||
>
|
||||
{{t "components.project_settings.machine_translations.use_platform_label"}}
|
||||
</label>
|
||||
|
||||
</div>
|
||||
|
||||
{{#unless this.usePlatform}}
|
||||
|
||||
{{#if @project.machineTranslationsConfig.useConfigKey}}
|
||||
<p local-class="config-key-help">
|
||||
{{t "components.project_settings.machine_translations.config_key_help_present"}}
|
||||
</p>
|
||||
{{else}}
|
||||
<p local-class="config-key-help">
|
||||
{{t "components.project_settings.machine_translations.config_key_help_not_present"}}
|
||||
</p>
|
||||
{{/if}}
|
||||
|
||||
<textarea
|
||||
placeholder={{if @project.machineTranslationsConfig.useConfigKey "••••••••••••••" (t "components.project_settings.machine_translations.config_key_placeholder")}}
|
||||
local-class="textInput"
|
||||
rows={{if @project.machineTranslationsConfig.useConfigKey 1 8}}
|
||||
{{on "change" (fn this.onConfigKeyChange)}}
|
||||
>{{this.configKey}}</textarea>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<div local-class="actions">
|
||||
{{#if @project.machineTranslationsConfig}}
|
||||
<AsyncButton
|
||||
@onClick={{perform this.remove}}
|
||||
@loading={{this.isRemoving}}
|
||||
class="button button--red button--borderless"
|
||||
>
|
||||
{{t "components.project_settings.integrations.delete"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
|
||||
<AsyncButton
|
||||
@onClick={{perform this.submit}}
|
||||
@loading={{this.isSubmitting}}
|
||||
class="button button--filled"
|
||||
>
|
||||
{{t "components.project_settings.integrations.save"}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,93 @@
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {readOnly, equal, and} from '@ember/object/computed';
|
||||
import Controller from '@ember/controller';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
import GlobalState from 'accent-webapp/services/global-state';
|
||||
import FlashMessages from 'ember-cli-flash/services/flash-messages';
|
||||
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
|
||||
|
||||
import machineTranslationsConfigSaveQuery from 'accent-webapp/queries/save-project-machine-translations-config';
|
||||
import machineTranslationsConfigDeleteQuery from 'accent-webapp/queries/delete-project-machine-translations-config';
|
||||
|
||||
const FLASH_MESSAGE_PREFIX = 'pods.project.edit.flash_messages.';
|
||||
const FLASH_MESSAGE_CONFIG_SUCCESS = `${FLASH_MESSAGE_PREFIX}machine_translations_config_success`;
|
||||
const FLASH_MESSAGE_CONFIG_ERROR = `${FLASH_MESSAGE_PREFIX}machine_translations_config_error`;
|
||||
const FLASH_MESSAGE_CONFIG_REMOVE_SUCCESS = `${FLASH_MESSAGE_PREFIX}machine_translations_config_remove_success`;
|
||||
const FLASH_MESSAGE_CONFIG_REMOVE_ERROR = `${FLASH_MESSAGE_PREFIX}machine_translations_config_remove_error`;
|
||||
|
||||
export default class MachineTranslationController extends Controller {
|
||||
@service('intl')
|
||||
intl: IntlService;
|
||||
|
||||
@service('global-state')
|
||||
globalState: GlobalState;
|
||||
|
||||
@service('apollo-mutate')
|
||||
apolloMutate: ApolloMutate;
|
||||
|
||||
@service('flash-messages')
|
||||
flashMessages: FlashMessages;
|
||||
|
||||
@readOnly('model.project')
|
||||
project: any;
|
||||
|
||||
@readOnly('globalState.permissions')
|
||||
permissions: any;
|
||||
|
||||
@equal('model.project.name', undefined)
|
||||
emptyData: boolean;
|
||||
|
||||
@and('emptyData', 'model.loading')
|
||||
showLoading: boolean;
|
||||
|
||||
@action
|
||||
async saveMachineTranslationsConfig(config: {
|
||||
provider: string;
|
||||
usePlatform: boolean;
|
||||
configKey: string | null;
|
||||
}) {
|
||||
return this.mutateResource({
|
||||
mutation: machineTranslationsConfigSaveQuery,
|
||||
successMessage: FLASH_MESSAGE_CONFIG_SUCCESS,
|
||||
errorMessage: FLASH_MESSAGE_CONFIG_ERROR,
|
||||
variables: {projectId: this.project.id, ...config},
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
async deleteMachineTranslationsConfig() {
|
||||
return this.mutateResource({
|
||||
mutation: machineTranslationsConfigDeleteQuery,
|
||||
successMessage: FLASH_MESSAGE_CONFIG_REMOVE_SUCCESS,
|
||||
errorMessage: FLASH_MESSAGE_CONFIG_REMOVE_ERROR,
|
||||
variables: {projectId: this.project.id},
|
||||
});
|
||||
}
|
||||
|
||||
private async mutateResource({
|
||||
mutation,
|
||||
variables,
|
||||
successMessage,
|
||||
errorMessage,
|
||||
}: {
|
||||
mutation: any;
|
||||
variables: any;
|
||||
successMessage: string;
|
||||
errorMessage: string;
|
||||
}) {
|
||||
const response = await this.apolloMutate.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
refetchQueries: ['ProjectMachineTranslationsConfig'],
|
||||
});
|
||||
|
||||
if (response.errors) {
|
||||
this.flashMessages.error(this.intl.t(errorMessage));
|
||||
} else {
|
||||
this.flashMessages.success(this.intl.t(successMessage));
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
@ -0,0 +1,44 @@
|
||||
import {inject as service} from '@ember/service';
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
import projectMachineTranslationsConfigQuery from 'accent-webapp/queries/project-machine-translations-config';
|
||||
import RouteParams from 'accent-webapp/services/route-params';
|
||||
import ApolloSubscription, {
|
||||
Subscription,
|
||||
} from 'accent-webapp/services/apollo-subscription';
|
||||
import Transition from '@ember/routing/-private/transition';
|
||||
|
||||
export default class MachineTranslationsRoute extends Route {
|
||||
@service('apollo-subscription')
|
||||
apolloSubscription: ApolloSubscription;
|
||||
|
||||
@service('route-params')
|
||||
routeParams: RouteParams;
|
||||
|
||||
subscription: Subscription;
|
||||
|
||||
model(_params: any, transition: Transition) {
|
||||
this.subscription = this.apolloSubscription.graphql(
|
||||
() => this.modelFor(this.routeName),
|
||||
projectMachineTranslationsConfigQuery,
|
||||
{
|
||||
props: (data) => ({
|
||||
project: data.viewer.project,
|
||||
}),
|
||||
options: {
|
||||
fetchPolicy: 'cache-and-network',
|
||||
variables: {
|
||||
projectId: this.routeParams.fetch(transition, 'logged-in.project')
|
||||
.projectId,
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
return this.subscription.currentResult();
|
||||
}
|
||||
|
||||
deactivate() {
|
||||
this.apolloSubscription.clearSubscription(this.subscription);
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
{{#if this.showLoading}}
|
||||
<LoadingContent @label={{t "pods.project.edit.machine_translations.loading_content"}} />
|
||||
{{else}}
|
||||
<ProjectSettings::BackLink @project={{this.project}} />
|
||||
|
||||
<ProjectSettings::MachineTranslations
|
||||
@project={{this.project}}
|
||||
@onSave={{fn this.saveMachineTranslationsConfig}}
|
||||
@onDelete={{fn this.deleteMachineTranslationsConfig}}
|
||||
/>
|
||||
{{/if}}
|
@ -22,7 +22,7 @@
|
||||
<DocumentsAddButton @project={{this.model.project}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if (get this.permissions "machine_translations_translate_file")}}
|
||||
{{#if (get this.permissions "machine_translations_translate")}}
|
||||
<DocumentsMachineTranslationsButton @project={{this.model.project}} />
|
||||
{{/if}}
|
||||
|
||||
|
@ -84,8 +84,8 @@ export default class ConflictsController extends Controller {
|
||||
},
|
||||
});
|
||||
|
||||
if (data.viewer?.project?.translatedText?.[0]) {
|
||||
return data.viewer.project.translatedText[0];
|
||||
if (data.viewer?.project?.translatedText) {
|
||||
return data.viewer.project.translatedText;
|
||||
} else {
|
||||
return {text: null};
|
||||
}
|
||||
|
@ -0,0 +1,25 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export interface DeleteProjectMachineTranslationsConfigVariables {
|
||||
projectId: string;
|
||||
}
|
||||
|
||||
export interface DeleteProjectMachineTranslationsConfigResponse {
|
||||
project: {
|
||||
id: string;
|
||||
};
|
||||
|
||||
errors: any;
|
||||
}
|
||||
|
||||
export default gql`
|
||||
mutation ProjectMachineTranslationsConfigDelete($projectId: ID!) {
|
||||
deleteProjectMachineTranslationsConfig(projectId: $projectId) {
|
||||
project {
|
||||
id
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
`;
|
16
webapp/app/queries/project-machine-translations-config.ts
Normal file
16
webapp/app/queries/project-machine-translations-config.ts
Normal file
@ -0,0 +1,16 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export default gql`
|
||||
query ProjectMachineTranslationsConfig($projectId: ID!) {
|
||||
viewer {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
machineTranslationsConfig {
|
||||
provider
|
||||
usePlatform
|
||||
useConfigKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -0,0 +1,38 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export interface SaveProjectMachineTranslationsConfigVariables {
|
||||
name: string;
|
||||
projectId: string;
|
||||
pictureUrl: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
export interface SaveProjectMachineTranslationsConfigResponse {
|
||||
project: {
|
||||
id: string;
|
||||
};
|
||||
|
||||
errors: any;
|
||||
}
|
||||
|
||||
export default gql`
|
||||
mutation ProjectMachineTranslationsConfigSave(
|
||||
$provider: String!
|
||||
$configKey: String
|
||||
$usePlatform: Boolean!
|
||||
$projectId: ID!
|
||||
) {
|
||||
saveProjectMachineTranslationsConfig(
|
||||
provider: $provider
|
||||
configKey: $configKey
|
||||
usePlatform: $usePlatform
|
||||
projectId: $projectId
|
||||
) {
|
||||
project {
|
||||
id
|
||||
}
|
||||
|
||||
errors
|
||||
}
|
||||
}
|
||||
`;
|
@ -15,7 +15,6 @@ export default gql`
|
||||
sourceLanguageSlug: $sourceLanguageSlug
|
||||
targetLanguageSlug: $targetLanguageSlug
|
||||
) {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ export default Router.map(function () {
|
||||
this.route('api-token');
|
||||
this.route('service-integrations');
|
||||
this.route('jipt');
|
||||
this.route('machine-translations');
|
||||
});
|
||||
|
||||
this.route('collaborators');
|
||||
|
@ -10,16 +10,20 @@ export default class ApolloMutate extends Service {
|
||||
apollo: Apollo;
|
||||
|
||||
async mutate(args: any) {
|
||||
const {data} = await this.apollo.client.mutate(args);
|
||||
const operationName = Object.keys(data)[0];
|
||||
try {
|
||||
const {data} = await this.apollo.client.mutate(args);
|
||||
const operationName = Object.keys(data)[0];
|
||||
|
||||
if (!data[operationName]?.errors?.length) {
|
||||
data[operationName].errors = null;
|
||||
if (!data[operationName]?.errors?.length) {
|
||||
data[operationName].errors = null;
|
||||
|
||||
return data[operationName];
|
||||
}
|
||||
|
||||
return data[operationName];
|
||||
} catch {
|
||||
return {errors: ['internal_server_error'], data: null};
|
||||
}
|
||||
|
||||
return data[operationName];
|
||||
}
|
||||
}
|
||||
|
||||
|
15
webapp/public/assets/machine-translations.svg
Normal file
15
webapp/public/assets/machine-translations.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M4 17l9.21-9.244C14.405 6.306 15 5.054 15 4M1 3.5h15M8.5 1v3M6.234 9c-.877 4.463.712 6.796 4.766 7"
|
||||
/>
|
||||
<path stroke-linejoin="round" d="M14 21.87L18.584 12 23 22" />
|
||||
<path stroke-linejoin="bevel" d="M16 18.5h7" />
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
@ -0,0 +1,23 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 54 68"
|
||||
>
|
||||
<path
|
||||
fill="#0F2B46"
|
||||
d="M.188 17.274v26.81c0 1.393.73 2.67 1.921 3.367l23.063 13.386a3.797 3.797 0 0 0 3.844 0L52.078 47.45A3.887 3.887 0 0 0 54 44.085v-26.81c0-1.394-.73-2.67-1.922-3.367L29.016.522a3.797 3.797 0 0 0-3.844 0L2.109 13.947a3.871 3.871 0 0 0-1.921 3.327Z"
|
||||
/>
|
||||
<path fill="#0F2B46" d="m36.703 67.53-.038-5.803.038-5.339-13.453 3.327" />
|
||||
<path
|
||||
fill="#142C46"
|
||||
d="m36.088 55.924 2.537-.658-.961.542c-.577.348-.961.967-.961 1.663v1.084l-.615-2.631Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M17.79 18.474a3.95 3.95 0 0 1 5.535 0 4.016 4.016 0 0 1 0 5.804 3.95 3.95 0 0 1-5.535 0 4.016 4.016 0 0 1 0-5.804ZM35.087 28.572a3.95 3.95 0 0 1 5.535 0 4.016 4.016 0 0 1 0 5.803 3.95 3.95 0 0 1-5.535 0 4.016 4.016 0 0 1 0-5.803ZM17.79 39.25a3.95 3.95 0 0 1 5.535 0 4.016 4.016 0 0 1 0 5.803 3.95 3.95 0 0 1-5.535 0 4.016 4.016 0 0 1 0-5.803Z"
|
||||
/>
|
||||
<path
|
||||
fill="#fff"
|
||||
d="m22.48 23.542 11.532 6.693 1.922-1.083-11.532-6.732-1.922 1.122ZM34.78 35.148l-10.378 6.035-1.922-1.121 10.379-5.997 1.922 1.083Z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@ -0,0 +1,56 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 998.1 998.3"
|
||||
>
|
||||
<path
|
||||
fill="#DBDBDB"
|
||||
d="M931.7 998.3c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4H283.6l260.1 797.9h388z"
|
||||
/>
|
||||
<path
|
||||
fill="#DCDCDC"
|
||||
d="M931.7 230.4c9.7 0 18.9 3.8 25.8 10.6 6.8 6.7 10.6 15.5 10.6 24.8v667.1c0 9.3-3.7 18.1-10.6 24.8-6.9 6.8-16.1 10.6-25.8 10.6H565.5L324.9 230.4h606.8m0-30H283.6l260.1 797.9h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z"
|
||||
/>
|
||||
<path fill="#4352B8" d="m482.3 809.8 61.4 188.5 170.7-188.5z" />
|
||||
<path
|
||||
fill="#607988"
|
||||
d="M936.1 476.1V437H747.6v-63.2h-61.2V437H566.1v39.1h239.4c-12.8 45.1-41.1 87.7-68.7 120.8-48.9-57.9-49.1-76.7-49.1-76.7h-50.8s2.1 28.2 70.7 108.6c-22.3 22.8-39.2 36.3-39.2 36.3l15.6 48.8s23.6-20.3 53.1-51.6c29.6 32.1 67.8 70.7 117.2 116.7l32.1-32.1c-52.9-48-91.7-86.1-120.2-116.7 38.2-45.2 77-102.1 85.2-154.2H936v.1z"
|
||||
/>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M66.4 0C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h648.1L454.4 0h-388z"
|
||||
/>
|
||||
<linearGradient
|
||||
id="a"
|
||||
x1="534.3"
|
||||
x2="998.1"
|
||||
y1="433.2"
|
||||
y2="433.2"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".2" />
|
||||
<stop offset="1" stop-color="#fff" stop-opacity=".02" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#a)"
|
||||
d="M534.3 200.4h397.4c36.5 0 66.4 29.4 66.4 65.4V666L534.3 200.4z"
|
||||
/>
|
||||
<path
|
||||
fill="#EEE"
|
||||
d="M371.4 430.6c-2.5 30.3-28.4 75.2-91.1 75.2-54.3 0-98.3-44.9-98.3-100.2s44-100.2 98.3-100.2c30.9 0 51.5 13.4 63.3 24.3l41.2-39.6c-27.1-25-62.4-40.6-104.5-40.6-86.1 0-156 69.9-156 156s69.9 156 156 156c90.2 0 149.8-63.3 149.8-152.6 0-12.8-1.6-22.2-3.7-31.8h-146v53.4l91 .1z"
|
||||
/>
|
||||
<radialGradient
|
||||
id="b"
|
||||
cx="65.208"
|
||||
cy="19.366"
|
||||
r="1398.271"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#fff" stop-opacity=".1" />
|
||||
<stop offset="1" stop-color="#fff" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
<path
|
||||
fill="url(#b)"
|
||||
d="M931.7 200.4H518.8L454.4 0h-388C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h415.9l61.4 188.4h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z"
|
||||
/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.1 KiB |
Loading…
Reference in New Issue
Block a user