mirror of
https://github.com/mirego/accent.git
synced 2024-09-11 10:55:28 +03:00
Add Google Translate service with machine translations feature
This commit is contained in:
parent
820d9ed2bf
commit
6e0c3e129a
@ -32,6 +32,10 @@ config :sentry,
|
||||
root_source_code_path: File.cwd!(),
|
||||
release: version
|
||||
|
||||
config :tesla,
|
||||
auth_enabled: true,
|
||||
adapter: Tesla.Adapter.Hackney
|
||||
|
||||
config :ueberauth, Ueberauth, providers: []
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
@ -42,6 +42,21 @@ end
|
||||
|
||||
config :accent, Accent.Repo, url: System.get_env("DATABASE_URL") || "postgres://localhost/accent_development"
|
||||
|
||||
google_translate_provider = {Accent.MachineTranslations.Adapter.GoogleTranslations, [key: System.get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY")]}
|
||||
|
||||
translate_list_provider = if System.get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY"), do: google_translate_provider, else: nil
|
||||
|
||||
translate_text_providers = []
|
||||
|
||||
translate_text_providers =
|
||||
if System.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
|
||||
|
||||
providers = []
|
||||
providers = if System.get_env("GOOGLE_API_CLIENT_ID"), do: [{:google, {Ueberauth.Strategy.Google, [scope: "email openid"]}} | providers], else: providers
|
||||
providers = if System.get_env("SLACK_CLIENT_ID"), do: [{:slack, {Ueberauth.Strategy.Slack, [team: System.get_env("SLACK_TEAM_ID")]}} | providers], else: providers
|
||||
@ -73,6 +88,14 @@ config :sentry,
|
||||
dsn: System.get_env("SENTRY_DSN"),
|
||||
environment_name: System.get_env("SENTRY_ENVIRONMENT_NAME")
|
||||
|
||||
if System.get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY") do
|
||||
config :goth, json: System.get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY")
|
||||
else
|
||||
config :goth, disabled: true
|
||||
end
|
||||
|
||||
config :tesla, logger_enabled: true
|
||||
|
||||
if !System.get_env("SENTRY_DSN") do
|
||||
config :sentry, included_environments: []
|
||||
end
|
||||
|
@ -16,4 +16,7 @@ config :accent, Accent.Hook,
|
||||
outbounds: [{Accent.Hook.Outbounds.Mock, events: events}],
|
||||
inbounds: [{Accent.Hook.Inbounds.Mock, events: events}]
|
||||
|
||||
config :goth, disabled: true
|
||||
config :tesla, logger_enabled: false, adapter: Tesla.Mock
|
||||
|
||||
config :logger, level: :warn
|
||||
|
@ -73,11 +73,27 @@ defmodule Accent.RoleAbilities do
|
||||
delete_project
|
||||
)a ++ @developer_actions
|
||||
|
||||
def actions_for(@owner_role), do: @admin_actions
|
||||
def actions_for(@admin_role), do: @admin_actions
|
||||
def actions_for(@bot_role), do: @bot_actions
|
||||
def actions_for(@developer_role), do: @developer_actions
|
||||
def actions_for(@reviewer_role), do: @any_actions
|
||||
@configurable_actions ~w(machine_translations_translate_file machine_translations_translate_text)a
|
||||
|
||||
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 add_configurable_actions(actions, role) do
|
||||
Enum.reduce(@configurable_actions, actions, fn action, actions ->
|
||||
if can?(role, action), do: [action | actions], else: actions
|
||||
end)
|
||||
end
|
||||
|
||||
def can?(role, :machine_translations_translate_file) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
Accent.MachineTranslations.translate_list_enabled?()
|
||||
end
|
||||
|
||||
def can?(role, :machine_translations_translate_text) when role in [@owner_role, @admin_role, @developer_role] do
|
||||
Accent.MachineTranslations.translate_text_enabled?()
|
||||
end
|
||||
|
||||
# Define abilities function at compile time to remove list lookup at runtime
|
||||
def can?(@owner_role, _action), do: true
|
||||
|
@ -16,6 +16,21 @@ defmodule Accent.Scopes.Operation do
|
||||
from(query, where: [user_id: ^user_id])
|
||||
end
|
||||
|
||||
@doc """
|
||||
## Examples
|
||||
|
||||
iex> Accent.Scopes.Operation.filter_from_project(Accent.Operation, nil)
|
||||
Accent.Operation
|
||||
iex> Accent.Scopes.Operation.filter_from_project(Accent.Operation, "test")
|
||||
#Ecto.Query<from o0 in Accent.Operation, left_join: r1 in assoc(o0, :revision), where: r1.project_id == ^"test" or o0.project_id == ^"test">
|
||||
"""
|
||||
@spec filter_from_project(Ecto.Queryable.t(), String.t() | nil) :: Ecto.Queryable.t()
|
||||
def filter_from_project(query, nil), do: query
|
||||
|
||||
def filter_from_project(query, project_id) do
|
||||
from(o in query, left_join: r in assoc(o, :revision), where: r.project_id == ^project_id or o.project_id == ^project_id)
|
||||
end
|
||||
|
||||
@doc """
|
||||
## Examples
|
||||
|
||||
|
@ -1,21 +1,20 @@
|
||||
defmodule Accent.TranslationsRenderer do
|
||||
alias Langue
|
||||
|
||||
def render(args) do
|
||||
{:ok, serializer} = Langue.serializer_from_format(args[:document].format)
|
||||
entries = entries(args)
|
||||
def render_entries(args) do
|
||||
{:ok, serializer} = Langue.serializer_from_format(args.document.format)
|
||||
|
||||
serialzier_input = %Langue.Formatter.ParserResult{
|
||||
entries: entries,
|
||||
entries: args.entries,
|
||||
language: %Langue.Language{
|
||||
slug: args[:language].slug,
|
||||
plural_forms: args[:language].plural_forms
|
||||
slug: args.language.slug,
|
||||
plural_forms: args.language.plural_forms
|
||||
},
|
||||
document: %Langue.Document{
|
||||
path: args[:document].path,
|
||||
master_language: Accent.Revision.language(args[:master_revision]).slug,
|
||||
top_of_the_file_comment: args[:document].top_of_the_file_comment,
|
||||
header: args[:document].header
|
||||
path: args.document.path,
|
||||
master_language: args.master_language.slug,
|
||||
top_of_the_file_comment: args.document.top_of_the_file_comment,
|
||||
header: args.document.header
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,10 +25,20 @@ defmodule Accent.TranslationsRenderer do
|
||||
end
|
||||
end
|
||||
|
||||
def entries(args) do
|
||||
translations = args[:translations]
|
||||
master_translations = Enum.group_by(args[:master_translations], & &1.key)
|
||||
def render_translations(args) do
|
||||
value_map = Map.get(args, :value_map, & &1.corrected_text)
|
||||
entries = translations_to_entries(args[:translations], args[:master_translations], value_map)
|
||||
|
||||
render_entries(%{
|
||||
entries: entries,
|
||||
document: args[:document],
|
||||
language: args[:language],
|
||||
master_language: args[:master_language]
|
||||
})
|
||||
end
|
||||
|
||||
defp translations_to_entries(translations, master_translations, value_map) do
|
||||
master_translations = Enum.group_by(master_translations || [], & &1.key)
|
||||
|
||||
Enum.map(translations, fn translation ->
|
||||
master_translation = Map.get(master_translations, translation.key)
|
||||
|
34
lib/graphql/resolvers/machine_translation.ex
Normal file
34
lib/graphql/resolvers/machine_translation.ex
Normal file
@ -0,0 +1,34 @@
|
||||
defmodule Accent.GraphQL.Resolvers.MachineTranslation do
|
||||
require Ecto.Query
|
||||
alias Ecto.Query
|
||||
|
||||
alias Accent.Scopes.Revision, as: RevisionScope
|
||||
|
||||
alias Accent.{
|
||||
Language,
|
||||
MachineTranslations,
|
||||
Project,
|
||||
Repo,
|
||||
Revision
|
||||
}
|
||||
|
||||
@spec translate_text(Project.t(), %{text: String.t(), source_language_slug: String.t(), target_language_slug: String.t()}, GraphQLContext.t()) :: nil
|
||||
def translate_text(project, args, _info) 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)}
|
||||
end
|
||||
|
||||
defp slug_language(project_id, slug) do
|
||||
revision =
|
||||
Revision
|
||||
|> RevisionScope.from_project(project_id)
|
||||
|> Query.where(slug: ^slug)
|
||||
|> Repo.one()
|
||||
|
||||
language = Repo.get_by(Language, slug: slug)
|
||||
|
||||
revision || language
|
||||
end
|
||||
end
|
@ -2,6 +2,7 @@ defmodule Accent.GraphQL.Resolvers.Project do
|
||||
require Ecto.Query
|
||||
|
||||
alias Accent.Scopes.Project, as: ProjectScope
|
||||
alias Accent.Scopes.Operation, as: OperationScope
|
||||
|
||||
alias Accent.{
|
||||
GraphQL.Paginated,
|
||||
@ -105,11 +106,11 @@ defmodule Accent.GraphQL.Resolvers.Project do
|
||||
end
|
||||
|
||||
@spec last_activity(Project.t(), any(), GraphQLContext.t()) :: {:ok, Operation.t() | nil}
|
||||
def last_activity(project, _, _) do
|
||||
def last_activity(project, args, _) do
|
||||
Operation
|
||||
|> Query.join(:left, [o], r in assoc(o, :revision))
|
||||
|> Query.where([o, r], r.project_id == ^project.id or o.project_id == ^project.id)
|
||||
|> Query.order_by([o], desc: o.inserted_at)
|
||||
|> OperationScope.filter_from_project(project.id)
|
||||
|> OperationScope.filter_from_action(args[:action])
|
||||
|> OperationScope.order_last_to_first()
|
||||
|> Query.limit(1)
|
||||
|> Repo.one()
|
||||
|> (&{:ok, &1}).()
|
||||
|
@ -11,6 +11,11 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
field(:nodes, list_of(:project))
|
||||
end
|
||||
|
||||
object :project_translated_text do
|
||||
field(:id, non_null(:id))
|
||||
field(:text, :string)
|
||||
end
|
||||
|
||||
object :project do
|
||||
field(:id, :id)
|
||||
field(:name, :string)
|
||||
@ -22,9 +27,20 @@ defmodule Accent.GraphQL.Types.Project do
|
||||
field(:conflicts_count, non_null(:integer))
|
||||
field(:reviewed_count, non_null(:integer))
|
||||
|
||||
field(:last_activity, :activity, resolve: &Accent.GraphQL.Resolvers.Project.last_activity/3)
|
||||
field :last_activity, :activity do
|
||||
arg(:action, :string)
|
||||
resolve(&Accent.GraphQL.Resolvers.Project.last_activity/3)
|
||||
end
|
||||
|
||||
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
|
||||
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))
|
||||
end
|
||||
|
||||
field :access_token, :string do
|
||||
resolve(project_authorize(:show_project_access_token, &Accent.GraphQL.Resolvers.AccessToken.show_project/3))
|
||||
end
|
||||
|
4
lib/machine_translations/adapter/adapter.ex
Normal file
4
lib/machine_translations/adapter/adapter.ex
Normal file
@ -0,0 +1,4 @@
|
||||
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
|
69
lib/machine_translations/adapter/google_translation.ex
Normal file
69
lib/machine_translations/adapter/google_translation.ex
Normal file
@ -0,0 +1,69 @@
|
||||
defmodule Accent.MachineTranslations.Adapter.GoogleTranslations do
|
||||
@behaviour Accent.MachineTranslations.Adapter
|
||||
|
||||
alias Tesla.Middleware
|
||||
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
|
||||
@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
|
||||
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"]})}
|
||||
|
||||
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
|
||||
end
|
52
lib/machine_translations/machine_translations.ex
Normal file
52
lib/machine_translations/machine_translations.ex
Normal file
@ -0,0 +1,52 @@
|
||||
defmodule Accent.MachineTranslations do
|
||||
alias Accent.MachineTranslations.TranslatedText
|
||||
|
||||
def translate_list_enabled? do
|
||||
not is_nil(provider_from_service(:translate_list))
|
||||
end
|
||||
|
||||
def translate_text_enabled? do
|
||||
not Enum.empty?(provider_from_service(:translate_text))
|
||||
end
|
||||
|
||||
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
|
||||
values = Enum.map(entries, & &1.value)
|
||||
|
||||
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.with_index()
|
||||
|> Enum.map(fn {entry, index} ->
|
||||
case Enum.at(translated_values, index) do
|
||||
%TranslatedText{text: text} -> %{entry | value: text}
|
||||
_ -> entry
|
||||
end
|
||||
end)
|
||||
else
|
||||
_ -> entries
|
||||
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
|
3
lib/machine_translations/translated_text.ex
Normal file
3
lib/machine_translations/translated_text.ex
Normal file
@ -0,0 +1,3 @@
|
||||
defmodule Accent.MachineTranslations.TranslatedText do
|
||||
defstruct id: nil, text: nil
|
||||
end
|
@ -157,10 +157,10 @@ defmodule Accent.ExportController do
|
||||
_
|
||||
) do
|
||||
%{render: render} =
|
||||
Accent.TranslationsRenderer.render(%{
|
||||
Accent.TranslationsRenderer.render_translations(%{
|
||||
master_translations: master_translations,
|
||||
master_revision: master_revision,
|
||||
translations: translations,
|
||||
master_language: Accent.Revision.language(master_revision),
|
||||
language: Accent.Revision.language(revision),
|
||||
document: document
|
||||
})
|
||||
|
@ -123,11 +123,10 @@ defmodule Accent.ExportJIPTController do
|
||||
revision = Enum.at(project.revisions, 0)
|
||||
|
||||
%{render: render} =
|
||||
Accent.TranslationsRenderer.render(%{
|
||||
master_translations: [],
|
||||
Accent.TranslationsRenderer.render_translations(%{
|
||||
translations: translations,
|
||||
master_revision: revision,
|
||||
document: document,
|
||||
master_language: Accent.Revision.language(revision),
|
||||
language: Accent.Revision.language(revision),
|
||||
value_map: &"{^#{&1.key}@#{document.path}}"
|
||||
})
|
||||
|
69
lib/web/controllers/machine_translations_controller.ex
Normal file
69
lib/web/controllers/machine_translations_controller.ex
Normal file
@ -0,0 +1,69 @@
|
||||
defmodule Accent.MachineTranslationsController do
|
||||
use Phoenix.Controller
|
||||
|
||||
import Canary.Plugs
|
||||
|
||||
alias Accent.{Language, Project, Repo}
|
||||
alias Accent.Scopes.Revision, as: RevisionScope
|
||||
|
||||
plug(Plug.Assign, canary_action: :machine_translations_translate_file)
|
||||
plug(:load_and_authorize_resource, model: Project, id_name: "project_id")
|
||||
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(Accent.Plugs.MovementContextParser)
|
||||
plug(:fetch_master_revision)
|
||||
|
||||
@doc """
|
||||
Translate a file from a language to another language.
|
||||
|
||||
## Endpoint
|
||||
|
||||
POST /machine-translations/translate-file
|
||||
|
||||
### Required params
|
||||
- `project_id`
|
||||
- `from_language_id`
|
||||
- `to_language_id`
|
||||
- `file`
|
||||
- `document_format`
|
||||
|
||||
### Response
|
||||
|
||||
#### Success
|
||||
`200` - A file containing the rendered document.
|
||||
|
||||
#### Error
|
||||
`404` - Unknown project, language or format
|
||||
"""
|
||||
def translate_file(conn, _params) do
|
||||
entries =
|
||||
Accent.MachineTranslations.translate_entries(
|
||||
conn.assigns[:movement_context].entries,
|
||||
conn.assigns[:source_language],
|
||||
conn.assigns[:target_language]
|
||||
)
|
||||
|
||||
%{render: render} =
|
||||
Accent.TranslationsRenderer.render_entries(%{
|
||||
entries: entries,
|
||||
language: conn.assigns[:target_language],
|
||||
master_language: Accent.Revision.language(conn.assigns[:master_revision]),
|
||||
document: conn.assigns[:movement_context].assigns[:document]
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_resp_header("content-type", "text/plain")
|
||||
|> send_resp(:ok, render)
|
||||
end
|
||||
|
||||
defp fetch_master_revision(conn = %{assigns: %{project: project}}, _) do
|
||||
revision =
|
||||
project
|
||||
|> Ecto.assoc(:revisions)
|
||||
|> RevisionScope.master()
|
||||
|> Repo.one()
|
||||
|> Repo.preload(:language)
|
||||
|
||||
assign(conn, :master_revision, revision)
|
||||
end
|
||||
end
|
@ -7,9 +7,9 @@ defmodule Accent.Plugs.MovementContextParser do
|
||||
alias Movement.Context
|
||||
|
||||
plug(:validate_params)
|
||||
plug(:assign_document_format)
|
||||
plug(:assign_document_parser)
|
||||
plug(:assign_document_path)
|
||||
plug(:assign_document_format)
|
||||
plug(:assign_version)
|
||||
plug(:assign_movement_context)
|
||||
plug(:assign_movement_document)
|
||||
@ -19,7 +19,7 @@ defmodule Accent.Plugs.MovementContextParser do
|
||||
def validate_params(conn = %{params: %{"document_format" => _format, "file" => _file, "language" => _language}}, _), do: conn
|
||||
def validate_params(conn, _), do: conn |> send_resp(:unprocessable_entity, "file, language and document_format are required") |> halt
|
||||
|
||||
def assign_document_parser(conn = %{params: %{"document_format" => document_format}}, _) do
|
||||
def assign_document_parser(conn = %{assigns: %{document_format: document_format}}, _) do
|
||||
case Langue.parser_from_format(document_format) do
|
||||
{:ok, parser} -> assign(conn, :document_parser, parser)
|
||||
{:error, _reason} -> conn |> send_resp(:unprocessable_entity, "document_format is invalid") |> halt
|
||||
@ -27,7 +27,7 @@ defmodule Accent.Plugs.MovementContextParser do
|
||||
end
|
||||
|
||||
def assign_document_format(conn = %{params: %{"document_format" => format}}, _) do
|
||||
assign(conn, :document_format, format)
|
||||
assign(conn, :document_format, String.downcase(format))
|
||||
end
|
||||
|
||||
def assign_document_path(conn = %{params: %{"document_path" => path}}, _) when path !== "" and not is_nil(path) do
|
||||
|
@ -42,6 +42,7 @@ defmodule Accent.Router do
|
||||
post("/add-translations/peek", PeekController, :merge, as: :peek_add_translations)
|
||||
post("/merge", MergeController, [])
|
||||
post("/merge/peek", PeekController, :merge, as: :peek_merge)
|
||||
post("/machine-translations/translate-file", MachineTranslationsController, :translate_file, as: :translate_file)
|
||||
|
||||
# File export
|
||||
get("/export", ExportController, [])
|
||||
|
6
mix.exs
6
mix.exs
@ -106,6 +106,12 @@ defmodule Accent.Mixfile do
|
||||
{:mix_gleam, "~> 0.1", only: [:dev, :test]},
|
||||
{:gleam_stdlib, "~> 0.11", only: [:dev, :test]},
|
||||
|
||||
# Google API authentication
|
||||
{:goth, "~> 1.1"},
|
||||
|
||||
# Network request
|
||||
{:tesla, "~> 1.3"},
|
||||
|
||||
# Dev
|
||||
{:dialyxir, "~> 1.0", only: ~w(dev test)a, runtime: false},
|
||||
{:credo, ">= 0.0.0", only: ~w(dev test)a},
|
||||
|
4
mix.lock
4
mix.lock
@ -29,10 +29,13 @@
|
||||
"gen_smtp": {:hex, :gen_smtp, "0.15.0", "9f51960c17769b26833b50df0b96123605a8024738b62db747fece14eb2fbfcc", [:rebar3], [], "hexpm", "29bd14a88030980849c7ed2447b8db6d6c9278a28b11a44cafe41b791205440f"},
|
||||
"gettext": {:hex, :gettext, "0.18.2", "7df3ea191bb56c0309c00a783334b288d08a879f53a7014341284635850a6e55", [:mix], [], "hexpm", "f9f537b13d4fdd30f3039d33cb80144c3aa1f8d9698e47d7bcbcc8df93b1f5c5"},
|
||||
"gleam_stdlib": {:hex, :gleam_stdlib, "0.11.0", "9b1089739574cdf78a1c25a463d770a98f59e63a92324d17095ad67e867ee549", [:rebar3], [], "hexpm", "5508e169e20369fc8d400481d3f37ccb2cfd880509f63d6e2b6d85f939bc991b"},
|
||||
"goth": {:hex, :goth, "1.2.0", "92d6d926065a72a7e0da8818cc3a133229b56edf378022c00d9886c4125ce769", [:mix], [{:httpoison, "~> 0.11 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:joken, "~> 2.0", [hex: :joken, repo: "hexpm", optional: false]}], "hexpm", "4974932ab3b782c99a6fdeb0b968ddd61436ef14de5862bd6bb0227386c63b26"},
|
||||
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
|
||||
"httpoison": {:hex, :httpoison, "1.7.0", "abba7d086233c2d8574726227b6c2c4f6e53c4deae7fe5f6de531162ce9929a0", [:mix], [{:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "975cc87c845a103d3d1ea1ccfd68a2700c211a434d8428b10c323dc95dc5b980"},
|
||||
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
|
||||
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
|
||||
"joken": {:hex, :joken, "2.3.0", "62a979c46f2c81dcb8ddc9150453b60d3757d1ac393c72bb20fc50a7b0827dc6", [:mix], [{:jose, "~> 1.10", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "57b263a79c0ec5d536ac02d569c01e6b4de91bd1cb825625fe90eab4feb7bc1e"},
|
||||
"jose": {:hex, :jose, "1.10.1", "16d8e460dae7203c6d1efa3f277e25b5af8b659febfc2f2eb4bacf87f128b80a", [:mix, :rebar3], [], "hexpm", "3c7ddc8a9394b92891db7c2771da94bf819834a1a4c92e30857b7d582e2f8257"},
|
||||
"jsone": {:hex, :jsone, "1.5.2", "87adea283c9cf24767b4deed44602989a5331156df5d60a2660e9c9114d54046", [:rebar3], [], "hexpm", "170c171ce7f6dd70c858065154a3305b8564833c6dcca17e10b676ca31ea976f"},
|
||||
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm", "d34f013c156db51ad57cc556891b9720e6a1c1df5fe2e15af999c84d6cebeb1a"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
@ -65,6 +68,7 @@
|
||||
"sentry": {:hex, :sentry, "7.2.5", "570db92c3bbacd6ad02ac81cba8ac5af11235a55d65ac4375e3ec833975b83d3", [:mix], [{:hackney, "~> 1.8 or 1.6.5", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "ea84ed6848505ff2a246567df562f465d2b34c317d3ecba7c7df58daa56e5e5d"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.2", "2808c992455e08d6177322f14d3bdb6b625fbcfd233a73505870d8738a2f4599", [:rebar3], [], "hexpm", "2d1419bd9dda6a206d7b5852179511722e2b18812310d304620c7bd92a13fcef"},
|
||||
"tesla": {:hex, :tesla, "1.3.3", "26ae98627af5c406584aa6755ab5fc96315d70d69a24dd7f8369cfcb75094a45", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2648f1c276102f9250299e0b7b57f3071c67827349d9173f34c281756a1b124c"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.6.3", "d42ace28b870e8072cf30e32e385579c57b9cc96ec74fa1f30f30da9c14f3cc0", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "afc293d8a1140d6591b53e3eaf415ca92842cb1d32fad3c450c6f045f7f91b60"},
|
||||
"ueberauth_discord": {:hex, :ueberauth_discord, "0.5.2", "afc5d68879575c365972fd4d7cf7b01c16f7d062fc6bf7e86e2595736ac41127", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.3", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "bbd7701d8fef02623fb106ed7c24427ed2327ef769bcb1d2eba5670e54716cdc"},
|
||||
"ueberauth_github": {:hex, :ueberauth_github, "0.8.0", "2216c8cdacee0de6245b422fb397921b64a29416526985304e345dab6a799d17", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.6.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "b65ccc001a7b0719ba069452f3333d68891f4613ae787a340cce31e2a43307a3"},
|
||||
|
@ -42,9 +42,9 @@ defmodule AccentTest.TranslationsRenderer do
|
||||
|> Repo.insert!()
|
||||
|
||||
%{render: render} =
|
||||
TranslationsRenderer.render(%{
|
||||
TranslationsRenderer.render_translations(%{
|
||||
master_translations: [],
|
||||
master_revision: revision,
|
||||
master_language: revision.language,
|
||||
translations: [translation],
|
||||
document: document,
|
||||
language: revision.language
|
||||
@ -82,9 +82,9 @@ defmodule AccentTest.TranslationsRenderer do
|
||||
|> Enum.map(&Repo.insert!/1)
|
||||
|
||||
%{render: render} =
|
||||
TranslationsRenderer.render(%{
|
||||
TranslationsRenderer.render_translations(%{
|
||||
master_translations: [],
|
||||
master_revision: revision,
|
||||
master_language: revision.language,
|
||||
translations: translations,
|
||||
document: document,
|
||||
language: revision.language
|
||||
@ -107,9 +107,9 @@ defmodule AccentTest.TranslationsRenderer do
|
||||
|> Repo.insert!()
|
||||
|
||||
%{render: render} =
|
||||
TranslationsRenderer.render(%{
|
||||
TranslationsRenderer.render_translations(%{
|
||||
master_translations: [],
|
||||
master_revision: revision,
|
||||
master_language: revision.language,
|
||||
translations: [translation],
|
||||
document: document,
|
||||
language: %Language{slug: "fr"}
|
||||
@ -138,14 +138,14 @@ defmodule AccentTest.TranslationsRenderer do
|
||||
|> Repo.insert!()
|
||||
|
||||
%{render: render} =
|
||||
TranslationsRenderer.render(%{
|
||||
TranslationsRenderer.render_translations(%{
|
||||
master_translations: [
|
||||
%Translation{
|
||||
key: "a",
|
||||
corrected_text: "master A"
|
||||
}
|
||||
],
|
||||
master_revision: revision,
|
||||
master_language: Accent.Revision.language(revision),
|
||||
translations: [translation],
|
||||
document: document,
|
||||
language: %Language{slug: "fr"}
|
||||
|
1
webapp/app/config/environment.d.ts
vendored
1
webapp/app/config/environment.d.ts
vendored
@ -37,6 +37,7 @@ declare const config: {
|
||||
MERGE_REVISION_PATH: string;
|
||||
EXPORT_DOCUMENT: string;
|
||||
JIPT_EXPORT_DOCUMENT: string;
|
||||
MACHINE_TRANSLATIONS_TRANSLATE_FILE_PROJECT_PATH: string;
|
||||
PERCENTAGE_REVIEWED_BADGE_SVG_PROJECT_PATH: string;
|
||||
JIPT_SCRIPT_PATH: string;
|
||||
};
|
||||
|
@ -17,9 +17,18 @@
|
||||
"title_count": "{count, plural, =1 {This element contains 1 string} other {This element contains # strings}}"
|
||||
}
|
||||
},
|
||||
"machine_translations_translate_upload_form": {
|
||||
"translate": "Translate",
|
||||
"reset": "Reset",
|
||||
"step_1": "Select or drop a file here to translate it.",
|
||||
"step_2": "Choose the correct format and submit to translate <code>{name}</code> in the chosen target language"
|
||||
},
|
||||
"documents_add_button": {
|
||||
"link": "Synchronize a new file"
|
||||
},
|
||||
"documents_machine_translations_button": {
|
||||
"link": "Translate with machine translations"
|
||||
},
|
||||
"versions_add_button": {
|
||||
"link": "Create new version"
|
||||
},
|
||||
@ -155,6 +164,7 @@
|
||||
"overview_link_title": "Overview"
|
||||
},
|
||||
"dashboard_revisions": {
|
||||
"last_synced_at_label": "Last sync:",
|
||||
"manage_languages_link_title": "Manage languages",
|
||||
"new_language_link_title": "New language",
|
||||
"new_language_link_text": "With another language, you can keep track of translation based on the master language.",
|
||||
@ -309,7 +319,7 @@
|
||||
"update": "updated a string",
|
||||
"batch_update": "updated multiple strings",
|
||||
"update_proposed": "updated an uploaded text reference",
|
||||
"sync": "synced a file",
|
||||
"sync": "synced a file: {document}",
|
||||
"batch_sync": "synced files",
|
||||
"new_slave": "added a new language",
|
||||
"uncorrect_all": "put all strings back to review",
|
||||
@ -680,7 +690,6 @@
|
||||
"uncorrect_button": "Put back to review",
|
||||
"uneditable": "The text is not editable because it has been marked as reviewed",
|
||||
"update_text": "Update text",
|
||||
"last_updated_label": "Last updated:",
|
||||
"form": {
|
||||
"true_option": "true",
|
||||
"false_option": "false",
|
||||
@ -724,6 +733,7 @@
|
||||
"plural_label": "plural",
|
||||
"conflicted_label": "in review",
|
||||
"master_label": "master",
|
||||
"last_updated_label": "Last updated:",
|
||||
"removed_label": "This string was removed {removedAt}"
|
||||
},
|
||||
"translations_filter": {
|
||||
@ -738,7 +748,8 @@
|
||||
"empty": "Text is empty (\"\" or null value)",
|
||||
"not_empty": "Text is not empty",
|
||||
"added_last_sync": "Strings added last sync",
|
||||
"commented_on": "Strings with comments"
|
||||
"commented_on": "Strings with comments",
|
||||
"conflicted": "In review"
|
||||
}
|
||||
},
|
||||
"translations_list": {
|
||||
@ -746,6 +757,7 @@
|
||||
"save": "Save",
|
||||
"empty_text": "Empty text",
|
||||
"in_review_label": "in review",
|
||||
"lint_messages_label": "{count, plural, =0 {No linting warnings} =1 {1 linting warning} other {# linting warnings}}",
|
||||
"last_updated_label": "Last updated: ",
|
||||
"maybe_sync_before": "Maybe try to",
|
||||
"maybe_sync_link": "sync some files →",
|
||||
|
@ -1,7 +1,8 @@
|
||||
.badge {
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: background;
|
||||
display: inline-block;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 1px 6px 1px 5px;
|
||||
background: var(--background-light);
|
||||
border-radius: 3px;
|
||||
@ -17,11 +18,25 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.danger {
|
||||
background: var(--color-error);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.icon {
|
||||
background: transparent;
|
||||
position: relative;
|
||||
top: 2px;
|
||||
|
||||
svg {
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.link {
|
||||
padding: 0;
|
||||
|
||||
|
@ -1,3 +1,6 @@
|
||||
<div local-class="badge {{if @link "link"}} {{if @primary "primary"}} {{if @danger "danger"}}">
|
||||
<div
|
||||
local-class="badge {{if @icon "icon"}} {{if @link "link"}} {{if @warning "warning"}} {{if @primary "primary"}} {{if @danger "danger"}}"
|
||||
...attributes
|
||||
>
|
||||
{{yield}}
|
||||
</div>
|
||||
|
@ -12,6 +12,7 @@ const ACTIONS_ICON_PATHS = {
|
||||
add_to_version: 'assets/tag.svg',
|
||||
create_version: 'assets/tag.svg',
|
||||
batch_sync: 'assets/sync.svg',
|
||||
batch_update: 'assets/pencil.svg',
|
||||
sync: 'assets/sync.svg',
|
||||
merge: 'assets/merge.svg',
|
||||
batch_merge: 'assets/merge.svg',
|
||||
|
@ -13,7 +13,8 @@ export default class AsyncButton extends Component<Args> {
|
||||
}
|
||||
|
||||
@action
|
||||
onClick() {
|
||||
onClick(event: Event) {
|
||||
event.preventDefault();
|
||||
if (this.args.disabled) return;
|
||||
|
||||
if (typeof this.args.onClick === 'function') this.args.onClick();
|
||||
|
@ -10,6 +10,7 @@
|
||||
type="text"
|
||||
placeholder={{t "components.conflicts_filters.input_placeholder_text"}}
|
||||
value={{this.debouncedQuery}}
|
||||
{{on-key "Enter" (fn this.submitForm)}}
|
||||
{{on "keyup" this.setDebouncedQuery}}
|
||||
>
|
||||
</div>
|
||||
|
@ -5,7 +5,12 @@ interface Args {
|
||||
project: any;
|
||||
conflicts: any;
|
||||
query: any;
|
||||
onCorrect: () => Promise<void>;
|
||||
onCorrect: (conflict: any, textInput: string) => Promise<void>;
|
||||
onCopyTranslation: (
|
||||
text: string,
|
||||
sourceLanguageSlug: string,
|
||||
targetLanguageSlug: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
export default class ConflictsItems extends Component<Args> {}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import {action} from '@ember/object';
|
||||
import {empty} from '@ember/object/computed';
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
import parsedKeyProperty from 'accent-webapp/computed-macros/parsed-key';
|
||||
import {dropTask} from 'ember-concurrency-decorators';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
interface Args {
|
||||
@ -10,6 +10,11 @@ interface Args {
|
||||
project: any;
|
||||
conflict: any;
|
||||
onCorrect: (conflict: any, textInput: string) => Promise<void>;
|
||||
onCopyTranslation: (
|
||||
text: string,
|
||||
sourceLanguageSlug: string,
|
||||
targetLanguageSlug: string
|
||||
) => Promise<{text: string | null}>;
|
||||
}
|
||||
|
||||
export default class ConflictItem extends Component<Args> {
|
||||
@ -29,6 +34,17 @@ export default class ConflictItem extends Component<Args> {
|
||||
resolved = false;
|
||||
|
||||
conflictKey = parsedKeyProperty(this.args.conflict.key);
|
||||
textOriginal = this.args.conflict.correctedText;
|
||||
|
||||
get showTextDiff() {
|
||||
if (!this.args.conflict.conflictedText) return false;
|
||||
|
||||
return this.textInput !== this.args.conflict.conflictedText;
|
||||
}
|
||||
|
||||
get showOriginalButton() {
|
||||
return this.textInput !== this.textOriginal;
|
||||
}
|
||||
|
||||
get revisionName() {
|
||||
return (
|
||||
@ -42,6 +58,24 @@ export default class ConflictItem extends Component<Args> {
|
||||
this.textInput = text;
|
||||
}
|
||||
|
||||
@action
|
||||
setOriginalText() {
|
||||
this.textInput = this.textOriginal;
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*copyTranslationTask(text: string, sourceLanguageSlug: string) {
|
||||
const copyTranslation = yield this.args.onCopyTranslation(
|
||||
text,
|
||||
sourceLanguageSlug,
|
||||
this.revisionSlug
|
||||
);
|
||||
|
||||
if (copyTranslation.text) {
|
||||
this.textInput = copyTranslation.text;
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async correct() {
|
||||
this.onLoading();
|
||||
@ -68,4 +102,11 @@ export default class ConflictItem extends Component<Args> {
|
||||
this.resolved = true;
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
private get revisionSlug() {
|
||||
return (
|
||||
this.args.conflict.revision.slug ||
|
||||
this.args.conflict.revision.language.slug
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {action} from '@ember/object';
|
||||
|
||||
interface Args {
|
||||
project: any;
|
||||
translation: any;
|
||||
onCopyTranslation: (text: string, languageSlug: string) => void;
|
||||
}
|
||||
|
||||
const MAX_TEXT_LENGTH = 600;
|
||||
@ -26,4 +28,19 @@ export default class ConflictItemRelatedTranslation extends Component<Args> {
|
||||
this.args.translation.revision.language.name
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
translate() {
|
||||
this.args.onCopyTranslation(
|
||||
this.args.translation.correctedText,
|
||||
this.revisionSlug
|
||||
);
|
||||
}
|
||||
|
||||
private get revisionSlug() {
|
||||
return (
|
||||
this.args.translation.revision.slug ||
|
||||
this.args.translation.revision.language.slug
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -95,15 +95,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions-flag--link {
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--body-background);
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.item-actions-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
|
@ -24,7 +24,16 @@
|
||||
{{inline-svg "assets/eye.svg" local-class="item-actions-icon"}}
|
||||
</LinkTo>
|
||||
|
||||
{{#if relatedTranslation.isConflicted}}
|
||||
{{#if (get @permissions "machine_translations_translate_text")}}
|
||||
<button
|
||||
local-class="item-actions-link"
|
||||
{{on "click" this.translate}}
|
||||
>
|
||||
{{inline-svg "assets/language.svg" local-class="item-actions-icon"}}
|
||||
</button>
|
||||
{{/if}}
|
||||
|
||||
{{#if @translation.isConflicted}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.revision.conflicts"
|
||||
@models={{array @project.id @translation.revision.id}}
|
||||
|
@ -186,12 +186,12 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.itemInput-actions {
|
||||
.button-submit {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 5px;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
@ -247,3 +247,27 @@
|
||||
.conflictedText-references {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.conflictedText-references-conflicted {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--color-black);
|
||||
}
|
||||
|
||||
.conflictedText-references-conflicted-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
flex-shrink: 0;
|
||||
width: 90px;
|
||||
margin-right: 10px;
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.conflictedText-references-conflicted-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
@ -65,31 +65,54 @@
|
||||
@showTypeHints={{false}}
|
||||
@onKeyUp={{fn this.changeTranslationText}}
|
||||
@onSubmit={{fn this.correct}}
|
||||
/>
|
||||
as |form|
|
||||
>
|
||||
{{#component form.submit}}
|
||||
<div local-class="button-submit">
|
||||
{{#if this.showOriginalButton}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.setOriginalText}}
|
||||
class="button button--iconOnly button--white"
|
||||
>
|
||||
{{inline-svg "/assets/revert.svg" class="button-icon"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
{{#if (get @permissions "correct_translation")}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.correct}}
|
||||
@loading={{this.loading}}
|
||||
class="button button--iconOnly button--filled button--green"
|
||||
>
|
||||
{{inline-svg "/assets/check.svg" class="button-icon"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/component}}
|
||||
</TranslationEdit::Form>
|
||||
</div>
|
||||
|
||||
<div local-class="itemInput-actions">
|
||||
{{#if (get @permissions "correct_translation")}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.correct}}
|
||||
@loading={{this.loading}}
|
||||
class="button button--iconOnly button--filled button--green"
|
||||
>
|
||||
{{inline-svg "/assets/check.svg" class="button-icon"}}
|
||||
</AsyncButton>
|
||||
<div local-class="conflictedText-references">
|
||||
{{#if this.showTextDiff}}
|
||||
<div local-class="conflictedText-references-conflicted">
|
||||
<span local-class="conflictedText-references-conflicted-label">
|
||||
{{inline-svg "/assets/diff.svg" local-class="conflictedText-references-conflicted-icon"}}
|
||||
</span>
|
||||
|
||||
{{string-diff this.textInput @conflict.conflictedText}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{#if @conflict.relatedTranslations}}
|
||||
<div local-class="conflictedText-references">
|
||||
{{#if @conflict.relatedTranslations}}
|
||||
{{#each @conflict.relatedTranslations key="id" as |relatedTranslation|}}
|
||||
<ConflictsList::Item::RelatedTranslation
|
||||
@project={{@project}}
|
||||
@translation={{relatedTranslation}}
|
||||
@permissions={{@permissions}}
|
||||
@onCopyTranslation={{perform this.copyTranslationTask}}
|
||||
/>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
@ -2,3 +2,21 @@
|
||||
position: relative;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.all-reviewed {
|
||||
max-width: 400px;
|
||||
margin: 100px auto 20px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.all-reviewed-title {
|
||||
margin-top: 10px;
|
||||
font-weight: bold;
|
||||
font-size: 22px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.all-reviewed-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--color-grey);
|
||||
}
|
||||
|
@ -5,17 +5,24 @@
|
||||
@project={{@project}}
|
||||
@conflict={{conflict}}
|
||||
@onCorrect={{@onCorrect}}
|
||||
@onCopyTranslation={{@onCopyTranslation}}
|
||||
/>
|
||||
{{else if query}}
|
||||
{{else if @query}}
|
||||
<EmptyContent
|
||||
@iconPath="assets/empty.svg"
|
||||
@text={{t "components.conflicts_items.no_translations" query=@query}}
|
||||
/>
|
||||
{{else}}
|
||||
<EmptyContent
|
||||
@success={{true}}
|
||||
@iconPath="assets/thumbs-up.svg"
|
||||
@text={{t "components.conflicts_items.review_completed"}}
|
||||
/>
|
||||
<div local-class="all-reviewed">
|
||||
<img local-class="all-reviewed-image" src="/assets/all-reviewed-splash.svg" />
|
||||
|
||||
<div local-class="all-reviewed-title">
|
||||
All reviewed
|
||||
</div>
|
||||
|
||||
<div local-class="all-reviewed-subtitle">
|
||||
Every strings have been marked as reviewed
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
</ul>
|
||||
|
@ -15,6 +15,7 @@ interface Args {
|
||||
referenceRevision: any;
|
||||
referenceRevisions: any;
|
||||
onCorrect: () => void;
|
||||
onCopyTranslation: () => void;
|
||||
onCorrectAll: () => void;
|
||||
onSelectPage: () => void;
|
||||
onChangeDocument: () => void;
|
||||
|
@ -24,6 +24,7 @@
|
||||
@conflicts={{@translations.entries}}
|
||||
@query={{@query}}
|
||||
@onCorrect={{@onCorrect}}
|
||||
@onCopyTranslation={{@onCopyTranslation}}
|
||||
/>
|
||||
<ResourcePagination
|
||||
@meta={{@translations.meta}}
|
||||
|
@ -1,6 +1,6 @@
|
||||
.dashboard-revisions-item {
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: border-color;
|
||||
transition-property: box-shadow;
|
||||
position: relative;
|
||||
flex: 1 1 calc(50% - 10px);
|
||||
justify-content: space-between;
|
||||
@ -9,7 +9,7 @@
|
||||
margin-right: 10px;
|
||||
padding: 8px 15px 12px;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 4px var(--shadow-color);
|
||||
box-shadow: 0 9px 19px var(--shadow-color);
|
||||
|
||||
&:nth-child(even) {
|
||||
flex: 1 1 50%;
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
border-color: #ddd;
|
||||
box-shadow: 0 3px 10px var(--shadow-color);
|
||||
|
||||
.actionsButton {
|
||||
opacity: 1;
|
||||
|
@ -169,11 +169,17 @@
|
||||
.activities-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.activities-title-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.activities-title-icon {
|
||||
stroke: #bbb;
|
||||
width: 15px;
|
||||
@ -187,6 +193,12 @@
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.activities-last-sync {
|
||||
font-size: 11px;
|
||||
text-decoration: none;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@media (max-width: var(--screen-md)) {
|
||||
.dashboard-revisions > .content {
|
||||
width: 100%;
|
||||
|
@ -140,8 +140,21 @@
|
||||
{{#if @activities}}
|
||||
<div local-class="activities">
|
||||
<h2 local-class="activities-title">
|
||||
{{inline-svg "assets/activity.svg" local-class="activities-title-icon"}}
|
||||
{{t "components.dashboard_revisions.activities_title"}}
|
||||
<span local-class="activities-title-text">
|
||||
{{inline-svg "assets/activity.svg" local-class="activities-title-icon"}}
|
||||
{{t "components.dashboard_revisions.activities_title"}}
|
||||
</span>
|
||||
|
||||
{{#if @project.lastActivitySync.insertedAt}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.activity"
|
||||
@models={{array @project.id @project.lastActivitySync.id}}
|
||||
local-class="activities-last-sync"
|
||||
>
|
||||
{{t "components.dashboard_revisions.last_synced_at_label"}}
|
||||
<TimeAgoInWordsTag @date={{@project.lastActivitySync.insertedAt}} />
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<ProjectActivitiesList
|
||||
@permissions={{@permissions}}
|
||||
|
@ -1,13 +1,17 @@
|
||||
.documents-list-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
margin-bottom: 30px;
|
||||
padding-bottom: 30px;
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
.deleteDocumentButton {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.links {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,6 +119,10 @@
|
||||
}
|
||||
|
||||
.item-form {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
|
||||
&.item-form--editing {
|
||||
@ -123,6 +131,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.item-form-content {
|
||||
flex-grow: 1;
|
||||
min-width: 300px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
|
||||
.item-form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
@ -199,6 +213,9 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
transform: translate3d(30px, 0, 0);
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: transform;
|
||||
|
||||
:global(.button) {
|
||||
margin-right: 6px;
|
||||
@ -209,11 +226,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.links--editing {
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
||||
.deleteDocumentButton {
|
||||
opacity: 0;
|
||||
padding: 2px 6px !important;
|
||||
}
|
||||
|
||||
:global(.button).button-sync {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
@media (max-width: (800px)) {
|
||||
.item {
|
||||
margin-right: 0;
|
||||
|
@ -1,135 +1,136 @@
|
||||
<li local-class="documents-list-item {{if this.lowPercentage "low-percentage"}} {{if this.mediumPercentage "medium-percentage"}} {{if this.highPercentage "high-percentage"}}">
|
||||
<form local-class="item-form {{if this.isDeleting "item-form--deleting"}}" {{on "submit" (fn this.updateDocument)}}>
|
||||
<div local-class="item-form-inputs">
|
||||
<h2 local-class="item-title">
|
||||
{{#if this.isEditing}}
|
||||
<label local-class="item-form-label">
|
||||
{{t "components.documents_list.item.path_label"}}
|
||||
<small local-class="item-form-help">
|
||||
{{{t "components.documents_list.item.path_help"}}}
|
||||
</small>
|
||||
<div local-class="item-form-content">
|
||||
<div local-class="item-form-inputs">
|
||||
<h2 local-class="item-title">
|
||||
{{#if this.isEditing}}
|
||||
<label local-class="item-form-label">
|
||||
{{t "components.documents_list.item.path_label"}}
|
||||
<small local-class="item-form-help">
|
||||
{{{t "components.documents_list.item.path_help"}}}
|
||||
</small>
|
||||
|
||||
<input
|
||||
{{on-key "cmd+Enter" (fn this.updateDocument)}}
|
||||
{{on "input" (fn this.changePath)}}
|
||||
local-class="textInput"
|
||||
value={{this.renamedDocumentPath}}
|
||||
>
|
||||
</label>
|
||||
{{else}}
|
||||
<span local-class="item-toggle-edit">
|
||||
{{#if this.isDeleting}}
|
||||
<span local-class="item-document-deleting-title">
|
||||
{{inline-svg "/assets/loading.svg" local-class="item-document-deleting-title-loading"}}
|
||||
|
||||
{{t "components.documents_list.item.deleting_label" path=@document.path extension=this.documentFormatItem.extension}}
|
||||
</span>
|
||||
{{else}}
|
||||
<button local-class="item-edit-button" {{on "click" (fn this.toggleEdit)}}>
|
||||
{{inline-svg "assets/pencil.svg" local-class="item-edit-icon"}}
|
||||
</button>
|
||||
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.export"
|
||||
@models={{array @project.id @document.id}}
|
||||
local-class="item-document"
|
||||
<input
|
||||
{{on-key "cmd+Enter" (fn this.updateDocument)}}
|
||||
{{on "input" (fn this.changePath)}}
|
||||
local-class="textInput"
|
||||
value={{this.renamedDocumentPath}}
|
||||
>
|
||||
{{@document.path}}
|
||||
<span local-class="item-subtitle">.{{this.documentFormatItem.extension}}</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</label>
|
||||
{{else}}
|
||||
<span local-class="item-toggle-edit">
|
||||
{{#if this.isDeleting}}
|
||||
<span local-class="item-document-deleting-title">
|
||||
{{inline-svg "/assets/loading.svg" local-class="item-document-deleting-title-loading"}}
|
||||
|
||||
{{t "components.documents_list.item.deleting_label" path=@document.path extension=this.documentFormatItem.extension}}
|
||||
</span>
|
||||
{{else}}
|
||||
<button local-class="item-edit-button" {{on "click" (fn this.toggleEdit)}}>
|
||||
{{inline-svg "assets/pencil.svg" local-class="item-edit-icon"}}
|
||||
</button>
|
||||
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.export"
|
||||
@models={{array @project.id @document.id}}
|
||||
local-class="item-document"
|
||||
>
|
||||
{{@document.path}}
|
||||
<span local-class="item-subtitle">.{{this.documentFormatItem.extension}}</span>
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
{{#unless this.isEditing}}
|
||||
<div local-class="stat">
|
||||
<span local-class="reviewedPercentage">
|
||||
{{this.correctedKeysPercentage}}
|
||||
%
|
||||
</span>
|
||||
{{/if}}
|
||||
</h2>
|
||||
<span local-class="reviewedStats">
|
||||
<span local-class="reviewedStats-reviewedCount">
|
||||
{{this.reviewsCount}}
|
||||
</span>
|
||||
/
|
||||
<span local-class="reviewedStats-translationsCount">
|
||||
{{@document.translationsCount}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div local-class="progress">
|
||||
<ReviewProgressBar @correctedKeysPercentage={{this.correctedKeysPercentage}} />
|
||||
</div>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
{{#unless this.isEditing}}
|
||||
<div local-class="stat">
|
||||
<span local-class="reviewedPercentage">
|
||||
{{this.correctedKeysPercentage}}
|
||||
%
|
||||
</span>
|
||||
<span local-class="reviewedStats">
|
||||
<span local-class="reviewedStats-reviewedCount">
|
||||
{{this.reviewsCount}}
|
||||
</span>
|
||||
/
|
||||
<span local-class="reviewedStats-translationsCount">
|
||||
{{@document.translationsCount}}
|
||||
</span>
|
||||
</span>
|
||||
{{#if this.isEditing}}
|
||||
<div local-class="links links--editing">
|
||||
<AsyncButton
|
||||
class="button button--filled"
|
||||
@loading={{this.isUpdating}}
|
||||
@onClick={{fn this.updateDocument}}
|
||||
>
|
||||
{{t "components.documents_list.save_button"}}
|
||||
</AsyncButton>
|
||||
<button
|
||||
class="button button--filled button--white"
|
||||
{{on "click" (fn this.toggleEdit)}}
|
||||
>
|
||||
{{t "components.documents_list.cancel_button"}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div local-class="progress">
|
||||
<ReviewProgressBar @correctedKeysPercentage={{this.correctedKeysPercentage}} />
|
||||
</div>
|
||||
{{/unless}}
|
||||
|
||||
<div local-class="links">
|
||||
{{#if this.isEditing}}
|
||||
<div>
|
||||
<button
|
||||
class="button button--filled button--white"
|
||||
{{on "click" (fn this.toggleEdit)}}
|
||||
>
|
||||
{{t "components.documents_list.cancel_button"}}
|
||||
</button>
|
||||
<AsyncButton
|
||||
class="button button--filled"
|
||||
@loading={{this.isUpdating}}
|
||||
@onClick={{fn this.updateDocument}}
|
||||
>
|
||||
{{t "components.documents_list.save_button"}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{{else}}
|
||||
<div>
|
||||
{{#if (get @permissions "sync")}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.sync"
|
||||
@models={{array @project.id @document.id}}
|
||||
class="button button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/sync.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.sync"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{#if (get @permissions "merge")}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.add-translations"
|
||||
@models={{array @project.id @document.id}}
|
||||
class="button button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/merge.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.merge"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div local-class="links">
|
||||
{{#if (get @permissions "sync")}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.export"
|
||||
@route="logged-in.project.files.sync"
|
||||
@models={{array @project.id @document.id}}
|
||||
local-class="button-sync"
|
||||
class="button button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/sync.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.sync"}}
|
||||
</LinkTo>
|
||||
{{/if}}
|
||||
{{#if (get @permissions "merge")}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.add-translations"
|
||||
@models={{array @project.id @document.id}}
|
||||
class="button button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/export.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.export"}}
|
||||
{{inline-svg "/assets/merge.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.merge"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
{{/if}}
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.export"
|
||||
@models={{array @project.id @document.id}}
|
||||
class="button button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/export.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.export"}}
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{{#if (get @permissions "delete_document")}}
|
||||
{{#if this.canDeleteFile}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.deleteFile @document}}
|
||||
@loading={{this.isDeleting}}
|
||||
class="button button--small button--red button--borderless"
|
||||
local-class="deleteDocumentButton"
|
||||
>
|
||||
{{inline-svg "/assets/x.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.delete_document"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
<div>
|
||||
{{#if (get @permissions "delete_document")}}
|
||||
{{#if this.canDeleteFile}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.deleteFile @document}}
|
||||
@loading={{this.isDeleting}}
|
||||
class="button button--small button--red button--borderless"
|
||||
local-class="deleteDocumentButton"
|
||||
>
|
||||
{{inline-svg "/assets/x.svg" class="button-icon"}}
|
||||
{{t "components.documents_list.delete_document"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</form>
|
||||
</li>
|
||||
|
@ -0,0 +1,9 @@
|
||||
import Component from '@glimmer/component';
|
||||
|
||||
interface Args {
|
||||
project: any;
|
||||
}
|
||||
|
||||
export default class DocumentsMachineTranslationsButton extends Component<
|
||||
Args
|
||||
> {}
|
@ -0,0 +1,3 @@
|
||||
:global(.button.button--filled).button {
|
||||
margin-left: 15px;
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
<LinkTo
|
||||
@route="logged-in.project.files.machine-translations"
|
||||
@model={{@project.id}}
|
||||
local-class="button"
|
||||
class="button button--borderLess button--filled button--white"
|
||||
>
|
||||
{{inline-svg "/assets/language.svg" class="button-icon"}}
|
||||
{{t "components.documents_machine_translations_button.link"}}
|
||||
</LinkTo>
|
||||
|
@ -0,0 +1,142 @@
|
||||
import Component from '@glimmer/component';
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import {htmlSafe} from '@ember/string';
|
||||
import {dropTask} from 'ember-concurrency-decorators';
|
||||
import GlobalState from 'accent-webapp/services/global-state';
|
||||
import LanguageSearcher from 'accent-webapp/services/language-searcher';
|
||||
|
||||
interface Args {
|
||||
languages: any;
|
||||
content: string | null;
|
||||
onFileReset: () => void;
|
||||
onFileChange: (
|
||||
file: File,
|
||||
fromLanguage: string,
|
||||
toLanguage: string,
|
||||
documentFormat: string
|
||||
) => void;
|
||||
}
|
||||
|
||||
const preventDefault = (event: Event) => event.preventDefault();
|
||||
|
||||
export default class MachineTranslationsTranslateUploadForm extends Component<
|
||||
Args
|
||||
> {
|
||||
@service('global-state')
|
||||
globalState: GlobalState;
|
||||
|
||||
@service('language-searcher')
|
||||
languageSearcher: LanguageSearcher;
|
||||
|
||||
@tracked
|
||||
documentFormat = this.mappedDocumentFormats[0];
|
||||
|
||||
@tracked
|
||||
file: File | null;
|
||||
|
||||
@tracked
|
||||
fromLanguage = this.mappedLanguages[0];
|
||||
|
||||
@tracked
|
||||
toLanguage = this.mappedLanguages[1];
|
||||
|
||||
get mappedLanguages(): Array<{label: string; value: string}> {
|
||||
return this.args.languages.map(
|
||||
({id, name}: {id: string; name: string}) => ({
|
||||
label: name,
|
||||
value: id,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
get mappedDocumentFormats(): Array<{value: string; label: string}> {
|
||||
if (!this.globalState.documentFormats) return [];
|
||||
|
||||
return this.globalState.documentFormats.map(({slug, name}) => ({
|
||||
value: slug,
|
||||
label: name,
|
||||
}));
|
||||
}
|
||||
|
||||
@action
|
||||
onSelectDocumentFormat(documentFormat: {label: string; value: string}) {
|
||||
this.documentFormat = documentFormat;
|
||||
}
|
||||
|
||||
@action
|
||||
switchLanguages() {
|
||||
const fromLanguage = this.fromLanguage;
|
||||
|
||||
this.fromLanguage = this.toLanguage;
|
||||
this.toLanguage = fromLanguage;
|
||||
}
|
||||
|
||||
@action
|
||||
onSelectFromLanguage(langage: {label: string; value: string}) {
|
||||
this.fromLanguage = langage;
|
||||
}
|
||||
|
||||
@action
|
||||
onSelectToLanguage(langage: {label: string; value: string}) {
|
||||
this.toLanguage = langage;
|
||||
}
|
||||
|
||||
@action
|
||||
fileChange(files: File[]) {
|
||||
this.file = files[0];
|
||||
}
|
||||
|
||||
@action
|
||||
deactivateDocumentDrop() {
|
||||
document.addEventListener('dragover', preventDefault);
|
||||
}
|
||||
|
||||
@action
|
||||
activateDocumentDrop() {
|
||||
document.removeEventListener('dragover', preventDefault);
|
||||
}
|
||||
|
||||
@action
|
||||
async searchLanguages(term: string) {
|
||||
const languages = await this.languageSearcher.search({term});
|
||||
|
||||
return this.mapLanguages(languages);
|
||||
}
|
||||
|
||||
@action
|
||||
dropFile(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer?.files[0];
|
||||
this.file = file || null;
|
||||
}
|
||||
|
||||
@action
|
||||
resetFile() {
|
||||
this.file = null;
|
||||
this.args.onFileReset();
|
||||
}
|
||||
|
||||
@dropTask
|
||||
*submitTask() {
|
||||
if (!this.file) return;
|
||||
|
||||
yield this.args.onFileChange(
|
||||
this.file,
|
||||
this.fromLanguage.value,
|
||||
this.toLanguage.value,
|
||||
this.documentFormat.value
|
||||
);
|
||||
}
|
||||
|
||||
private mapLanguages(languages: any) {
|
||||
return languages.map(
|
||||
({id, name, slug}: {id: string; name: string; slug: string}) => {
|
||||
const label = htmlSafe(`${name} <em>${slug}</em>`);
|
||||
|
||||
return {label, value: id};
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,109 @@
|
||||
.content {
|
||||
background: var(--body-background);
|
||||
}
|
||||
|
||||
.filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 15px;
|
||||
background: var(--background-light);
|
||||
border-bottom: 1px solid var(--background-light-highlight);
|
||||
|
||||
:global(.ember-power-select-trigger) {
|
||||
min-height: 26px;
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
background: var(--body-background);
|
||||
padding: 4px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.filters-languages {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.filters-file {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.form {
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.form-content {
|
||||
margin: 0 auto;
|
||||
padding: 100px 10px;
|
||||
max-width: 400px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.form-content-icons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto 20px;
|
||||
}
|
||||
|
||||
.form-content-icon {
|
||||
width: 40px;
|
||||
opacity: 0.6;
|
||||
stroke: var(--color-grey);
|
||||
}
|
||||
|
||||
.form-content-icon--add {
|
||||
width: 20px;
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
.form-content-icon--highlight {
|
||||
stroke: var(--color-primary);
|
||||
}
|
||||
|
||||
.fileInput {
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background: red;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.button).button-switch {
|
||||
margin: 0 10px;
|
||||
opacity: 0.6;
|
||||
color: var(--color-black);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 0.7;
|
||||
color: var(--color-black);
|
||||
}
|
||||
}
|
||||
|
||||
:global(.button).button-resubmit {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
:global(.button).button-submit {
|
||||
display: block;
|
||||
margin: 30px auto 0;
|
||||
|
||||
:global(.label) {
|
||||
padding: 15px 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.button).button-reset {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
@ -0,0 +1,104 @@
|
||||
<div local-class="content"
|
||||
{{did-insert (fn this.deactivateDocumentDrop)}}
|
||||
{{will-destroy (fn this.activateDocumentDrop)}}
|
||||
{{on "drop" (fn this.dropFile)}}
|
||||
>
|
||||
<div local-class="filters">
|
||||
<div local-class="filters-languages">
|
||||
<AccSelect
|
||||
@searchEnabled={{true}}
|
||||
@search={{fn this.searchLanguages}}
|
||||
@selected={{this.fromLanguage}}
|
||||
@options={{this.mappedLanguages}}
|
||||
@onchange={{fn this.onSelectFromLanguage}}
|
||||
/>
|
||||
|
||||
<button
|
||||
{{on "click" (fn this.switchLanguages)}}
|
||||
local-class="button-switch"
|
||||
class="button button--iconOnly"
|
||||
>
|
||||
{{inline-svg "/assets/switch.svg" class="button-icon"}}
|
||||
</button>
|
||||
|
||||
<AccSelect
|
||||
@searchEnabled={{true}}
|
||||
@search={{fn this.searchLanguages}}
|
||||
@selected={{this.toLanguage}}
|
||||
@options={{this.mappedLanguages}}
|
||||
@onchange={{fn this.onSelectToLanguage}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div local-class="filters-file">
|
||||
<AccSelect
|
||||
@searchEnabled={{false}}
|
||||
@selected={{this.documentFormat}}
|
||||
@options={{this.mappedDocumentFormats}}
|
||||
@onchange={{fn this.onSelectDocumentFormat}}
|
||||
/>
|
||||
|
||||
{{#if @content}}
|
||||
<AsyncButton
|
||||
local-class="button-resubmit"
|
||||
class="button button--filled"
|
||||
@loading={{this.submitTask.isRunning}}
|
||||
@onClick={{perform this.submitTask}}
|
||||
>
|
||||
{{inline-svg "/assets/language.svg" class="button-icon"}}
|
||||
{{t "components.machine_translations_translate_upload_form.translate"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div local-class="form">
|
||||
{{#if @content}}
|
||||
<button
|
||||
{{on "click" (fn this.resetFile)}}
|
||||
local-class="button-reset"
|
||||
class="button button--white button--filled"
|
||||
>
|
||||
{{t "components.machine_translations_translate_upload_form.reset"}}
|
||||
</button>
|
||||
{{else}}
|
||||
{{#if this.file}}
|
||||
<div local-class="form-content">
|
||||
<div local-class="form-content-icons">
|
||||
{{inline-svg "/assets/import.svg" local-class="form-content-icon"}}
|
||||
{{inline-svg "/assets/add.svg" local-class="form-content-icon form-content-icon--add"}}
|
||||
{{inline-svg "/assets/language.svg" local-class="form-content-icon form-content-icon--highlight"}}
|
||||
</div>
|
||||
|
||||
{{{t "components.machine_translations_translate_upload_form.step_2" name=this.file.name}}}
|
||||
|
||||
<AsyncButton
|
||||
local-class="button-submit"
|
||||
class="button button--filled"
|
||||
@loading={{this.submitTask.isRunning}}
|
||||
@onClick={{perform this.submitTask}}
|
||||
>
|
||||
{{inline-svg "/assets/language.svg" class="button-icon"}}
|
||||
{{t "components.machine_translations_translate_upload_form.translate"}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{{else}}
|
||||
<FileInput
|
||||
name="file-input"
|
||||
id="file-input"
|
||||
@onChange={{fn this.fileChange}}
|
||||
local-class="fileInput"
|
||||
/>
|
||||
|
||||
<div local-class="form-content">
|
||||
<div local-class="form-content-icons">
|
||||
{{inline-svg "/assets/import.svg" local-class="form-content-icon form-content-icon--highlight"}}
|
||||
{{inline-svg "/assets/add.svg" local-class="form-content-icon form-content-icon--add"}}
|
||||
{{inline-svg "/assets/language.svg" local-class="form-content-icon"}}
|
||||
</div>
|
||||
{{t "components.machine_translations_translate_upload_form.step_1"}}
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
</div>
|
||||
</div>
|
@ -0,0 +1,11 @@
|
||||
.render {
|
||||
padding: 15px;
|
||||
min-height: 50px;
|
||||
border-top: 0;
|
||||
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.05);
|
||||
background: var(--body-background);
|
||||
overflow-x: scroll;
|
||||
font-family: var(--font-monospace);
|
||||
font-size: 11px;
|
||||
line-height: 1.7;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
{{#if @content}}
|
||||
<pre local-class="render">{{@content}}</pre>
|
||||
{{/if}}
|
@ -1,3 +1,9 @@
|
||||
:global(.filters).filters {
|
||||
border: 1px solid #eee;
|
||||
border-radius: 3px;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.filterList {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="filters">
|
||||
<div class="filters" local-class="filters">
|
||||
<div class="filters-wrapper">
|
||||
<ul local-class="filterList">
|
||||
{{#unless @actionFilter}}
|
||||
|
@ -95,7 +95,8 @@ export default class ProjectActivity extends Component<Args> {
|
||||
if (!this.args.activity.action) return;
|
||||
|
||||
return this.intl.t(
|
||||
`components.project_activity.action_text.${this.args.activity.action}`
|
||||
`components.project_activity.action_text.${this.args.activity.action}`,
|
||||
{document: this.args.activity.document?.path}
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -201,7 +201,6 @@
|
||||
{{t "components.project_activity.batch_operation_label"}}
|
||||
</span>
|
||||
<ActivityItem
|
||||
@compact={{true}}
|
||||
@permissions={{@permissions}}
|
||||
@showTranslationLink={{true}}
|
||||
@componentTranslationPrefix="project_activities_list_item"
|
||||
|
@ -3,7 +3,6 @@
|
||||
}
|
||||
|
||||
.translationCommentsList {
|
||||
background: var(--background-light);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
|
@ -11,30 +11,24 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 30px;
|
||||
padding: 20px;
|
||||
margin: 0 30px 30px 0;
|
||||
min-width: 225px;
|
||||
min-width: 125px;
|
||||
text-align: center;
|
||||
background: var(--body-background);
|
||||
border-radius: 3px;
|
||||
color: #999;
|
||||
box-shadow: 0 1px 4px var(--shadow-color);
|
||||
border: 1px solid transparent;
|
||||
color: var(--color-black);
|
||||
box-shadow: 0 9px 19px var(--shadow-color);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: background, border-color, color;
|
||||
|
||||
.link-icon {
|
||||
stroke: #999;
|
||||
}
|
||||
transition-property: box-shadow, color;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
background: var(--background-light);
|
||||
box-shadow: 0 3px 10px var(--shadow-color);
|
||||
|
||||
.link-icon {
|
||||
stroke: var(--color-primary);
|
||||
@ -45,9 +39,11 @@
|
||||
.link-icon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-bottom: 3px;
|
||||
margin-right: 10px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: stroke;
|
||||
opacity: 0.3;
|
||||
stroke: var(--color-black);
|
||||
}
|
||||
|
||||
@media (max-width: var(--screen-md)) {
|
||||
|
@ -31,14 +31,14 @@
|
||||
.projects-list-item {
|
||||
padding: 12px 15px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: background;
|
||||
transition-property: box-shadow;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 2px 4px var(--shadow-color);
|
||||
color: var(--color-black);
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
background: var(--background-light);
|
||||
box-shadow: 0 9px 19px var(--shadow-color);
|
||||
|
||||
.projectName {
|
||||
color: var(--color-primary);
|
||||
|
@ -18,6 +18,12 @@
|
||||
flex-direction: column;
|
||||
text-decoration: none;
|
||||
margin: 0 10px 30px;
|
||||
transition: 0.2s ease-in-out;
|
||||
transition-property: background;
|
||||
|
||||
&:focus {
|
||||
background: var(--background-light);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: var(--screen-md)) {
|
||||
|
@ -53,30 +53,21 @@
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.textEdit-cancel {
|
||||
margin-right: 20px;
|
||||
opacity: 0.6;
|
||||
color: var(--color-black);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.textEdit-button {
|
||||
padding: 1px 9px !important;
|
||||
position: absolute !important;
|
||||
top: 41px !important;
|
||||
right: 14px !important;
|
||||
}
|
||||
|
||||
.textEdit-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 14px;
|
||||
}
|
||||
|
||||
.textEdit-actions-warning {
|
||||
margin-right: 10px;
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
color: #bbb;
|
||||
.textEdit-actions-translate {
|
||||
margin-right: 5px;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.textEdit-actions-translate-icon {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
|
@ -64,15 +64,19 @@
|
||||
@value={{this.editText}}
|
||||
@onKeyUp={{fn this.changeText}}
|
||||
@onSubmit={{fn this.save}}
|
||||
/>
|
||||
|
||||
<AsyncButton
|
||||
@onClick={{fn this.save}}
|
||||
@loading={{this.isSaving}}
|
||||
@disabled={{not showSaveButton}}
|
||||
class="button button--filled button--iconOnly"
|
||||
local-class="textEdit-button"
|
||||
as |form|
|
||||
>
|
||||
{{t "components.related_translations_list.save_button"}}
|
||||
</AsyncButton>
|
||||
{{#component form.submit}}
|
||||
<div local-class="textEdit-actions">
|
||||
<AsyncButton
|
||||
@onClick={{fn this.save}}
|
||||
@loading={{this.isSaving}}
|
||||
@disabled={{not showSaveButton}}
|
||||
class="button button--filled button--iconOnly"
|
||||
>
|
||||
{{t "components.related_translations_list.save_button"}}
|
||||
</AsyncButton>
|
||||
</div>
|
||||
{{/component}}
|
||||
</TranslationEdit::Form>
|
||||
</li>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<div local-class="revision-export-options">
|
||||
<div class="filters filters--white" local-class="filters">
|
||||
<div class="filters" local-class="filters">
|
||||
<div class="filters-wrapper">
|
||||
<div local-class="exportOptions">
|
||||
{{#if this.showRevisions}}
|
||||
|
@ -1,8 +1,7 @@
|
||||
.translation-comments-list {
|
||||
position: relative;
|
||||
box-shadow: 0 1px 5px var(--shadow-color);
|
||||
box-shadow: 0 9px 19px var(--shadow-color);
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--background-light-highlight);
|
||||
}
|
||||
|
||||
.translation-comments-list.translationRemoved {
|
||||
|
@ -5,6 +5,7 @@ import {tracked} from '@glimmer/tracking';
|
||||
|
||||
interface Args {
|
||||
translation: any;
|
||||
text: string | null;
|
||||
project: any;
|
||||
permissions: Record<string, true>;
|
||||
onChangeText?: (text: string) => void;
|
||||
@ -45,6 +46,12 @@ export default class TranslationEdit extends Component<Args> {
|
||||
return this.text === this.args.translation.correctedText;
|
||||
}
|
||||
|
||||
@action
|
||||
setOriginalText() {
|
||||
this.text = this.args.translation.correctedText;
|
||||
this.args.onChangeText?.(this.text);
|
||||
}
|
||||
|
||||
@action
|
||||
async correctConflict() {
|
||||
this.isCorrectingConflict = true;
|
||||
@ -88,6 +95,7 @@ export default class TranslationEdit extends Component<Args> {
|
||||
|
||||
@action
|
||||
focusTextarea(element: HTMLElement) {
|
||||
if (!this.text) return;
|
||||
const focusable = element.querySelector('textarea');
|
||||
focusable?.focus();
|
||||
focusable?.setSelectionRange(this.text.length, this.text.length);
|
||||
|
@ -95,11 +95,13 @@ export default class TranslationEditForm extends Component<Args> {
|
||||
|
||||
@action
|
||||
async changeHTML(value: string) {
|
||||
const previousText = this.args.value;
|
||||
this.args.onKeyUp?.(value);
|
||||
}
|
||||
|
||||
if (previousText !== value)
|
||||
await (this.fetchLintMessagesTask as Task).perform(value);
|
||||
@action
|
||||
async changeText(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
this.args.onKeyUp?.(target.value);
|
||||
}
|
||||
|
||||
@action
|
||||
@ -107,14 +109,9 @@ export default class TranslationEditForm extends Component<Args> {
|
||||
this.args.onEscape?.();
|
||||
}
|
||||
|
||||
@action
|
||||
async changeText(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const previousText = this.args.value;
|
||||
this.args.onKeyUp?.(target.value);
|
||||
|
||||
if (previousText !== target.value)
|
||||
await (this.fetchLintMessagesTask as Task).perform(target.value);
|
||||
@restartableTask
|
||||
*onUpdateValue(_element: HTMLElement, [value]: string[]) {
|
||||
yield (this.fetchLintMessagesTask as Task).perform(value);
|
||||
}
|
||||
|
||||
@restartableTask
|
||||
@ -137,6 +134,5 @@ export default class TranslationEditForm extends Component<Args> {
|
||||
@action
|
||||
async replaceText(value: string) {
|
||||
this.args.onKeyUp?.(value);
|
||||
await (this.fetchLintMessagesTask as Task).perform(value);
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.label {
|
||||
width: 100%;
|
||||
padding: 5px 5px 5px 8px;
|
||||
|
@ -0,0 +1 @@
|
||||
{{yield}}
|
@ -1,4 +1,4 @@
|
||||
<div local-class="translation-edit-form">
|
||||
<div local-class="translation-edit-form" {{did-update (perform this.onUpdateValue) @value}}>
|
||||
{{#if @placeholders}}
|
||||
<div local-class="placeholders">
|
||||
<span local-class="placeholders-title">
|
||||
@ -84,21 +84,28 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if this.isHTMLType}}
|
||||
<HtmlTextarea
|
||||
@value={{@text}}
|
||||
@onChange={{fn this.changeHTML}}
|
||||
@wysiwygOptions={{this.wysiwygOptions}}
|
||||
/>
|
||||
<div local-class="input-wrapper">
|
||||
<HtmlTextarea
|
||||
@value={{@text}}
|
||||
@onChange={{fn this.changeHTML}}
|
||||
@wysiwygOptions={{this.wysiwygOptions}}
|
||||
/>
|
||||
|
||||
{{yield (hash submit=(component "translation-edit/form/submit"))}}
|
||||
</div>
|
||||
{{else}}
|
||||
<textarea
|
||||
{{autoresize @value}}
|
||||
{{on-key "Escape" (fn this.cancel)}}
|
||||
{{on-key "cmd+Enter" @onSubmit}}
|
||||
{{on "input" (fn this.changeText)}}
|
||||
local-class="inputText"
|
||||
value={{@value}}
|
||||
...attributes
|
||||
/>
|
||||
<div local-class="input-wrapper">
|
||||
<textarea
|
||||
{{autoresize @value}}
|
||||
{{on-key "Escape" (fn this.cancel)}}
|
||||
{{on-key "cmd+Enter" @onSubmit}}
|
||||
{{on "input" (fn this.changeText)}}
|
||||
local-class="inputText"
|
||||
value={{@value}}
|
||||
...attributes
|
||||
/>
|
||||
{{yield (hash submit=(component "translation-edit/form/submit"))}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#each this.lintMessages as |message|}}
|
||||
|
@ -69,9 +69,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.actions-updatedAt {
|
||||
margin-right: 10px;
|
||||
color: var(--color-grey);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
.actions-button-revert {
|
||||
opacity: 0.6;
|
||||
|
||||
:global(.label) {
|
||||
padding: 3px 2px;
|
||||
}
|
||||
}
|
||||
|
@ -33,10 +33,16 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
<div local-class="actions-buttons">
|
||||
<div local-class="actions-updatedAt">
|
||||
{{t "components.translation_edit.last_updated_label"}}
|
||||
<TimeAgoInWordsTag @date={{@translation.updatedAt}} />
|
||||
</div>
|
||||
{{#unless this.hasTextNotChanged}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.setOriginalText}}
|
||||
local-class="actions-button-revert"
|
||||
class="button button--iconOnly button--white"
|
||||
>
|
||||
{{inline-svg "/assets/revert.svg" class="button-icon"}}
|
||||
</AsyncButton>
|
||||
{{/unless}}
|
||||
|
||||
{{#if (get @permissions "update_translation")}}
|
||||
<AsyncButton
|
||||
@loading={{this.isUpdatingText}}
|
||||
@ -75,10 +81,16 @@
|
||||
{{/if}}
|
||||
</div>
|
||||
<div local-class="actions-buttons">
|
||||
<div local-class="actions-updatedAt">
|
||||
{{t "components.translation_edit.last_updated_label"}}
|
||||
<TimeAgoInWordsTag @date={{@translation.updatedAt}} />
|
||||
</div>
|
||||
{{#unless this.hasTextNotChanged}}
|
||||
<AsyncButton
|
||||
@onClick={{fn this.setOriginalText}}
|
||||
local-class="actions-button-revert"
|
||||
class="button button--iconOnly button--white"
|
||||
>
|
||||
{{inline-svg "/assets/revert.svg" class="button-icon"}}
|
||||
</AsyncButton>
|
||||
{{/unless}}
|
||||
|
||||
{{#if (get @permissions "update_translation")}}
|
||||
<AsyncButton
|
||||
@loading={{this.isUpdatingText}}
|
||||
@ -89,6 +101,7 @@
|
||||
{{t "components.translation_edit.update_text"}}
|
||||
</AsyncButton>
|
||||
{{/if}}
|
||||
|
||||
{{#unless @translation.version}}
|
||||
{{#if (get @permissions "uncorrect_translation")}}
|
||||
<AsyncButton
|
||||
|
@ -21,6 +21,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
.updatedAt {
|
||||
color: var(--color-grey);
|
||||
font-size: 11px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
width: 11px;
|
||||
height: 11px;
|
||||
|
@ -68,4 +68,20 @@
|
||||
{{t "components.translation_splash_title.master_label"}}
|
||||
</AccBadge>
|
||||
{{/if}}
|
||||
|
||||
{{#if @translation.lintMessages}}
|
||||
<AccBadge
|
||||
@warning={{true}}
|
||||
@icon={{true}}
|
||||
class="tooltip"
|
||||
title={{t "components.translations_list.lint_messages_label" count=@translation.lintMessages.length}}
|
||||
>
|
||||
{{inline-svg "/assets/warning.svg"}}
|
||||
</AccBadge>
|
||||
{{/if}}
|
||||
|
||||
<span local-class="updatedAt">
|
||||
{{t "components.translation_splash_title.last_updated_label"}}
|
||||
<TimeAgoInWordsTag @date={{@translation.updatedAt}} />
|
||||
</span>
|
||||
</div>
|
||||
|
@ -4,9 +4,15 @@ interface Args {
|
||||
isTextEmptyFilter: boolean;
|
||||
isTextNotEmptyFilter: boolean;
|
||||
isAddedLastSyncFilter: boolean;
|
||||
isConflictedFilter: boolean;
|
||||
isCommentedOnFilter: boolean;
|
||||
onChangeAdvancedFilterBoolean: (
|
||||
key: 'isTextEmpty' | 'isTextNotEmpty' | 'isAddedLastSync' | 'isCommentedOn',
|
||||
key:
|
||||
| 'isTextEmpty'
|
||||
| 'isTextNotEmpty'
|
||||
| 'isAddedLastSync'
|
||||
| 'isCommentedOn'
|
||||
| 'isConflicted',
|
||||
event: InputEvent
|
||||
) => void;
|
||||
}
|
||||
|
@ -14,13 +14,13 @@
|
||||
.label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 50%;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
margin-top: 10px;
|
||||
color: var(--color-grey);
|
||||
font-size: 12px;
|
||||
|
||||
input {
|
||||
margin-right: 4px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,5 +57,18 @@
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<label local-class="label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={{@isConflictedFilter}}
|
||||
{{on "change" (fn @onChangeAdvancedFilterBoolean "isConflicted")}}
|
||||
>
|
||||
<span>
|
||||
{{t
|
||||
"components.translations_filter.advanced_filters.conflicted"
|
||||
}}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -21,6 +21,7 @@ interface Args {
|
||||
isTextNotEmptyFilter: boolean;
|
||||
isAddedLastSyncFilter: boolean;
|
||||
isCommentedOnFilter: boolean;
|
||||
isConflictedFilter: boolean;
|
||||
onChangeQuery: (query: string) => void;
|
||||
onChangeDocument: () => void;
|
||||
onChangeVersion: () => void;
|
||||
|
@ -15,6 +15,7 @@
|
||||
"components.translations_filter.input_placeholder_text"
|
||||
}}
|
||||
value={{this.debouncedQuery}}
|
||||
{{on-key "Enter" (fn this.submitForm)}}
|
||||
{{on "keyup" (fn this.setDebouncedQuery)}}
|
||||
>
|
||||
</div>
|
||||
@ -72,6 +73,7 @@
|
||||
@isTextNotEmptyFilter={{@isTextNotEmptyFilter}}
|
||||
@isAddedLastSyncFilter={{@isAddedLastSyncFilter}}
|
||||
@isCommentedOnFilter={{@isCommentedOnFilter}}
|
||||
@isConflictedFilter={{@isConflictedFilter}}
|
||||
@onChangeAdvancedFilterBoolean={{@onChangeAdvancedFilterBoolean}}
|
||||
/>
|
||||
{{/if}}
|
||||
|
@ -100,6 +100,11 @@
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.item-text {
|
||||
display: block;
|
||||
width: 100%;
|
||||
|
@ -28,7 +28,7 @@
|
||||
</LinkTo>
|
||||
</span>
|
||||
|
||||
<span>
|
||||
<span local-class="item-meta">
|
||||
{{#if @translation.isConflicted}}
|
||||
<AccBadge @link={{true}} @primary={{true}}>
|
||||
<LinkTo
|
||||
@ -41,6 +41,18 @@
|
||||
</AccBadge>
|
||||
{{/if}}
|
||||
|
||||
{{#if @translation.lintMessages}}
|
||||
<AccBadge
|
||||
{{on "click" (fn this.toggleEdit)}}
|
||||
@warning={{true}}
|
||||
@icon={{true}}
|
||||
class="tooltip"
|
||||
title={{t "components.translations_list.lint_messages_label" count=@translation.lintMessages.length}}
|
||||
>
|
||||
{{inline-svg "/assets/warning.svg"}}
|
||||
</AccBadge>
|
||||
{{/if}}
|
||||
|
||||
{{#if @translation.commentsCount}}
|
||||
<AccBadge @link={{true}}>
|
||||
<LinkTo
|
||||
|
@ -0,0 +1,50 @@
|
||||
import {inject as service} from '@ember/service';
|
||||
import {action} from '@ember/object';
|
||||
import Controller from '@ember/controller';
|
||||
import RouterService from '@ember/routing/router-service';
|
||||
import MachineTranslations from 'accent-webapp/services/machine-translations';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
|
||||
export default class MachineTranslationsController extends Controller {
|
||||
@service('router')
|
||||
router: RouterService;
|
||||
|
||||
@service('machine-translations')
|
||||
machineTranslations: MachineTranslations;
|
||||
|
||||
@tracked
|
||||
translatedFileContent = '';
|
||||
|
||||
@action
|
||||
closeModal() {
|
||||
this.router.transitionTo('logged-in.project.files.index');
|
||||
}
|
||||
|
||||
get languages() {
|
||||
return this.model.project.revisions.map(
|
||||
(revision: {language: any}) => revision.language
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
resetContent() {
|
||||
this.translatedFileContent = '';
|
||||
}
|
||||
|
||||
@action
|
||||
async translate(
|
||||
file: File,
|
||||
fromLanguage: string,
|
||||
toLanguage: string,
|
||||
documentFormat: string
|
||||
) {
|
||||
const content = await this.machineTranslations.translateFile({
|
||||
project: this.model.project,
|
||||
file,
|
||||
fromLanguage,
|
||||
toLanguage,
|
||||
documentFormat,
|
||||
});
|
||||
this.translatedFileContent = content;
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
import Route from '@ember/routing/route';
|
||||
import MachineTranslationsController from 'accent-webapp/pods/logged-in/project/files/machine-translations/controller';
|
||||
|
||||
export default class MachineTranslationsRoute extends Route {
|
||||
resetController(
|
||||
controller: MachineTranslationsController,
|
||||
isExiting: boolean
|
||||
) {
|
||||
if (isExiting) {
|
||||
controller.translatedFileContent = '';
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
<AccModal @onClose={{fn this.closeModal}}>
|
||||
<MachineTranslationsTranslateUploadForm
|
||||
@languages={{this.languages}}
|
||||
@onFileChange={{fn this.translate}}
|
||||
@onFileReset={{fn this.resetContent}}
|
||||
@content={{this.translatedFileContent}}
|
||||
/>
|
||||
|
||||
<MachineTranslationsTranslatedFile
|
||||
@content={{this.translatedFileContent}}
|
||||
/>
|
||||
</AccModal>
|
||||
|
@ -17,6 +17,10 @@
|
||||
<DocumentsAddButton @project={{this.model.project}} />
|
||||
{{/if}}
|
||||
|
||||
{{#if (get this.permissions "machine_translations_translate_file")}}
|
||||
<DocumentsMachineTranslationsButton @project={{this.model.project}} />
|
||||
{{/if}}
|
||||
|
||||
<ResourcePagination
|
||||
@meta={{this.model.documents.meta}}
|
||||
@onSelectPage={{fn this.selectPage}}
|
||||
|
@ -5,9 +5,11 @@ import Controller from '@ember/controller';
|
||||
import translationCorrectQuery from 'accent-webapp/queries/correct-translation';
|
||||
import IntlService from 'ember-intl/services/intl';
|
||||
import FlashMessages from 'ember-cli-flash/services/flash-messages';
|
||||
import Apollo from 'accent-webapp/services/apollo';
|
||||
import ApolloMutate from 'accent-webapp/services/apollo-mutate';
|
||||
import GlobalState from 'accent-webapp/services/global-state';
|
||||
import {tracked} from '@glimmer/tracking';
|
||||
import projectTranslateTextQuery from 'accent-webapp/queries/translate-text-project';
|
||||
|
||||
const FLASH_MESSAGE_CORRECT_SUCCESS =
|
||||
'pods.project.conflicts.flash_messages.correct_success';
|
||||
@ -21,6 +23,9 @@ export default class ConflictsController extends Controller {
|
||||
@service('flash-messages')
|
||||
flashMessages: FlashMessages;
|
||||
|
||||
@service('apollo')
|
||||
apollo: Apollo;
|
||||
|
||||
@service('apollo-mutate')
|
||||
apolloMutate: ApolloMutate;
|
||||
|
||||
@ -56,6 +61,34 @@ export default class ConflictsController extends Controller {
|
||||
@and('emptyEntries', 'model.loading', 'emptyQuery', 'emptyDocument')
|
||||
showSkeleton: boolean;
|
||||
|
||||
@action
|
||||
async copyTranslation(
|
||||
text: string,
|
||||
sourceLanguageSlug: string,
|
||||
targetLanguageSlug: string
|
||||
) {
|
||||
try {
|
||||
const {data} = await this.apollo.client.query({
|
||||
fetchPolicy: 'network-only',
|
||||
query: projectTranslateTextQuery,
|
||||
variables: {
|
||||
text,
|
||||
sourceLanguageSlug,
|
||||
targetLanguageSlug,
|
||||
projectId: this.model.project.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (data.viewer?.project?.translatedText?.[0]) {
|
||||
return data.viewer.project.translatedText[0];
|
||||
} else {
|
||||
return {text: null};
|
||||
}
|
||||
} catch (error) {
|
||||
return {text: null};
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
async correctConflict(conflict: any, text: string) {
|
||||
try {
|
||||
|
@ -9,6 +9,7 @@
|
||||
@showSkeleton={{this.showSkeleton}}
|
||||
@query={{this.query}}
|
||||
@onCorrect={{fn this.correctConflict}}
|
||||
@onCopyTranslation={{fn this.copyTranslation}}
|
||||
@onSelectPage={{fn this.selectPage}}
|
||||
@onChangeDocument={{fn this.changeDocument}}
|
||||
@onChangeQuery={{fn this.changeQuery}}
|
||||
|
@ -32,6 +32,7 @@ export default class TranslationsController extends Controller {
|
||||
'isTextNotEmpty',
|
||||
'isAddedLastSync',
|
||||
'isCommentedOn',
|
||||
'isConflictedFilter',
|
||||
];
|
||||
|
||||
@tracked
|
||||
@ -55,6 +56,9 @@ export default class TranslationsController extends Controller {
|
||||
@tracked
|
||||
isAddedLastSync: 'true' | null = null;
|
||||
|
||||
@tracked
|
||||
isConflicted: 'true' | null = null;
|
||||
|
||||
@tracked
|
||||
isCommentedOn: 'true' | null = null;
|
||||
|
||||
@ -76,6 +80,7 @@ export default class TranslationsController extends Controller {
|
||||
this.isTextNotEmpty,
|
||||
this.isAddedLastSync,
|
||||
this.isCommentedOn,
|
||||
this.isConflicted,
|
||||
].filter((filter) => filter === 'true').length;
|
||||
}
|
||||
|
||||
@ -87,7 +92,12 @@ export default class TranslationsController extends Controller {
|
||||
|
||||
@action
|
||||
changeAdvancedFilterBoolean(
|
||||
key: 'isTextEmpty' | 'isTextNotEmpty' | 'isAddedLastSync' | 'isCommentedOn',
|
||||
key:
|
||||
| 'isTextEmpty'
|
||||
| 'isTextNotEmpty'
|
||||
| 'isAddedLastSync'
|
||||
| 'isCommentedOn'
|
||||
| 'isConflicted',
|
||||
event: InputEvent
|
||||
) {
|
||||
this[key] = (event.target as HTMLInputElement).checked ? 'true' : null;
|
||||
|
@ -46,6 +46,9 @@ export default class TranslationsRoute extends Route {
|
||||
isCommentedOn: {
|
||||
refreshModel: true,
|
||||
},
|
||||
isConflicted: {
|
||||
refreshModel: true,
|
||||
},
|
||||
};
|
||||
|
||||
subscription: Subscription;
|
||||
@ -55,6 +58,7 @@ export default class TranslationsRoute extends Route {
|
||||
params.isTextNotEmpty = params.isTextNotEmpty === 'true' ? true : null;
|
||||
params.isAddedLastSync = params.isAddedLastSync === 'true' ? true : null;
|
||||
params.isCommentedOn = params.isCommentedOn === 'true' ? true : null;
|
||||
params.isConflicted = params.isConflicted === 'true' ? true : null;
|
||||
|
||||
this.subscription = this.apolloSubscription.graphql(
|
||||
() => this.modelFor(this.routeName),
|
||||
|
@ -9,6 +9,7 @@
|
||||
@isTextNotEmptyFilter={{this.isTextNotEmpty}}
|
||||
@isAddedLastSyncFilter={{this.isAddedLastSync}}
|
||||
@isCommentedOnFilter={{this.isCommentedOn}}
|
||||
@isConflictedFilter={{this.isConflicted}}
|
||||
@onChangeQuery={{fn this.changeQuery}}
|
||||
@onChangeAdvancedFilterBoolean={{fn this.changeAdvancedFilterBoolean}}
|
||||
@onChangeDocument={{fn this.changeDocument}}
|
||||
|
@ -56,10 +56,12 @@ export default gql`
|
||||
revision {
|
||||
id
|
||||
isMaster
|
||||
slug
|
||||
name
|
||||
|
||||
language {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
@ -72,10 +74,12 @@ export default gql`
|
||||
id
|
||||
isMaster
|
||||
name
|
||||
slug
|
||||
|
||||
language {
|
||||
id
|
||||
name
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -90,6 +90,15 @@ export default gql`
|
||||
path
|
||||
}
|
||||
|
||||
revision {
|
||||
id
|
||||
name
|
||||
language {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
version {
|
||||
id
|
||||
tag
|
||||
|
@ -115,6 +115,12 @@ export default gql`
|
||||
name
|
||||
lastSyncedAt
|
||||
|
||||
lastActivitySync: lastActivity(action: "sync") {
|
||||
id
|
||||
action
|
||||
insertedAt
|
||||
}
|
||||
|
||||
documents {
|
||||
entries {
|
||||
id
|
||||
|
@ -8,6 +8,10 @@ export default gql`
|
||||
|
||||
revisions {
|
||||
id
|
||||
language {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
documents(page: $page) {
|
||||
|
24
webapp/app/queries/translate-text-project.ts
Normal file
24
webapp/app/queries/translate-text-project.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export default gql`
|
||||
query TranslateTextProject(
|
||||
$projectId: ID!
|
||||
$text: String
|
||||
$sourceLanguageSlug: String!
|
||||
$targetLanguageSlug: String!
|
||||
) {
|
||||
viewer {
|
||||
project(id: $projectId) {
|
||||
id
|
||||
translatedText(
|
||||
text: $text
|
||||
sourceLanguageSlug: $sourceLanguageSlug
|
||||
targetLanguageSlug: $targetLanguageSlug
|
||||
) {
|
||||
id
|
||||
text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -62,10 +62,12 @@ export default gql`
|
||||
revision {
|
||||
id
|
||||
name
|
||||
slug
|
||||
isMaster
|
||||
|
||||
language {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
@ -80,9 +82,11 @@ export default gql`
|
||||
id
|
||||
name
|
||||
isMaster
|
||||
slug
|
||||
|
||||
language {
|
||||
id
|
||||
slug
|
||||
name
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ export default gql`
|
||||
$isTextNotEmpty: Boolean
|
||||
$isAddedLastSync: Boolean
|
||||
$isCommentedOn: Boolean
|
||||
$isConflicted: Boolean
|
||||
) {
|
||||
viewer {
|
||||
project(id: $projectId) {
|
||||
@ -42,6 +43,7 @@ export default gql`
|
||||
isTextEmpty: $isTextEmpty
|
||||
isTextNotEmpty: $isTextNotEmpty
|
||||
isAddedLastSync: $isAddedLastSync
|
||||
isConflicted: $isConflicted
|
||||
isCommentedOn: $isCommentedOn
|
||||
) {
|
||||
meta {
|
||||
|
@ -41,6 +41,7 @@ export default Router.map(function () {
|
||||
this.route('activities');
|
||||
this.route('files', function () {
|
||||
this.route('new-sync');
|
||||
this.route('machine-translations');
|
||||
this.route('sync', {path: ':fileId/sync'});
|
||||
this.route('add-translations', {path: ':fileId/add-translations'});
|
||||
this.route('export', {path: ':fileId/export'});
|
||||
|
@ -17,6 +17,10 @@ interface PeekOptions {
|
||||
documentFormat: string;
|
||||
}
|
||||
|
||||
interface MachineTranslationsTranslateFileOptions {
|
||||
file: File;
|
||||
}
|
||||
|
||||
export default class AuthenticatedRequest extends Service {
|
||||
@service('session')
|
||||
session: Session;
|
||||
@ -25,6 +29,15 @@ export default class AuthenticatedRequest extends Service {
|
||||
return this.postFile(url, options);
|
||||
}
|
||||
|
||||
async machineTranslationsTranslateFile(
|
||||
url: string,
|
||||
options: MachineTranslationsTranslateFileOptions
|
||||
) {
|
||||
const response = await this.postFile(url, options);
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
async peek(url: string, options: PeekOptions) {
|
||||
const response = await this.postFile(url, options);
|
||||
|
||||
@ -43,7 +56,13 @@ export default class AuthenticatedRequest extends Service {
|
||||
return response.text();
|
||||
}
|
||||
|
||||
private async postFile(url: string, options: PeekOptions | CommitOptions) {
|
||||
private async postFile(
|
||||
url: string,
|
||||
options:
|
||||
| PeekOptions
|
||||
| CommitOptions
|
||||
| MachineTranslationsTranslateFileOptions
|
||||
) {
|
||||
const fetchOptions: RequestInit = {};
|
||||
|
||||
fetchOptions.method = 'POST';
|
||||
@ -63,19 +82,17 @@ export default class AuthenticatedRequest extends Service {
|
||||
return response;
|
||||
}
|
||||
|
||||
private setupFormFile({
|
||||
file,
|
||||
documentPath,
|
||||
documentFormat,
|
||||
}: {
|
||||
private setupFormFile(options: {
|
||||
file: File;
|
||||
documentPath: string;
|
||||
documentFormat: string;
|
||||
documentPath?: string;
|
||||
documentFormat?: string;
|
||||
}) {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('document_path', documentPath);
|
||||
formData.append('document_format', documentFormat);
|
||||
formData.append('file', options.file);
|
||||
if (options.documentPath)
|
||||
formData.append('document_path', options.documentPath);
|
||||
if (options.documentFormat)
|
||||
formData.append('document_format', options.documentFormat);
|
||||
|
||||
return formData;
|
||||
}
|
||||
|
43
webapp/app/services/machine-translations.ts
Normal file
43
webapp/app/services/machine-translations.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import fmt from 'simple-fmt';
|
||||
import Service, {inject as service} from '@ember/service';
|
||||
import config from 'accent-webapp/config/environment';
|
||||
import AuthenticatedRequest from 'accent-webapp/services/authenticated-request';
|
||||
|
||||
interface TranslateFileOptions {
|
||||
project: any;
|
||||
file: File;
|
||||
toLanguage: string;
|
||||
fromLanguage: string;
|
||||
documentFormat: string;
|
||||
}
|
||||
|
||||
export default class MachineTranslations extends Service {
|
||||
@service('authenticated-request')
|
||||
authenticatedRequest: AuthenticatedRequest;
|
||||
|
||||
async translateFile({
|
||||
project,
|
||||
file,
|
||||
toLanguage,
|
||||
fromLanguage,
|
||||
documentFormat,
|
||||
}: TranslateFileOptions) {
|
||||
const url = fmt(
|
||||
config.API.MACHINE_TRANSLATIONS_TRANSLATE_FILE_PROJECT_PATH,
|
||||
project.id,
|
||||
fromLanguage,
|
||||
toLanguage,
|
||||
documentFormat
|
||||
);
|
||||
|
||||
return this.authenticatedRequest.machineTranslationsTranslateFile(url, {
|
||||
file,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare module '@ember/service' {
|
||||
interface Registry {
|
||||
'machine-translations': MachineTranslations;
|
||||
}
|
||||
}
|
@ -197,14 +197,16 @@ html {
|
||||
--color-primary-hue: 155;
|
||||
--color-primary-saturation: 67%;
|
||||
--color-primary-darken-10: #1c9060;
|
||||
--color-primary-opacity-10: #233e32;
|
||||
--color-primary-opacity-50: #296349;
|
||||
--color-primary-opacity-70: #238458;
|
||||
--color-primary-opacity-10: rgba(40, 203, 135, 0.1);
|
||||
--color-primary-opacity-50: rgba(40, 203, 135, 0.5);
|
||||
--color-primary-opacity-70: rgba(40, 203, 135, 0.7);
|
||||
|
||||
--color-highlight-lighteness: 95%;
|
||||
|
||||
--color-grey: #adadad;
|
||||
--color-black: #484848;
|
||||
--background-tooltip: #484848;
|
||||
--text-color-tooltip: #fff;
|
||||
|
||||
--color-green: #28cb87;
|
||||
--color-green-hue: 155;
|
||||
@ -228,7 +230,7 @@ html {
|
||||
--color-error-hue: 0;
|
||||
--color-error-saturation: 65%;
|
||||
|
||||
--shadow-color: rgba(0, 0, 0, 0.1);
|
||||
--shadow-color: rgba(0, 0, 0, 0.07);
|
||||
|
||||
background: #eee;
|
||||
}
|
||||
@ -257,6 +259,9 @@ html[data-theme='dark'] {
|
||||
--color-black: #adadad;
|
||||
--color-grey: #616161;
|
||||
|
||||
--background-tooltip: #484848;
|
||||
--text-color-tooltip: #fff;
|
||||
|
||||
background: #171717;
|
||||
}
|
||||
|
||||
@ -300,6 +305,46 @@ a {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:global(.tooltip) {
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
margin-left: -5px;
|
||||
border: 6px solid;
|
||||
border-color: transparent var(--background-tooltip) transparent transparent;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: attr(title);
|
||||
text-transform: none;
|
||||
margin-left: 7px;
|
||||
padding: 5px 10px;
|
||||
white-space: nowrap;
|
||||
border-radius: 4px;
|
||||
background: var(--background-tooltip);
|
||||
color: var(--text-color-tooltip);
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
position: absolute;
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
transition: 0.3s opacity;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover::before,
|
||||
&:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.added) {
|
||||
padding: 0 3px;
|
||||
background: hsl(
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user