Add languagetool vendored integration for performant and accurate spelling lint check

This commit is contained in:
Simon Prévost 2023-10-06 13:49:32 -04:00
parent 44a32ac1e9
commit 2aa9b82e5a
68 changed files with 1466 additions and 508 deletions

View File

@ -33,3 +33,5 @@ jipt/.cache
jipt/dist
jipt/webapp-dist
cli
priv/native/language-tool.jar

View File

@ -1,5 +1,11 @@
[
inputs: ["mix.exs", ".formatter.exs", ".credo.exs", "{config,lib,test,rel,priv}/**/*.{ex,exs}"],
inputs: [
"mix.exs",
".formatter.exs",
".credo.exs",
"vendor/language_tool/**/*.ex",
"{config,lib,test,rel,priv}/**/*.{ex,exs}"
],
plugins: [Styler],
import_deps: [:ecto, :phoenix],
line_length: 120

4
.gitignore vendored
View File

@ -43,3 +43,7 @@ cli/*-debug.log
cli/*-error.log
cli/.oclif.manifest.json
.elixir_ls
priv/native/language-tool.jar
vendor/language_tool/priv/native/languagetool/.gradle
vendor/language_tool/priv/native/languagetool/app/build

View File

@ -2,7 +2,10 @@
## v1.19.0
Added: Optional spelling server to add spelling check to linting.
Added: Spellchecker with https://languagetool.org/
We bundle a jar file and install Java runtime in docker to be able to use languagetool spellchecker locally and very fast.
This is the first version of the integration, we are still missing some important feature like "Ignore rules", "Custom project dictionary" or "Placeholder handling".
## v1.18.4

View File

@ -24,7 +24,7 @@ RUN npm ci --no-audit --no-color && \
#
# Build the OTP binary
#
FROM hexpm/elixir:1.14.3-erlang-25.1.2-debian-bullseye-20221004-slim AS builder
FROM hexpm/elixir:1.15.7-erlang-26.1.2-debian-bullseye-20230612-slim AS builder
ENV MIX_ENV=prod
@ -32,7 +32,7 @@ WORKDIR /build
# Install Debian dependencies
RUN apt-get update -y && \
apt-get install -y build-essential git libyaml-dev && \
apt-get install -y build-essential git libyaml-dev default-jre && \
apt-get clean && \
rm -f /var/lib/apt/lists/*_*
@ -48,6 +48,12 @@ COPY mix.lock .
RUN mix deps.get --only prod
RUN mix deps.compile --only prod
COPY vendor vendor
RUN cd ./vendor/language_tool/priv/native/languagetool && ./gradlew shadowJar
RUN cp ./vendor/language_tool/priv/native/languagetool/app/build/libs/language-tool.jar priv/native/language-tool.jar
RUN mix compile --only prod
# Move static assets from other stages into the OTP release.
@ -65,12 +71,10 @@ RUN mkdir -p /opt/build && \
#
# Build a lean runtime container
#
FROM alpine:3.17.0
FROM debian:bullseye-20230109
FROM debian:bullseye-20231009
RUN apt-get update -y && \
apt-get install -y bash libyaml-dev openssl libncurses5 locales fontconfig hunspell hunspell-fr hunspell-en-ca hunspell-en-us hunspell-es && \
apt-get install -y default-jre bash libyaml-dev openssl libncurses5 locales fontconfig hunspell hunspell-fr hunspell-en-ca hunspell-en-us hunspell-es && \
apt-get clean && \
rm -f /var/lib/apt/lists/*_*

View File

@ -67,6 +67,12 @@ build: ## Build the Docker image for the OTP release
compose-build: ## Build the Docker image from the docker-compose.yml file
docker-compose build
.PHONY: build-language-tool
build-language-tool:
rm -f vendor/language_tool/priv/native/language-tool.jar
cd vendor/language_tool/priv/native/languagetool && ./gradlew shadowJar
cp vendor/language_tool/priv/native/languagetool/app/build/libs/language-tool.jar priv/native/language-tool.jar
# CI targets
# ----------

View File

@ -124,19 +124,11 @@ config :ueberauth, Ueberauth.Strategy.Microsoft.OAuth,
client_secret: get_env("MICROSOFT_CLIENT_SECRET"),
tenant_id: get_env("MICROSOFT_TENANT_ID")
config :accent, Accent.Lint, spelling_server_url: get_env("SPELLING_SERVER_URL")
config :accent, Accent.WebappView,
path: "priv/static/webapp/index.html",
sentry_dsn: get_env("WEBAPP_SENTRY_DSN") || "",
skip_subresource_integrity: get_env("WEBAPP_SKIP_SUBRESOURCE_INTEGRITY", :boolean)
if get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY") do
config :goth, json: get_env("GOOGLE_TRANSLATIONS_SERVICE_ACCOUNT_KEY")
else
config :goth, disabled: true
end
config :tesla, logger_enabled: true
config :new_relic_agent,

View File

@ -10,6 +10,7 @@ defmodule Accent do
Accent.Repo,
{Oban, oban_config()},
Accent.Vault,
{LanguageTool.Server, language_tool_config()},
{TelemetryUI, Accent.TelemetryUI.config()},
{Phoenix.PubSub, [name: Accent.PubSub, adapter: Phoenix.PubSub.PG2]}
]
@ -33,6 +34,14 @@ defmodule Accent do
:ok
end
defp language_tool_config do
[
languages:
~w(ar be-BY br-FR ca-ES da-DK de de-AT de-CH de-DE de-LU el-GR en en-AU en-CA en-GB en-NZ en-US en-ZA eo es es-AR es-ES fa fa-IR fr fr-BE fr-CA fr-CH fr-FR it it-IT ja-JP nl nl-BE nl-NL pl-PL pt pt-AO pt-BR pt-MZ pt-PT ro-RO ru-RU sk-SK sl-SI sv sv-SE ta-IN tl-PH uk-UA zh-CN),
disabled_rule_ids: ~w(UPPERCASE_SENTENCE_START POINTS_2 FRENCH_WHITESPACE DETERMINER_SENT_END)
]
end
defp oban_config do
opts = Application.get_env(:accent, Oban)

View File

@ -66,10 +66,20 @@ defmodule Accent.Translation do
locked: translation.locked,
plural: translation.plural,
placeholders: translation.placeholders,
placeholder_regex: extract_translation_document_placeholder_regex(translation),
language_slug: language_slug
}
end
defp extract_translation_document_placeholder_regex(translation) do
with true <- is_struct(translation.document, Accent.Document),
{:ok, regex} <- Langue.placeholder_regex_from_format(translation.document.format) do
regex
else
_ -> nil
end
end
def maybe_natural_order_by(translations, "key") do
Enum.sort_by(translations, & &1.key)
end

View File

@ -12,7 +12,7 @@ defmodule Accent.Scopes.Project do
iex> Accent.Scopes.Project.from_search(Accent.Project, 1234)
Accent.Project
iex> Accent.Scopes.Project.from_search(Accent.Project, "test")
#Ecto.Query<from p0 in Accent.Project, where: ilike(p0.name, ^"%test%")>
#Ecto.Query<from p0 in Accent.Project, where: ilike(p0.name, ^"%test%") or ^false>
"""
@spec from_search(Ecto.Queryable.t(), any()) :: Ecto.Queryable.t()
def from_search(query, term) do

View File

@ -14,25 +14,19 @@ defmodule Accent.Scopes.Search do
iex> Accent.Scopes.Search.from_search(Accent.Project, "test", :name)
#Ecto.Query<from p0 in Accent.Project, where: ilike(p0.name, ^"%test%")>
"""
@spec from_search(Ecto.Queryable.t(), any(), atom()) :: Ecto.Queryable.t()
@spec from_search(Ecto.Queryable.t(), any(), atom() | list(atom())) :: Ecto.Queryable.t()
def from_search(query, nil, _), do: query
def from_search(query, term, _) when term === "", do: query
def from_search(query, term, _) when not is_binary(term), do: query
def from_search(query, term, fields) when is_list(fields) do
def from_search(query, term, fields) do
term = "%" <> term <> "%"
conditions =
Enum.reduce(fields, false, fn field, conditions ->
Enum.reduce(List.wrap(fields), false, fn field, conditions ->
dynamic([q], ilike(field(q, ^field), ^term) or ^conditions)
end)
from(query, where: ^conditions)
end
def from_search(query, term, field) do
term = "%" <> term <> "%"
from(q in query, where: ilike(field(q, ^field), ^term))
end
end

View File

@ -345,7 +345,7 @@ defmodule Accent.Scopes.Translation do
iex> Accent.Scopes.Translation.from_search(Accent.Translation, 1234)
Accent.Translation
iex> Accent.Scopes.Translation.from_search(Accent.Translation, "test")
#Ecto.Query<from t0 in Accent.Translation, where: ilike(t0.key, ^\"%test%\") or ilike(t0.corrected_text, ^\"%test%\")>
#Ecto.Query<from t0 in Accent.Translation, where: ilike(t0.corrected_text, ^\"%test%\") or (ilike(t0.key, ^\"%test%\") or ^false)>
"""
@spec from_search(Queryable.t(), any()) :: Queryable.t()
def from_search(query, nil), do: query
@ -353,12 +353,8 @@ defmodule Accent.Scopes.Translation do
def from_search(query, term) when not is_binary(term), do: query
def from_search(query, search_term) do
term = "%" <> search_term <> "%"
from_search_id(
from(translation in query,
where: ilike(translation.key, ^term) or ilike(translation.corrected_text, ^term)
),
Accent.Scopes.Search.from_search(query, search_term, [:key, :corrected_text]),
search_term
)
end

View File

@ -12,6 +12,7 @@ defmodule Accent.TelemetryUI do
{"Absinthe", absinthe_metrics(), ui_options: [metrics_class: "grid-cols-8 gap-4"]},
{"Ecto", ecto_metrics(), ui_options: [metrics_class: "grid-cols-8 gap-4"]},
{"PSQL Extras", EctoPSQLExtras.all(Accent.Repo)},
{"Lint", lint_metrics(), ui_options: [metrics_class: "grid-cols-8 gap-4"]},
{"System", system_metrics()}
],
theme: theme(),
@ -19,6 +20,47 @@ defmodule Accent.TelemetryUI do
]
end
def lint_metrics do
[
counter("accent.language_tool.check.stop.duration",
description: "Number of spellchecks",
unit: {:native, :millisecond},
ui_options: [class: "col-span-3", unit: " checks"]
),
count_over_time("accent.language_tool.check.stop.duration",
description: "Number of spellchecks over time",
unit: {:native, :millisecond},
ui_options: [class: "col-span-5", unit: " checks"]
),
average("accent.language_tool.check.stop.duration",
description: "Spellchecks duration",
unit: {:native, :millisecond},
ui_options: [class: "col-span-3", unit: " ms"]
),
average_over_time("accent.language_tool.check.stop.duration",
description: "Spellchecks duration over time",
unit: {:native, :millisecond},
ui_options: [class: "col-span-5", unit: " ms"]
),
count_over_time("accent.language_tool.check.stop.duration",
description: "Spellchecks per language over time",
tags: [:language_code],
unit: {:native, :millisecond},
ui_options: [unit: " checks"]
),
count_list("accent.language_tool.check.stop.duration",
description: "Count spellchecks by language",
tags: [:language_code],
unit: {:native, :millisecond},
ui_options: [unit: " checks"]
),
average_over_time("accent.language_tool.check.stop.duration",
description: "Spellchecks duration per language",
tags: [:language_code]
)
]
end
def http_metrics do
http_keep = &(&1[:route] not in ~w(/metrics /graphql))
@ -52,23 +94,20 @@ defmodule Accent.TelemetryUI do
keep: http_keep,
tags: [:route],
unit: {:native, :millisecond},
ui_options: [unit: " requests"],
reporter_options: [class: "col-span-4"]
ui_options: [unit: " requests"]
),
counter("phoenix.router_dispatch.stop.duration",
count_list("phoenix.router_dispatch.stop.duration",
description: "Count HTTP requests by route",
keep: http_keep,
tags: [:route],
unit: {:native, :millisecond},
ui_options: [unit: " requests"],
reporter_options: [class: "col-span-4"]
ui_options: [unit: " requests"]
),
average_over_time("phoenix.router_dispatch.stop.duration",
description: "HTTP requests duration per route",
keep: http_keep,
tags: [:route],
unit: {:native, :millisecond},
reporter_options: [class: "col-span-4"]
unit: {:native, :millisecond}
),
distribution("phoenix.router_dispatch.stop.duration",
description: "Requests duration",
@ -97,12 +136,19 @@ defmodule Accent.TelemetryUI do
unit: {:native, :millisecond},
ui_options: [class: "col-span-5", unit: " ms"]
),
average("accent.repo.query.total_time",
average_list("accent.repo.query.total_time",
description: "Database query total time per source",
keep: ecto_keep,
tags: [:source],
unit: {:native, :millisecond},
ui_options: [class: "col-span-full", unit: " ms"]
),
count_list("accent.repo.query.total_time",
description: "Database query count per source",
keep: ecto_keep,
tags: [:source],
unit: {:native, :millisecond},
ui_options: [class: "col-span-full", unit: " ms"]
)
]
end
@ -139,7 +185,7 @@ defmodule Accent.TelemetryUI do
unit: {:native, :millisecond},
ui_options: [class: "col-span-5", unit: " ms"]
),
counter("absinthe.execute.operation.stop.duration",
count_list("absinthe.execute.operation.stop.duration",
description: "Count Absinthe executions per operation",
tags: [:operation_name],
tag_values: absinthe_tag_values,
@ -151,13 +197,6 @@ defmodule Accent.TelemetryUI do
tag_values: absinthe_tag_values,
unit: {:native, :millisecond}
),
count_list("absinthe.resolve.field.stop.duration",
description: "Absinthe field resolve count",
tags: [:resolution_path],
keep: list_keep,
tag_values: list_tag_values,
unit: {:native, :millisecond}
),
average_list("absinthe.resolve.field.stop.duration",
description: "Absinthe field resolve",
tags: [:resolution_path],
@ -305,7 +344,7 @@ defmodule Accent.TelemetryUI do
pruner_threshold: [months: -1],
pruner_interval_ms: 84_000,
max_buffer_size: 10_000,
flush_interval_ms: 1_000,
flush_interval_ms: 30_000,
verbose: false
}
end

View File

@ -38,7 +38,7 @@ defmodule Accent.GraphQL.Resolvers.Lint do
end
def preload_translations(_, [translation | _] = translations) do
translations = Repo.preload(translations, revision: :language)
translations = Repo.preload(translations, [:document, [revision: :language]])
project =
translation

View File

@ -135,7 +135,7 @@ defmodule Accent.GraphQL.Resolvers.Project do
|> TranslationScope.active()
|> TranslationScope.not_locked()
|> Query.distinct(true)
|> Query.preload(revision: :language)
|> Query.preload([:document, [revision: :language]])
|> Query.order_by({:asc, :key})
|> Repo.all()

View File

@ -12,6 +12,7 @@ defmodule Langue.Entry do
locked: false,
plural: false,
placeholders: [],
placeholder_regex: nil,
language_slug: nil
@type t :: %__MODULE__{
@ -26,6 +27,7 @@ defmodule Langue.Entry do
locked: boolean(),
plural: boolean(),
placeholders: list(binary),
placeholder_regex: Regex.t() | nil,
language_slug: String.t()
}
end

View File

@ -7,5 +7,5 @@ defmodule Langue.Formatter.Json do
parser: Langue.Formatter.Json.Parser,
serializer: Langue.Formatter.Json.Serializer
def placeholder_regex, do: ~r/{{[^}]*}}/
def placeholder_regex, do: ~r/{{?[^}]*}?}/
end

View File

@ -7,5 +7,5 @@ defmodule Langue.Formatter.SimpleJson do
parser: Langue.Formatter.SimpleJson.Parser,
serializer: Langue.Formatter.SimpleJson.Serializer
def placeholder_regex, do: ~r/{{[^}]*}}/
def placeholder_regex, do: ~r/{{?[^}]*}?}/
end

View File

@ -34,6 +34,12 @@ defmodule Langue do
def serializer_from_format(_), do: {:error, :unknown_serializer}
for module <- @format_modules, id = module.id() do
def placeholder_regex_from_format(unquote(id)), do: {:ok, unquote(module).placeholder_regex()}
end
def placeholder_regex_from_format(_), do: {:error, :unknown_format}
def placeholder_regex do
@format_modules
|> Enum.map(& &1.placeholder_regex())

View File

@ -6,32 +6,35 @@ defmodule Accent.Lint.Checks.Spelling do
alias Accent.Lint.Replacement
@impl true
def enabled?, do: not is_nil(base_url())
def enabled?, do: LanguageTool.available?()
@impl true
def applicable(entry) do
((!entry.is_master and entry.value !== entry.master_value) or entry.is_master) and
LanguageTool.ready?() and
not String.match?(entry.value, ~r/MMM|YYY|HH|AA/i) and
not String.starts_with?(entry.value, "{") and
((!entry.is_master and entry.value !== entry.master_value) or entry.is_master) and
String.length(entry.value) < 100 and String.length(entry.value) > 3
end
@impl true
def check(entry) do
req =
Req.new(
base_url: base_url(),
params: %{text: entry.value, language: build_language_slug(entry.language_slug)}
)
matches = Req.get!(req, url: "/v2/check").body["matches"]
{matches, markups} =
case LanguageTool.check(entry.language_slug, entry.value, placeholder_regex: entry.placeholder_regex) do
%{"matches" => matches, "markups" => markups} -> {matches, markups}
_ -> {[], []}
end
for match <- matches do
offset = match["offset"] + length(markups)
replacement =
case match["replacements"] do
[%{"value" => fixed_value} | _] ->
value =
String.replace(
entry.value,
String.slice(entry.value, match["offset"], match["length"]),
String.slice(entry.value, offset, match["length"]),
fixed_value
)
@ -44,23 +47,11 @@ defmodule Accent.Lint.Checks.Spelling do
%Message{
check: :spelling,
text: entry.value,
offset: match["offset"],
offset: offset,
length: match["length"],
message: match["message"],
replacement: replacement
}
end
end
defp build_language_slug(slug) do
if String.match?(slug, ~r/..-.*/) do
slug
else
slug <> "-CA"
end
end
defp base_url do
Application.get_env(:accent, Accent.Lint)[:spelling_server_url]
end
end

View File

@ -157,7 +157,7 @@ defmodule Accent.MachineTranslations.Provider.GoogleTranslate do
@impl Tesla.Middleware
def call(env, next, opts) do
case auth_enabled?() && Goth.Token.for_scope(opts[:scope]) do
case auth_enabled?() && Goth.Token.fetch(%{source: opts}) do
{:ok, %{token: token, type: type}} ->
env
|> Tesla.put_header("authorization", type <> " " <> token)
@ -174,13 +174,13 @@ defmodule Accent.MachineTranslations.Provider.GoogleTranslate do
end
defp client(config) do
project_id = project_id_from_config(config)
{base_url, auth_source} = parse_auth_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.BaseUrl, base_url},
{Auth, auth_source},
Middleware.DecodeJson,
Middleware.EncodeJson,
Middleware.Logger,
@ -190,9 +190,16 @@ defmodule Accent.MachineTranslations.Provider.GoogleTranslate do
Tesla.client(middlewares)
end
defp project_id_from_config(config) do
key = Map.fetch!(config, "key")
Jason.decode!(key)["project_id"]
defp parse_auth_config(config) do
config = Jason.decode!(Map.fetch!(config, "key"))
case config do
%{"project_id" => project_id, "type" => "service_account"} = credentials ->
{
"https://translation.googleapis.com/v3/projects/#{project_id}",
{:service_account, credentials, [scopes: ["https://www.googleapis.com/auth/cloud-translation"]]}
}
end
end
end
end

View File

@ -112,7 +112,7 @@ defmodule Accent.LintController do
|> base_translations(conn)
|> TranslationScope.from_revision(conn.assigns[:revision].id)
|> Repo.all()
|> Repo.preload(:revision)
|> Repo.preload([:revision, :document])
|> Map.new(&{{&1.key, &1.document_id}, &1})
assign(conn, :translations, translations)

View File

@ -42,7 +42,6 @@ defmodule Accent.Mixfile do
# Plugs
{:plug_assign, "~> 1.0.0"},
{:canada, "~> 1.0"},
{:canary, "~> 1.1.0"},
{:corsica, "~> 2.0"},
{:plug_cowboy, "~> 2.0"},
@ -57,8 +56,8 @@ defmodule Accent.Mixfile do
{:postgrex, "~> 0.14"},
{:cloak_ecto, "~> 1.2"},
# Spelling
{:req, "~> 0.1"},
# Spelling interop with Java runtime
{:exile, "~> 0.7"},
# Phoenix data helpers
{:phoenix_ecto, "~> 4.0"},
@ -113,7 +112,7 @@ defmodule Accent.Mixfile do
{:mock, "~> 0.3.0", only: :test},
# Google API authentication
{:goth, "~> 1.2.0"},
{:goth, "~> 1.4"},
# Network request
{:tesla, "~> 1.3"},

View File

@ -8,40 +8,40 @@
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
"canada": {:hex, :canada, "1.0.2", "040e4c47609b0a67d5773ac1fbe5e99f840cef173d69b739beda7c98453e0770", [:mix], [], "hexpm", "4269f74153fe89583fe50bd4d5de57bfe01f31258a6b676d296f3681f1483c68"},
"canary": {:hex, :canary, "1.1.1", "4138d5e05db8497c477e4af73902eb9ae06e49dceaa13c2dd9f0b55525ded48b", [:mix], [{:canada, "~> 1.0.1", [hex: :canada, repo: "hexpm", optional: false]}, {:ecto, ">= 1.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "f348d9848693c830a65b707bba9e4dfdd6434e8c356a8d4477e4535afb0d653b"},
"castore": {:hex, :castore, "1.0.3", "7130ba6d24c8424014194676d608cb989f62ef8039efd50ff4b3f33286d06db8", [:mix], [], "hexpm", "680ab01ef5d15b161ed6a95449fac5c6b8f60055677a8e79acf01b27baa4390b"},
"castore": {:hex, :castore, "1.0.4", "ff4d0fb2e6411c0479b1d965a814ea6d00e51eb2f58697446e9c41a97d940b28", [:mix], [], "hexpm", "9418c1b8144e11656f0be99943db4caf04612e3eaecefb5dae9a2a87565584f8"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
"cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"con_cache": {:hex, :con_cache, "0.14.0", "863acb90fa08017be3129074993af944cf7a4b6c3ee7c06c5cd0ed6b94fbc223", [:mix], [], "hexpm", "50887a8949377d0b707a3c6653b7610de06074751b52d0f267f52135f391aece"},
"corsica": {:hex, :corsica, "2.1.2", "0f1bc7648f9a41abca557c8158c110269d61a1465468be2416621991e316ff56", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c778c6ded25ec78c57c64a7b769edb388ec8f162ea3b6f10fa2580fb13fb2afb"},
"corsica": {:hex, :corsica, "2.1.3", "dccd094ffce38178acead9ae743180cdaffa388f35f0461ba1e8151d32e190e6", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "616c08f61a345780c2cf662ff226816f04d8868e12054e68963e95285b5be8bc"},
"cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.12.1", "a9fa9a625f1d2025fe6b462cb865881329b5caff8f1854d1cbc9f9533f00e1e1", [:make, :rebar3], [], "hexpm", "163b73f6367a7341b33c794c4e88e7dbfe6498ac42dcd69ef44c5bc5507c8db0"},
"credo": {:hex, :credo, "1.7.0", "6119bee47272e85995598ee04f2ebbed3e947678dee048d10b5feca139435f75", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "6839fcf63d1f0d1c0f450abc8564a57c43d644077ab96f2934563e68b8a769d7"},
"credo": {:hex, :credo, "1.7.1", "6e26bbcc9e22eefbff7e43188e69924e78818e2fe6282487d0703652bc20fd62", [:mix], [{:bunt, "~> 0.2.1", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2.8", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "e9871c6095a4c0381c89b6aa98bc6260a8ba6addccf7f6a53da8849c748a58a2"},
"credo_envvar": {:hex, :credo_envvar, "0.1.4", "40817c10334e400f031012c0510bfa0d8725c19d867e4ae39cf14f2cbebc3b20", [:mix], [{:credo, "~> 1.0", [hex: :credo, repo: "hexpm", optional: false]}], "hexpm", "5055cdb4bcbaf7d423bc2bb3ac62b4e2d825e2b1e816884c468dee59d0363009"},
"csv": {:hex, :csv, "2.5.0", "c47b5a5221bf2e56d6e8eb79e77884046d7fd516280dc7d9b674251e0ae46246", [:mix], [{:parallel_stream, "~> 1.0.4 or ~> 1.1.0", [hex: :parallel_stream, repo: "hexpm", optional: false]}], "hexpm", "e821f541487045c7591a1963eeb42afff0dfa99bdcdbeb3410795a2f59c77d34"},
"dataloader": {:hex, :dataloader, "2.0.0", "49b42d60b9bb06d761a71d7b034c4b34787957e713d4fae15387a25fcd639112", [:mix], [{:ecto, ">= 3.4.3 and < 4.0.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:opentelemetry_process_propagator, "~> 0.2.1", [hex: :opentelemetry_process_propagator, repo: "hexpm", optional: true]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "09d61781b76ce216e395cdbc883ff00d00f46a503e215c22722dba82507dfef0"},
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
"db_connection": {:hex, :db_connection, "2.6.0", "77d835c472b5b67fc4f29556dee74bf511bbafecdcaf98c27d27fa5918152086", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c2f992d15725e721ec7fbc1189d4ecdb8afef76648c746a8e1cad35e3b8a35f3"},
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
"dialyxir": {:hex, :dialyxir, "1.4.1", "a22ed1e7bd3a3e3f197b68d806ef66acb61ee8f57b3ac85fc5d57354c5482a93", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "84b795d6d7796297cca5a3118444b80c7d94f7ce247d49886e7c291e1ae49801"},
"dialyxir": {:hex, :dialyxir, "1.4.2", "764a6e8e7a354f0ba95d58418178d486065ead1f69ad89782817c296d0d746a5", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "516603d8067b2fd585319e4b13d3674ad4f314a5902ba8130cd97dc902ce6bbd"},
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
"ecto_dev_logger": {:hex, :ecto_dev_logger, "0.9.0", "cb631469ac1940e97655d6fce85905b792ac9250ab18b19c664978b79f8dad59", [:mix], [{:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "2e8bc98b4ae4fcc7108896eef7da5a109afad829f4fb2eb46d677fdc9101c2d5"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.13", "9947637f82b92dcec93d44ad09ba24d1990bd7ca69e1c68981fb3b6f8bd18829", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "0f2288e6163f6aacd7e59545a56adc8df7d2079d18be7d3d6159d10f4dffc396"},
"ecto_psql_extras": {:hex, :ecto_psql_extras, "0.7.14", "7a20cfe913b0476542b43870e67386461258734896035e3f284039fd18bd4c4c", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "22f5f98592dd597db9416fcef00effae0787669fdcb6faf447e982b553798e98"},
"ecto_sql": {:hex, :ecto_sql, "3.10.2", "6b98b46534b5c2f8b8b5f03f126e75e2a73c64f3c071149d32987a5378b0fdbd", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "68c018debca57cb9235e3889affdaec7a10616a4e3a80c99fa1d01fdafaa9007"},
"elixir_make": {:hex, :elixir_make, "0.7.7", "7128c60c2476019ed978210c245badf08b03dbec4f24d05790ef791da11aa17c", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "5bc19fff950fad52bbe5f211b12db9ec82c6b34a9647da0c2224b8b8464c7e6c"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
"erlsom": {:hex, :erlsom, "1.5.1", "c8fe2babd33ff0846403f6522328b8ab676f896b793634cfe7ef181c05316c03", [:rebar3], [], "hexpm", "7965485494c5844dd127656ac40f141aadfa174839ec1be1074e7edf5b4239eb"},
"ex_cmd": {:hex, :ex_cmd, "0.10.0", "f746150fea4421b49be9c773825f2e26f5f526e272515334135340ada2749fa6", [:mix], [{:gen_state_machine, "~> 3.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}], "hexpm", "d2575237e754676cd3d38dc39d36a99da455253a0889c1c2231a619d3ca5d7a4"},
"excoveralls": {:hex, :excoveralls, "0.17.1", "83fa7906ef23aa7fc8ad7ee469c357a63b1b3d55dd701ff5b9ce1f72442b2874", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "95bc6fda953e84c60f14da4a198880336205464e75383ec0f570180567985ae0"},
"excoveralls": {:hex, :excoveralls, "0.18.0", "b92497e69465dc51bc37a6422226ee690ab437e4c06877e836f1c18daeb35da9", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "1109bb911f3cb583401760be49c02cbbd16aed66ea9509fc5479335d284da60b"},
"exile": {:hex, :exile, "0.7.0", "a07228b191c7233f48e225289cc512dd268b54b5e799d952d03b6ee6db3b7ba5", [:make, :mix], [{:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f61ecfc4e0b2e9cc1ef177adf6b00a9bed2e8c2f4a2c2cfff0f3a28ae965720e"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"finch": {:hex, :finch, "0.16.0", "40733f02c89f94a112518071c0a91fe86069560f5dbdb39f9150042f44dcfb1a", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f660174c4d519e5fec629016054d60edd822cdfe2b7270836739ac2f97735ec5"},
"gen_smtp": {:hex, :gen_smtp, "1.2.0", "9cfc75c72a8821588b9b9fe947ae5ab2aed95a052b81237e0928633a13276fd3", [:rebar3], [{:ranch, ">= 1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "5ee0375680bca8f20c4d85f58c2894441443a743355430ff33a783fe03296779"},
"gen_state_machine": {:hex, :gen_state_machine, "3.0.0", "1e57f86a494e5c6b14137ebef26a7eb342b3b0070c7135f2d6768ed3f6b6cdff", [:mix], [], "hexpm", "0a59652574bebceb7309f6b749d2a41b45fdeda8dbb4da0791e355dd19f0ed15"},
"gettext": {:hex, :gettext, "0.20.0", "75ad71de05f2ef56991dbae224d35c68b098dd0e26918def5bb45591d5c8d429", [:mix], [], "hexpm", "1c03b177435e93a47441d7f681a7040bd2a816ece9e2666d1c9001035121eb3d"},
"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.19.1", "59de4716e985dd2b5cbd4954fa1ae187e2b610a9c4520ffcb0b1653c3d6e5559", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "8aa08234bdefc269995c63c2282cf3cd0e36febe3a6bfab11b610572fdd1cad0"},
"goth": {:hex, :goth, "1.4.2", "a598dfbce6fe65db3f5f43b1ab2ce8fbe3b2fe20a7569ad62d71c11c0ddc3f41", [:mix], [{:finch, "~> 0.9", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "d51bb6544dc551fe5754ab72e6cf194120b3c06d924282aaa3321a516ed3b98a"},
"hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"},
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
@ -63,12 +63,12 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.3.1", "2c54013ecf170e249e9291ed0a62e5832f70a476c61da16f6aac6dca0189f2af", [:mix], [], "hexpm", "2682e3c0b2eb58d90c6375fc0cc30bc7be06f365bf72608804fb9cffa5e1b167"},
"nimble_pool": {:hex, :nimble_pool, "1.0.0", "5eb82705d138f4dd4423f69ceb19ac667b3b492ae570c9f5c900bb3d2f50a847", [:mix], [], "hexpm", "80be3b882d2d351882256087078e1b1952a28bf98d0a287be87e4a24a710b67a"},
"oauth2": {:hex, :oauth2, "2.1.0", "beb657f393814a3a7a8a15bd5e5776ecae341fd344df425342a3b6f1904c2989", [:mix], [{:tesla, "~> 1.5", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "8ac07f85b3307dd1acfeb0ec852f64161b22f57d0ce0c15e616a1dfc8ebe2b41"},
"oban": {:hex, :oban, "2.16.1", "606242b4651c7a46747669ff5cfea4dc33bb7af8091ac44df93dedd8775b0d9e", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3dc78588bedaf7589c2619f9cee5c2e681a9af9e296ef0f9fc4671513243334b"},
"oban": {:hex, :oban, "2.16.2", "ec8dfd2f6dfdcd885061b58aeaa2794a0a6f62bad20c15939e4bb80bfd74ed76", [:mix], [{:ecto_sql, "~> 3.6", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3d343c80948676abf9652da1e793ab6140ba64e9de7c8d6630eb5bb4aa8fea79"},
"parallel_stream": {:hex, :parallel_stream, "1.1.0", "f52f73eb344bc22de335992377413138405796e0d0ad99d995d9977ac29f1ca9", [:mix], [], "hexpm", "684fd19191aedfaf387bbabbeb8ff3c752f0220c8112eb907d797f4592d6e871"},
"parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"},
"phoenix": {:hex, :phoenix, "1.7.7", "4cc501d4d823015007ba3cdd9c41ecaaf2ffb619d6fb283199fa8ddba89191e0", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "8966e15c395e5e37591b6ed0bd2ae7f48e961f0f60ac4c733f9566b519453085"},
"phoenix": {:hex, :phoenix, "1.7.9", "9a2b873e2cb3955efdd18ad050f1818af097fa3f5fc3a6aaba666da36bdd3f02", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.6", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2 or ~> 2.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:websock_adapter, "~> 0.5.3", [hex: :websock_adapter, repo: "hexpm", optional: false]}], "hexpm", "83e32da028272b4bfd076c61a964e6d2b9d988378df2f1276a0ed21b13b5e997"},
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.2", "b21bd01fdeffcfe2fab49e4942aa938b6d3e89e93a480d4aee58085560a0bc0d", [:mix], [{:ecto, "~> 3.5", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "70242edd4601d50b69273b057ecf7b684644c19ee750989fd555625ae4ce8f5d"},
"phoenix_html": {:hex, :phoenix_html, "3.3.2", "d6ce982c6d8247d2fc0defe625255c721fb8d5f1942c5ac051f6177bffa5973f", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "44adaf8e667c1c20fb9d284b6b0fa8dc7946ce29e81ce621860aa7e96de9a11d"},
"phoenix_html": {:hex, :phoenix_html, "3.3.3", "380b8fb45912b5638d2f1d925a3771b4516b9a78587249cabe394e0a5d579dc9", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "923ebe6fec6e2e3b3e569dfbdc6560de932cd54b000ada0208b5f45024bdd76c"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.4.1", "2aff698f5e47369decde4357ba91fc9c37c6487a512b41732818f2204a8ef1d3", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "9bffb834e7ddf08467fe54ae58b5785507aaba6255568ae22b4d46e2bb3615ab"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.20.0", "3f3531c835e46a3b45b4c3ca4a09cef7ba1d0f0d0035eef751c7084b8adb1299", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.15 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.3", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:phoenix_template, "~> 1.0", [hex: :phoenix_template, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 2.0", [hex: :phoenix_view, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "29875f8a58fb031f2dc8f3be025c92ed78d342b46f9bbf6dfe579549d7c81050"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.3", "3168d78ba41835aecad272d5e8cd51aa87a7ac9eb836eabc42f6e57538e3731d", [:mix], [], "hexpm", "bba06bc1dcfd8cb086759f0edc94a8ba2bc8896d5331a1e2c2902bf8e36ee502"},
@ -89,7 +89,7 @@
"scrivener_ecto": {:hex, :scrivener_ecto, "2.7.0", "cf64b8cb8a96cd131cdbcecf64e7fd395e21aaa1cb0236c42a7c2e34b0dca580", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:scrivener, "~> 2.4", [hex: :scrivener, repo: "hexpm", optional: false]}], "hexpm", "e809f171687806b0031129034352f5ae44849720c48dd839200adeaf0ac3e260"},
"sentry": {:hex, :sentry, "7.2.5", "570db92c3bbacd6ad02ac81cba8ac5af11235a55d65ac4375e3ec833975b83d3", [:mix], [{:hackney, "1.6.5 or ~> 1.8", [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.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
"styler": {:hex, :styler, "0.9.5", "6862ffa1d00adb4ec69cc4af60f72e17986c283e6287b778d2f0c3f50de01c8e", [:mix], [], "hexpm", "376674398da4126fbcbbc07ed51de30a5a45822b0d73e5f4d6e26d7942d54b7c"},
"styler": {:hex, :styler, "0.9.6", "749dd911ab5d99e80ebba29f3f63b1f1319c026e4b71dd88afb03bc10fe87791", [:mix], [], "hexpm", "70adc3f329f6ed8c1713748607c4ceea9a0ae3350c819394c68ed771ae3ee2ae"},
"table": {:hex, :table, "0.1.2", "87ad1125f5b70c5dea0307aa633194083eb5182ec537efc94e96af08937e14a8", [:mix], [], "hexpm", "7e99bc7efef806315c7e65640724bf165c3061cdc5d854060f74468367065029"},
"table_rex": {:hex, :table_rex, "3.1.1", "0c67164d1714b5e806d5067c1e96ff098ba7ae79413cc075973e17c38a587caa", [:mix], [], "hexpm", "678a23aba4d670419c23c17790f9dcd635a4a89022040df7d5d772cb21012490"},
"telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"},
@ -105,8 +105,8 @@
"ueberauth_discord": {:hex, :ueberauth_discord, "0.7.0", "463f6dfe1ed10a76739331ce8e1dd3600ab611f10524dd828eb3aa50e76e9d43", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "d6f98ef91abb4ddceada4b7acba470e0e68c4d2de9735ff2f24172a8e19896b4"},
"ueberauth_github": {:hex, :ueberauth_github, "0.8.3", "1c478629b4c1dae446c68834b69194ad5cead3b6c67c913db6fdf64f37f0328f", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "ae0ab2879c32cfa51d7287a48219b262bfdab0b7ec6629f24160564247493cc6"},
"ueberauth_gitlab_strategy": {:hex, :ueberauth_gitlab_strategy, "0.4.0", "96605d304ebb87ce508eccbeb1f94da9ea1c9da20d8913771b6cf24a6cc6c633", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "e86e2e794bb063c07c05a6b1301b73f2be3ba9308d8f47ecc4d510ef9226091e"},
"ueberauth_google": {:hex, :ueberauth_google, "0.11.0", "0689498c8bf9905cca304e2c6cfab26d5a3e4259f4dfe262a5a05534ec9b93a3", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "79ced73a06ce40e3d1b67ef4354d3383d1068783ac06b813288800332a6ac5a7"},
"ueberauth_microsoft": {:hex, :ueberauth_microsoft, "0.22.0", "25cb94b9493bff16be6b43e28b2a85dd123fe09f8bedb3c02299005496248752", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "f46660e56a7029cc982191df8c9e784dcb54a0dd108ac4f472132e7043095267"},
"ueberauth_google": {:hex, :ueberauth_google, "0.12.0", "e1aaaf5c0413a35623059cc177df4f755c5c8348a2f91a1e25a51f24c39b4c74", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.10.0", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "caa774ea8524ea6bb3b1233c4e2199c2443a5a2ca0f9b10c7729b6c1f8eb4905"},
"ueberauth_microsoft": {:hex, :ueberauth_microsoft, "0.23.0", "5c78e02a83d821ee45f96216bb6140ba688cc79b8b26e7ff438e3abe24615e1d", [:mix], [{:oauth2, "~> 1.0 or ~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "0c08d98203e6d3069f30306f09a6cb55b95c2bda94d6f8e90f05bd442ee96b82"},
"ueberauth_slack": {:hex, :ueberauth_slack, "0.7.0", "91dfd089371a6c5a21a505b3e3e140cced95d4cdc7b73afb5337bcf5f3c91a00", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:oauth2, "~> 1.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "5cba654352596f74a9e2547a19a3aab56634f2a0b928e93cd659ac7d05bf790e"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"vega_lite": {:hex, :vega_lite, "0.1.8", "7f6119126ecaf4bc2c1854084370d7091424f5cce4795fbac044eee9963f0752", [:mix], [{:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: false]}], "hexpm", "6c8a9271f850612dd8a90de8d1ebd433590ed07ffef76fc2397c240dc04d3fdc"},

0
priv/native/.gitkeep Normal file
View File

View File

@ -2,6 +2,9 @@ defmodule Accent.Repo.Migrations.AddTelemetryUiEventsTable do
@moduledoc false
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up do
TelemetryUI.Backend.EctoPostgres.Migrations.up()
end

View File

@ -2,6 +2,9 @@ defmodule Accent.Repo.Migrations.UpgradeTelemetryUi2 do
@moduledoc false
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up do
TelemetryUI.Backend.EctoPostgres.Migrations.up(version: 3)
end

View File

@ -48,7 +48,7 @@ defmodule AccentTest.GraphQL.Resolvers.Lint do
master_translation: master_translation,
conflicted: false,
key: "ok",
corrected_text: "bar foo",
corrected_text: " bar foo",
proposed_text: "bar"
})
@ -57,8 +57,8 @@ defmodule AccentTest.GraphQL.Resolvers.Lint do
assert result === [
%Message{
replacement: %Replacement{value: "bar foo", label: "bar foo"},
check: :double_spaces,
text: "bar foo"
check: :leading_spaces,
text: " bar foo"
}
]
end

View File

@ -62,19 +62,6 @@ defmodule AccentTest.Lint do
]
end
test "lint double spaces entry" do
entry = %Entry{key: "a", value: "fo o", master_value: "foo", value_type: "string"}
[{_, messages}] = Lint.lint([entry])
assert messages === [
%Message{
replacement: %Replacement{value: "fo o", label: "fo o"},
check: :double_spaces,
text: "fo o"
}
]
end
test "lint three dots ellipsis entry" do
entry = %Entry{key: "a", value: "foo...", master_value: "foo...", value_type: "string"}
[{_, messages}] = Lint.lint([entry])

View File

@ -0,0 +1,43 @@
defmodule LanguageTool do
@moduledoc false
def check(lang, text, opts \\ []) do
if lang in list_languages() do
metadata = %{language_code: lang, text_length: String.length(text)}
:telemetry.span(
[:accent, :language_tool, :check],
metadata,
fn ->
placeholder_regex = Keyword.get(opts, :placeholder_regex)
annotated_text = LanguageTool.AnnotatedText.build(text, placeholder_regex)
result =
GenServer.call(LanguageTool.Server, {:check, lang, Jason.encode!(%{items: annotated_text})}, :infinity)
{result, metadata}
end
)
else
empty_matches(lang, text)
end
catch
_ -> empty_matches(lang, text)
end
defp empty_matches(lang, text) do
%{"error" => nil, "language" => lang, "matches" => [], "text" => text, "markups" => []}
end
def available? do
LanguageTool.Server.available?()
end
def list_languages do
LanguageTool.Server.list_languages()
end
def ready? do
LanguageTool.Server.ready?()
end
end

View File

@ -0,0 +1,37 @@
defmodule LanguageTool.AnnotatedText do
@moduledoc false
def build(input, regex) do
matches = if regex, do: Regex.scan(regex, input, return: :index), else: []
matches = matches ++ Regex.scan(~r/<[^>]*>/, input, return: :index)
matches = Enum.sort_by(matches, fn [{match_index, _}] -> match_index end)
split_tokens(input, matches, 0, [])
end
defp split_tokens(input, [], position, acc) do
to_add =
case binary_slice(input, position..byte_size(input)) do
text_after when byte_size(text_after) > 1 ->
[%{text: text_after}]
_ ->
[]
end
acc ++ to_add
end
defp split_tokens(input, [[{start_index, match_length}] | matches], position, acc) do
text_before =
if position !== start_index do
case binary_slice(input, position..(start_index - 1)) do
"" -> []
text_before -> [%{text: String.trim_leading(text_before)}]
end
else
[]
end
markup = binary_slice(input, start_index, match_length)
split_tokens(input, matches, start_index + match_length, acc ++ text_before ++ [%{markup: markup}])
end
end

View File

@ -0,0 +1,97 @@
defmodule LanguageTool.Backend do
@moduledoc false
require Logger
def start(config) do
path = Application.app_dir(:accent, "priv/native")
args = [
executable(),
"-cp",
Path.join(path, "language-tool.jar"),
"com.mirego.accent.languagetool.AppKt",
"--languages",
Enum.join(config.languages, ",")
]
args =
if Enum.any?(config.disabled_rule_ids) do
args ++ ["--disabledRuleIds", Enum.join(config.disabled_rule_ids, ",")]
else
args
end
{:ok, backend} = Exile.Process.start_link(args)
receive_ready?(backend)
backend
end
def available? do
!!executable()
end
defp executable do
System.find_executable("java")
end
def check(process, lang, text) do
result =
text
|> String.split("\n")
|> Enum.map(&process_check(process, lang, &1))
|> Enum.reject(&is_nil/1)
|> Enum.reduce(
%{"offset" => 0, "language" => nil, "error" => nil, "text" => [], "matches" => [], "markups" => []},
fn result, acc ->
matches =
Enum.map(result["matches"], fn match ->
Map.update!(match, "offset", &(&1 + acc["offset"]))
end)
acc
|> Map.update!("markups", &(&1 ++ result["markups"]))
|> Map.update!("matches", &(&1 ++ matches))
|> Map.put("language", result["language"])
|> Map.put("error", result["error"])
|> Map.update!("offset", &(&1 + String.length(result["text"]) + 1))
|> Map.update!("text", &(&1 ++ [result["text"]]))
end
)
result
|> Map.delete("offset")
|> Map.update!("text", &Enum.join(&1, "\n"))
end
defp receive_ready?(backend) do
case Exile.Process.read(backend) do
{:ok, ">\n"} ->
Logger.info("LanguageTool is ready to spellcheck")
true
_ ->
receive_ready?(backend)
end
end
defp process_check(process, lang, text) do
lang = sanitize_lang(lang)
Exile.Process.write(process, IO.iodata_to_binary([String.pad_trailing(lang, 7), text, "\n"]))
with {:ok, data} <- Exile.Process.read(process),
{:ok, data} <- Jason.decode(data) do
data
else
_ -> nil
end
end
defp sanitize_lang(lang) do
if lang === "en" do
"en-US"
else
lang
end
end
end

View File

@ -0,0 +1,60 @@
defmodule LanguageTool.Server do
@moduledoc false
use GenServer
defmodule Config do
@moduledoc false
defstruct languages: [], disabled_rule_ids: []
def parse(opts) do
%__MODULE__{
languages: Keyword.fetch!(opts, :languages),
disabled_rule_ids: Keyword.get(opts, :disabled_rule_ids, [])
}
end
end
def init(opts) do
Process.send_after(self(), :init_server_process, 1)
{:ok, opts}
end
def start_link(opts) do
config = Config.parse(opts)
:persistent_term.put({:language_tool, :config}, config)
:persistent_term.put({:language_tool, :ready}, false)
GenServer.start_link(__MODULE__, %{config: config, backend: nil}, name: __MODULE__)
end
def list_languages do
:persistent_term.get({:language_tool, :config}).languages
end
def ready? do
:persistent_term.get({:language_tool, :ready})
end
def available? do
LanguageTool.Backend.available?()
end
def handle_call({:check, lang, text}, _, state) do
response = LanguageTool.Backend.check(state.backend, lang, text)
{:reply, response, state}
end
def handle_info(:init_server_process, state) do
backend = LanguageTool.Backend.start(state.config)
state = %{state | backend: backend}
:persistent_term.put({:language_tool, :ready}, true)
{:noreply, state}
end
def handle_info(_message, state) do
{:noreply, state}
end
end

View File

@ -0,0 +1,97 @@
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Kotlin application project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.4/userguide/building_java_projects.html in the Gradle documentation.
*/
buildscript {
repositories {
mavenCentral()
gradlePluginPortal()
}
dependencies {
classpath("gradle.plugin.com.github.johnrengelman:shadow:7.1.2")
}
}
plugins {
// Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin.
id("org.jetbrains.kotlin.jvm") version "1.9.10"
kotlin("plugin.serialization") version "1.9.10"
//java
// Apply the application plugin to add support for building a CLI application in Java.
application
}
apply(plugin = "com.github.johnrengelman.shadow")
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use the Kotlin JUnit 5 integration.
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
// Use the JUnit 5 integration.
testImplementation("org.junit.jupiter:junit-jupiter-engine:5.9.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// This dependency is used by the application.
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.0")
implementation("com.google.guava:guava:32.1.1-jre")
implementation("org.languagetool:language-all:6.3")
implementation("com.googlecode.json-simple:json-simple:1.1.1")
implementation("org.slf4j:slf4j-nop:2.0.7")
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(11))
}
}
tasks.withType<ShadowJar> {
archiveBaseName.set("language-tool")
archiveClassifier.set("")
archiveVersion.set("")
mergeServiceFiles("META-INF/org/languagetool/language-module.properties")
}
tasks.withType<Jar> {
manifest {
attributes["Main-Class"] = "com.mirego.accent.languagetool.AppKt"
}
}
tasks.register<Jar>("customFatJar") {
manifest {
attributes["Main-Class"] = "com.baeldung.fatjar.Application"
}
//baseName = "all-in-one-jar"
duplicatesStrategy = DuplicatesStrategy.EXCLUDE
from(configurations.runtimeClasspath.get().map {
if (it.isDirectory) {
it
} else {
zipTree(it)
}
})
with(tasks.jar.get())
}
application {
// Define the main class for the application.
mainClass.set("com.mirego.accent.languagetool.AppKt")
}
tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@ -0,0 +1,178 @@
package com.mirego.accent.languagetool
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.util.*
import java.util.concurrent.*
import org.json.simple.*
import org.languagetool.*
import org.languagetool.language.*
import org.languagetool.rules.*
import org.languagetool.markup.AnnotatedTextBuilder;
import kotlinx.serialization.*
import kotlinx.serialization.json.*
@Serializable
data class Base(val items: Array<Item>)
@Serializable
data class Item(val markup: String = "", val text: String = "")
fun main(args: Array<String>) {
val reader = BufferedReader(InputStreamReader(System.`in`))
val tools = HashMap<String, JLanguageTool>()
val languages = ArrayList<String>()
val disabledRuleIds = ArrayList<String>()
for (i in args.indices) {
when (args[i]) {
"--languages" -> {
if (i + 1 < args.size) {
val codes = args[i + 1].split(",")
for (code in codes) {
languages.add(code.trim())
}
} else {
println("Error: Missing languages.")
return
}
}
"--disabledRuleIds" -> {
if (i + 1 < args.size) {
val ids = args[i + 1].split(",")
for (id in ids) {
disabledRuleIds.add(id)
}
} else {
println("Error: Missing rule ids.")
return
}
}
}
}
for (code in languages) {
val cache = ResultCache(10000, 600, TimeUnit.SECONDS)
val globalConfig = GlobalConfig()
val userConfig = UserConfig()
val lt =
JLanguageTool(
Languages.getLanguageForShortCode(code),
ArrayList(),
null,
cache,
globalConfig,
userConfig
)
for (id in disabledRuleIds) {
lt.disableRule(id)
}
lt.check("")
tools[code] = lt
}
var input: String?
println(">")
while (true) {
input = reader.readLine()
if (input == null) break
val languageShortCode = input.substring(0, Math.min(7, input.length)).trim()
val text = input.substring(Math.min(7, input.length))
val langTool = tools[languageShortCode]
if (text.length == 0) {
printError("invalid_input", text, languageShortCode)
continue
}
if (langTool == null) {
printError("unsupported_language", text, languageShortCode)
continue
}
val parsedText = Json.decodeFromString<Base>(text)
val annotatedBuilder = AnnotatedTextBuilder()
val markups = JSONArray()
for (item in parsedText.items) {
if (item.markup != "") {
markups.add(item.markup)
annotatedBuilder.addMarkup(item.markup, "x");
} else {
annotatedBuilder.addText(item.text);
}
}
val annotatedText = annotatedBuilder.build()
val matches = langTool.check(annotatedText)
val responseObject = JSONObject()
responseObject.put("text", annotatedText.getTextWithMarkup())
responseObject.put("markups", markups)
responseObject.put("language", languageShortCode)
val matchesList = JSONArray()
for (match in matches) {
val matchObject = JSONObject()
matchObject.put("offset", match.fromPos)
matchObject.put("message", cleanSuggestion(match.message))
matchObject.put("length", match.toPos - match.fromPos)
matchObject.put("replacements", getReplacements(match))
matchObject.put("rule", getRule(match))
matchesList.add(matchObject)
}
responseObject.put("matches", matchesList)
println(responseObject.toString())
}
}
@Throws(IOException::class)
private fun printError(error: String, text: String, languageShortCode: String) {
val errorObject = JSONObject()
errorObject.put("error", error)
errorObject.put("text", text)
errorObject.put("matches", JSONArray())
errorObject.put("markups", JSONArray())
errorObject.put("language", languageShortCode)
println(errorObject.toString())
}
@Throws(IOException::class)
private fun getRule(match: RuleMatch): JSONObject {
val rule = match.rule
val ruleObject = JSONObject()
ruleObject.put("description", rule.description)
ruleObject.put("id", match.specificRuleId)
return ruleObject
}
@Throws(IOException::class)
private fun getReplacements(match: RuleMatch): JSONArray {
val replacements = JSONArray()
val matches = match.suggestedReplacementObjects
for (replacement in matches.subList(0, Math.min(5, Math.max(0, matches.size - 1)))) {
val replacementObject = JSONObject()
replacementObject.put("value", replacement.replacement)
replacementObject.put("confidence", replacement.confidence)
replacements.add(replacementObject)
}
return replacements
}
private fun cleanSuggestion(s: String): String {
return s.replace("<suggestion>", "\"").replace("</suggestion>", "\"")
}

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1,249 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -0,0 +1,92 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -0,0 +1,14 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.4/userguide/building_swift_projects.html in the Gradle documentation.
*/
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id("org.gradle.toolchains.foojay-resolver-convention") version "0.7.0"
}
rootProject.name = "languagetool"
include("app")

View File

@ -41,7 +41,7 @@
},
"revision_selector": {
"languages_count": "{count, plural, =1 {1 other language} other {# other languages}}",
"master": "master"
"master": "main"
},
"translation_comments_subscriptions": {
"title": "Notify on new messages"
@ -115,7 +115,7 @@
"instructions": {
"sync": {
"title": "Sync",
"text": "Update the list of strings in Accent, removing ones missing in the file and adding new ones.<em>The sync is always done in the master language.</em>New strings and removed strings changes will be reflected in all translated languages."
"text": "Update the list of strings in Accent, removing ones missing in the file and adding new ones.<em>The sync is always done in the main language.</em>New strings and removed strings changes will be reflected in all translated languages."
},
"merge": {
"title": "Add translations",
@ -124,8 +124,8 @@
"mistakes": {
"title": "Common mistakes",
"item_1": "Export Accent files locally before a sync. This will force you to resolve conflicts locally and potentially override reviewed texts or adding removed strings.",
"item_2": "Sync a translated language (not your master language). This will effectively generates conflicts for all your keys, changing the text of the master language for your translated language.",
"item_3": "Sync one file per language. The purpose of the \"file\" in Accent is to be translated in X languages. Sync your master language and let the translated languages be translated in the UI."
"item_2": "Sync a translated language (not your main language). This will effectively generates conflicts for all your keys, changing the text of the main language for your translated language.",
"item_3": "Sync one file per language. The purpose of the \"file\" in Accent is to be translated in X languages. Sync your main language and let the translated languages be translated in the UI."
}
}
},
@ -178,7 +178,7 @@
},
"keys": "strings",
"last_synced_at_label": "Last sync:",
"master_language_label": "Master language is:",
"master_language_label": "Main language is:",
"never_synced": "Sync the project for the first time",
"reviewed": "reviewed"
},
@ -191,10 +191,10 @@
"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.",
"new_language_link_text": "With another language, you can keep track of translation based on the main language.",
"view_more_activities": "View more activities →",
"title": "Dashboard",
"master": "Master language",
"master": "Main language",
"slaves": "Translations",
"sync": "Sync",
"strings": "strings",
@ -216,7 +216,7 @@
"update": "Edit"
},
"documents_list": {
"empty_text": "Files are the actual listing of your projects master language files. Add them here or from the CLI and keep them in sync with your project.",
"empty_text": "Files are the actual listing of your projects main language files. Add them here or from the CLI and keep them in sync with your project.",
"export_all": "Export all",
"show_deleted": "Show deleted",
"hide_deleted": "Hide deleted",
@ -306,7 +306,7 @@
"create_version": "When a user freeze the state of all strings to tag it with a version. Used to maintain multiple versions of the same app in parallel.",
"conflict_on_corrected": "When the uploaded text is different than the last synced text and the last synced text is different than the current text. This happens if the text has been touched by a user in Accent. It is uselful to identify a conflict that is caused by a difference in a sync and the modified version in Accent.",
"conflict_on_proposed": "When the uploaded text is different than the last uploaded text and the last synced text is equal to the current text. This happens if the text has not been touched by a user in Accent. It is uselful to identify a conflict that is only caused by a sync and not a human intervention.",
"conflict_on_slave": "When the activity \"conflict on proposed\" or \"conflict on corrected\" happens on a string, the matching keys in other languages apply this activity. The goal of this activity is to flag a change of meaning in the master language in the translations. The string will be flagged as \"in review\"",
"conflict_on_slave": "When the activity \"conflict on proposed\" or \"conflict on corrected\" happens on a string, the matching keys in other languages apply this activity. The goal of this activity is to flag a change of meaning in the main language in the translations. The string will be flagged as \"in review\"",
"correct_all": "When all the strings are manually marked as reviewed.",
"correct_conflict": "When a string is manually marked as reviewed.",
"batch_correct_conflict": "When multiple strings were marked as reviewed in a short lapse of time.",
@ -610,7 +610,7 @@
"title": "New project",
"error": "Invalid project",
"cancel_button": "Cancel",
"language_label": "Master language:",
"language_label": "Main language:",
"language_search_placeholder": "Search…",
"name_label": "Name:",
"save_button": "Create"
@ -700,12 +700,12 @@
"project_manage_languages": {
"create_error": "Language can not be added right now. Try again later.",
"conflicts_explain_title": "On conflicts",
"conflicts_explain_text": "Every string addition or deletion will be reflected in the language. When a text changes in the master revision, the string in the language will be marked as in review.",
"conflicts_explain_text": "Every string addition or deletion will be reflected in the language. When a text changes in the main revision, the string in the language will be marked as in review.",
"sync_explain_title": "On sync",
"sync_explain_text": "The master language will be the default source when syncing a file. The other languages are never \"synced\", they just follow the master language.",
"sync_explain_text": "The main language will be the default source when syncing a file. The other languages are never \"synced\", they just follow the main language.",
"add_translations_explain_title": "On add translations",
"add_translations_explain_text": "Every languages can have strings \"merged\" into it by adding translations. Conflict resolution will work the same but will never add or remove strings.",
"main_text": "You can add a new language that will follow the master language.",
"main_text": "You can add a new language that will follow the main language.",
"title": "Manage languages"
},
"projects_filters": {
@ -727,7 +727,7 @@
"related_translations_list": {
"comments_label": "{count, plural, =0 {No comments} =1 {1 comment} other {# comments}}",
"conflicted_label": "in review",
"master_label": "master",
"master_label": "main",
"save_button": "Save",
"last_updated_label": "Last updated:",
"new_language_link": "New language",
@ -746,11 +746,11 @@
"list_languages": "Languages:",
"revision_inserted_at_label": "Created",
"revision_deleted_label": "The language is currently being removed from your project. It will automatically disappear once all operations are propagated in the system.",
"master_badge": "master",
"master_badge": "main",
"delete_revision_confirm": "Are you sure you want to remove this language from your project? This action cannot be rollbacked.",
"delete_revision_button": "Remove this language",
"promote_revision_master_confirm": "Are you sure you want to use this language as the master language from your project?",
"promote_revision_master_button": "Use as master"
"promote_revision_master_confirm": "Are you sure you want to use this language as the main language from your project?",
"promote_revision_master_button": "Use as main"
},
"revision_export_options": {
"default_format": "Default format",
@ -858,15 +858,16 @@
"URL_COUNT": "URLs count"
},
"checks": {
"PLACEHOLDER_COUNT": "Number of placeholders does not match the master string",
"PLACEHOLDER_COUNT": "Number of placeholders does not match the main string",
"TRAILING_SPACE": "String contains a trailing space",
"LEADING_SPACES": "String contains leading spaces",
"DOUBLE_SPACES": "String contains double spaces",
"APOSTROPHE_AS_SINGLE_QUOTE": "A single quote as been used instead of an apostrophe",
"FIRST_LETTER_CASE": "First letter of translation does not match case of the masters",
"SPELLING": "String contains a spelling mistake",
"FIRST_LETTER_CASE": "First letter of translation does not match case of the mains",
"THREE_DOTS_ELLIPSIS": "String contains three dots instead of ellipsis",
"SAME_TRAILING_CHARACTER": "String does not match the trailing character of master",
"URL_COUNT": "Number of URL does not match the master string"
"SAME_TRAILING_CHARACTER": "String does not match the trailing character of main",
"URL_COUNT": "Number of URL does not match the main string"
}
}
},
@ -880,7 +881,7 @@
"translation_splash_title": {
"plural_label": "plural",
"conflicted_label": "in review",
"master_label": "master",
"master_label": "main",
"last_updated_label": "Last updated:",
"removed_label": "This string was removed {removedAt}"
},
@ -923,7 +924,7 @@
"sync_file": "Sync a new file",
"sync_file_text": "Add strings from multiple file formats to review them",
"manage_languages": "Add languages",
"manage_languages_text": "Target languages follow your master language strings and conflicts",
"manage_languages_text": "Target languages follow your main language strings and conflicts",
"add_collaborator": "Add collaborator",
"add_collaborator_text": "Translators, developers, etc.",
"api_token": "Get an API Token",
@ -1131,8 +1132,8 @@
"add_revision_success": "The new language has been created with success",
"delete_revision_failure": "The language could not be deleted",
"delete_revision_success": "The language has been deleted with success",
"promote_master_revision_failure": "The language could not be promoted as master",
"promote_master_revision_success": "The language has been promoted as master with success"
"promote_master_revision_failure": "The language could not be promoted as main",
"promote_master_revision_success": "The language has been promoted as main with success"
}
},
"comments": {

View File

@ -854,6 +854,7 @@
"FIRST_LETTER_CASE": "La casse de la première lettre ne concorde pas",
"THREE_DOTS_ELLIPSIS": "Trois points pour des points de suspension",
"SAME_TRAILING_CHARACTER": "Le caractère de fin ne concorde pas",
"SPELLING": "Ortographe",
"URL_COUNT": "Le nombre dURL ne concorde pas"
},
"checks": {
@ -865,6 +866,7 @@
"FIRST_LETTER_CASE": "La première lettre de traduction ne correspond pas à la casse de la langue principal",
"THREE_DOTS_ELLIPSIS": "La chaîne contient trois points au lieu de points de suspension",
"SAME_TRAILING_CHARACTER": "La chaîne ne correspond pas au caractère de fin de la langue principal",
"SPELLING": "La chaîne contient une faute dortographe",
"URL_COUNT": "Le cnombre dURL ne correspond pas à la chaîne principale"
}
}

View File

@ -6,8 +6,6 @@ import {dropTask} from 'ember-concurrency-decorators';
import {tracked} from '@glimmer/tracking';
import {MutationResponse} from 'accent-webapp/services/apollo-mutate';
const ALWAYS_SHOWN_COUNT = 3;
interface Conflict {
id: string;
key: string;
@ -66,9 +64,6 @@ export default class ConflictItem extends Component<Args> {
@tracked
inputDisabled = false;
@tracked
show = this.args.index <= ALWAYS_SHOWN_COUNT;
conflictKey = parsedKeyProperty(this.args.conflict.key);
textOriginal = this.args.conflict.correctedText;
@ -116,11 +111,6 @@ export default class ConflictItem extends Component<Args> {
: this.args.conflict.revision.language.rtl;
}
@action
didEnterViewport() {
this.show = true;
}
@action
changeTranslationText(text: string) {
this.textInput = text;

View File

@ -48,8 +48,7 @@
justify-content: center;
margin-left: 3px;
border-radius: 50%;
width: 24px;
height: 24px;
height: 17px;
background: transparent;
color: var(--color-black);
line-height: 1;

View File

@ -1,9 +1,32 @@
<div local-class='root' {{in-viewport onEnter=(fn this.didEnterViewport) scrollableArea='.conflict-items'}}>
{{#if this.show}}
<div local-class='conflict-item {{if this.resolved "resolved"}} {{if this.error "errored"}}'>
<li>
{{#if this.resolved}}
<div local-class='textResolved'>
<div local-class='root'>
<div local-class='conflict-item {{if this.resolved "resolved"}} {{if this.error "errored"}}'>
<li>
{{#if this.resolved}}
<div local-class='textResolved'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @conflict.id}} local-class='key'>
<strong local-class='item-key'>
{{this.conflictKey.value}}
<small local-class='item-key-prefix'>
{{#if this.conflictKey.prefix}}
{{this.conflictKey.prefix}}
{{else}}
{{@conflict.document.path}}
{{/if}}
</small>
</strong>
</LinkTo>
<div local-class='textResolved-content'>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.uncorrect_error_text'}}
</div>
{{/if}}
</div>
</div>
{{else}}
<div local-class='item-details'>
<div local-class='item-details__column'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @conflict.id}} local-class='key'>
<strong local-class='item-key'>
{{this.conflictKey.value}}
@ -17,106 +40,81 @@
</strong>
</LinkTo>
<div local-class='textResolved-content'>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.uncorrect_error_text'}}
</div>
{{/if}}
</div>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.correct_error_text'}}
</div>
{{/if}}
</div>
{{else}}
<div local-class='item-details'>
<div local-class='item-details__column'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @conflict.id}} local-class='key'>
<strong local-class='item-key'>
{{this.conflictKey.value}}
<small local-class='item-key-prefix'>
{{#if this.conflictKey.prefix}}
{{this.conflictKey.prefix}}
{{else}}
{{@conflict.document.path}}
<div local-class='item-details__column'>
<div local-class='textInput'>
<TranslationEdit::Form
@projectId={{@project.id}}
@translationId={{@conflict.id}}
@lintMessages={{@conflict.lintMessages}}
@valueType={{@conflict.valueType}}
@value={{this.textInput}}
@inputDisabled={{this.inputDisabled}}
@showTypeHints={{false}}
@onKeyUp={{fn this.changeTranslationText}}
@onSubmit={{fn this.correct}}
@rtl={{this.revisionTextDirRtl}}
lang={{this.revisionSlug}}
as |form|
>
{{#component form.submit}}
<div local-class='button-submit' data-dir={{form.dir}}>
{{#if this.showOriginalButton}}
<AsyncButton @onClick={{fn this.setOriginalText}} local-class='revert-button' class='button button--iconOnly button--white'>
{{inline-svg '/assets/revert.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
</small>
</strong>
</LinkTo>
{{#if this.error}}
<div local-class='error'>
{{t 'components.conflict_item.correct_error_text'}}
{{#if (get @permissions 'use_prompt_improve_text')}}
<ImprovePrompt
@project={{@project}}
@prompts={{@prompts}}
@text={{this.textInput}}
@onUpdatingText={{fn this.onImprovingPrompt}}
@onUpdateText={{fn this.onImprovePrompt}}
/>
{{/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='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>
<div local-class='conflictedText-references-conflicted-value'>{{string-diff this.textInput @conflict.conflictedText}}</div>
</div>
{{/if}}
</div>
<div local-class='item-details__column'>
<div local-class='textInput'>
<TranslationEdit::Form
@projectId={{@project.id}}
@translationId={{@conflict.id}}
@lintMessages={{@conflict.lintMessages}}
@valueType={{@conflict.valueType}}
@value={{this.textInput}}
@inputDisabled={{this.inputDisabled}}
@showTypeHints={{false}}
@onKeyUp={{fn this.changeTranslationText}}
@onSubmit={{fn this.correct}}
@rtl={{this.revisionTextDirRtl}}
lang={{this.revisionSlug}}
as |form|
>
{{#component form.submit}}
<div local-class='button-submit' data-dir={{form.dir}}>
{{#if this.showOriginalButton}}
<AsyncButton @onClick={{fn this.setOriginalText}} local-class='revert-button' class='button button--iconOnly button--white'>
{{inline-svg '/assets/revert.svg' class='button-icon'}}
</AsyncButton>
{{/if}}
{{#if (get @permissions 'use_prompt_improve_text')}}
<ImprovePrompt
@project={{@project}}
@prompts={{@prompts}}
@text={{this.textInput}}
@onUpdatingText={{fn this.onImprovingPrompt}}
@onUpdateText={{fn this.onImprovePrompt}}
/>
{{/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='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>
<div local-class='conflictedText-references-conflicted-value'>{{string-diff this.textInput @conflict.conflictedText}}</div>
</div>
{{/if}}
{{#if @conflict.relatedTranslations}}
{{#each this.relatedTranslations key='id' as |relatedTranslation|}}
<ConflictsList::Item::RelatedTranslation
@project={{@project}}
@translation={{relatedTranslation}}
@permissions={{@permissions}}
@onCopyTranslation={{perform this.copyTranslationTask}}
/>
{{/each}}
{{/if}}
</div>
{{#if @conflict.relatedTranslations}}
{{#each this.relatedTranslations key='id' as |relatedTranslation|}}
<ConflictsList::Item::RelatedTranslation
@project={{@project}}
@translation={{relatedTranslation}}
@permissions={{@permissions}}
@onCopyTranslation={{perform this.copyTranslationTask}}
/>
{{/each}}
{{/if}}
</div>
</div>
{{/if}}
</li>
</div>
{{/if}}
</div>
{{/if}}
</li>
</div>
</div>

View File

@ -1,4 +1,4 @@
<ul local-class='conflicts-items' class='conflict-items'>
<ul local-class='conflicts-items'>
{{#each @conflicts key='id' as |conflict index|}}
<ConflictsList::Item
@index={{index}}

View File

@ -262,10 +262,6 @@
padding: 2px 6px !important;
}
:global(.button).button-sync {
color: var(--color-primary);
}
@media (max-width: (1300px)) {
.links {
transform: translate3d(0, 0, 0);

View File

@ -87,7 +87,7 @@
@models={{array @project.id @document.id}}
local-class='button-sync'
title={{t 'components.documents_list.sync'}}
class='tooltip tooltip--top button button--filled button--white button--iconOnly'
class='tooltip tooltip--top button button--filled button--iconOnly'
>
{{inline-svg '/assets/sync.svg' class='button-icon'}}
</LinkTo>

View File

@ -9,23 +9,54 @@ interface Args {
export default class LintTranslationsPageItem extends Component<Args> {
translationKey = parsedKeyProperty(this.args.lintTranslation.translation.key);
get messages() {
const mapSet = new Set();
return this.args.lintTranslation.messages.flatMap((message: any) => {
if (mapSet.has(message.check)) {
return [];
} else {
mapSet.add(message.check);
return [message];
}
});
}
get annotatedText() {
return this.args.lintTranslation.messages.reduce(
(text: string, message: any) => {
if (message.offset && message.length && message.replacement) {
let offsetTotal = 0;
return this.args.lintTranslation.messages
.sort((a: any, b: any) => a.offset || 0 >= b.offset || 0)
.reduce((text: string, message: any) => {
if (message.length) {
const error = text.slice(
message.offset,
message.offset + message.length
);
return String(text).replace(
error,
`<span>${error}</span><strong>${message.replacement.label}</strong>`
message.offset + offsetTotal,
message.offset + message.length + offsetTotal
);
if (message.replacement) {
const replacement = `<span data-underline>${error}</span><strong>${message.replacement.label}</strong>`;
offsetTotal += replacement.length - error.length;
return String(text).replace(error, replacement);
} else {
const replacement = `<span data-underline>${error}</span>`;
offsetTotal += replacement.length - error.length;
return String(text).replace(error, replacement);
}
} else if (message.check === 'LEADING_SPACES') {
const replacement = `<span data-rect> </span>`;
offsetTotal += replacement.length - 1;
return String(text).replace(/^ /, replacement);
} else if (message.check === 'TRAILING_SPACE') {
const replacement = `<span data-rect> </span>`;
offsetTotal += replacement.length - 1;
return String(text).replace(/ $/, replacement);
} else {
return text;
}
},
this.args.lintTranslation.translation.text
);
}, this.args.lintTranslation.messages[0].text);
}
}

View File

@ -4,11 +4,6 @@
margin-bottom: 20px;
}
.details,
.messages {
width: 50%;
}
.details {
padding-right: 25px;
}
@ -54,6 +49,7 @@
display: block;
color: #959595;
font-weight: 300;
flex-shrink: 0;
&::before {
content: '/';
@ -79,7 +75,7 @@
display: block;
width: 100%;
font-size: 13px;
color: var(--color-black);
color: var(--text-color-normal);
padding: 0;
cursor: text;
white-space: pre-wrap;
@ -102,23 +98,23 @@
.item-text {
strong {
color: var(--color-green);
font-weight: normal;
margin-left: 3px;
font-weight: bold;
}
span {
span[data-underline] {
position: relative;
color: var(--color-error);
margin-right: 4px;
text-decoration-line: underline;
text-decoration-style: wavy;
text-decoration-color: var(--color-error);
text-decoration-skip-ink: none;
text-decoration-thickness: 1px;
}
span::after {
content: '';
width: 100%;
height: 1px;
background: var(--color-error);
display: inline-block;
position: absolute;
top: 50%;
left: 0;
span[data-rect] {
position: relative;
background-color: var(--color-error);
padding: 0 2px;
opacity: 0.4;
}
}

View File

@ -1,30 +1,34 @@
<div local-class='wrapper'>
<ul local-class='messages'>
{{#each @lintTranslation.messages as |message|}}
<span local-class='description'>
{{#if message.message}}
{{message.message}}
{{else}}
{{t (concat 'components.translation_edit.lint_message.checks.' message.check)}}
{{/if}}
</span>
{{/each}}
</ul>
<div local-class='details'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @lintTranslation.translation.id}} local-class='item-link'>
<strong local-class='item-key'>
{{this.translationKey.value}}
<small local-class='item-key-prefix'>
{{#if this.translationKey.prefix}}
{{this.translationKey.prefix}}
{{#if @lintTranslation.messages}}
<div local-class='wrapper'>
<ul local-class='messages'>
{{#each this.messages as |message|}}
<span local-class='description'>
{{#if message.message}}
{{message.message}}
{{else}}
{{@lintTranslation.translation.document.path}}
{{t (concat 'components.translation_edit.lint_message.checks.' message.check)}}
{{/if}}
</small>
</strong>
</LinkTo>
</div>
</span>
{{/each}}
</ul>
<div local-class='item-text'>{{{this.annotatedText}}}</div>
</div>
{{#if @project}}
<div local-class='details'>
<LinkTo @route='logged-in.project.translation' @models={{array @project.id @lintTranslation.translation.id}} local-class='item-link'>
<strong local-class='item-key'>
{{this.translationKey.value}}
<small local-class='item-key-prefix'>
{{#if this.translationKey.prefix}}
{{this.translationKey.prefix}}
{{else}}
{{@lintTranslation.translation.document.path}}
{{/if}}
</small>
</strong>
</LinkTo>
</div>
{{/if}}
<div local-class='item-text'>{{{this.annotatedText}}}</div>
</div>
{{/if}}

View File

@ -13,7 +13,7 @@
h1 {
font-size: 35px;
font-weight: 900;
letter-spacing: 0.1rem;
letter-spacing: -0.4px;
}
> svg {

View File

@ -8,7 +8,7 @@
.section {
display: flex;
flex-direction: column;
margin: 10px 0;
margin: 6px 0;
padding-bottom: 4px;
}
@ -41,7 +41,7 @@
padding: 5px 12px 4px;
text-decoration: none;
font-weight: 600;
font-size: 13px;
font-size: 14px;
border-radius: var(--border-radius);
margin-bottom: 3px;
color: var(--text-color-normal);
@ -74,9 +74,9 @@
}
.list-item-link-text {
width: 127px;
padding: 0 0 0 10px;
padding: 0 0 0 7px;
opacity: 0.9;
letter-spacing: -0.4px;
}
.list-item-link-icon {

View File

@ -40,7 +40,7 @@
}
.project {
padding: 20px 0 20px 20px;
padding: 20px 0 20px 23px;
margin-left: 0;
position: relative;
display: flex;

View File

@ -24,7 +24,7 @@
font-weight: 600;
font-size: 13px;
transition: 0.2s ease-in-out;
transition-property: background, box-shadow, color;
transition-property: background, box-shadow, color, transform;
strong {
padding: 10px 20px 5px;
@ -47,6 +47,7 @@
&:hover {
color: var(--color-primary);
box-shadow: 0 3px 8px var(--shadow-color), 0 9px 12px var(--shadow-color);
transform: scale(1.02);
.link-check {
color: var(--color-primary);

View File

@ -49,7 +49,10 @@ export default class TranslationEditForm extends Component<Args> {
apollo: Apollo;
@tracked
lintMessages = this.args.lintMessages;
lintTranslation = {
translation: {id: this.args.translationId, text: this.args.value},
messages: this.args.lintMessages,
};
@tracked
showTypeHints = true;
@ -137,7 +140,9 @@ export default class TranslationEditForm extends Component<Args> {
},
});
this.lintMessages = data.viewer.project.translation.lintMessages;
this.lintTranslation = Object.assign(this.lintTranslation, {
messages: data.viewer.project.translation.lintMessages,
});
}
@action

View File

@ -113,7 +113,5 @@
</div>
{{/if}}
{{#each this.lintMessages as |message|}}
<TranslationEdit::LintMessage @message={{message}} @onReplaceText={{fn this.replaceText}} />
{{/each}}
<LintTranslationsPage::Item @lintTranslation={{this.lintTranslation}} />
</div>

View File

@ -1,25 +0,0 @@
import {inject as service} from '@ember/service';
import IntlService from 'ember-intl/services/intl';
import Component from '@glimmer/component';
import {action} from '@ember/object';
interface Args {
message: any;
onReplaceText?: (value: string) => void;
}
export default class LintMessage extends Component<Args> {
@service('intl')
intl: IntlService;
@action
replaceText() {
this.args.onReplaceText?.(this.args.message.replacement.value);
}
get description() {
return this.intl.t(
`components.translation_edit.lint_message.checks.${this.args.message.check}`
);
}
}

View File

@ -1,38 +0,0 @@
.translation-edit-lint-message {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
padding: 4px 0 0;
margin-bottom: 5px;
font-size: 13px;
}
.description {
margin-top: 2px;
color: var(--color-error);
}
.text {
padding: 0 4px;
color: var(--color-error);
font-weight: bold;
}
.arrow {
margin-left: 5px;
color: var(--color-grey);
}
.replacement {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.replacement-text {
color: var(--text-color-normal);
font-size: 12px;
font-weight: normal;
}

View File

@ -1,24 +0,0 @@
<div local-class='translation-edit-lint-message'>
<span local-class='description'>
{{#if @message.message}}
{{@message.message}}
{{else}}
{{this.description}}
{{/if}}
</span>
{{#if @message.replacement}}
<div local-class='replacement'>
<span local-class='replacement-text'>
{{truncate @message.replacement.value 500}}
</span>
{{#if @onReplaceText}}
<button class='button button--small button--filled button--white' {{on 'click' this.replaceText}}>
{{t 'components.translation_edit.lint_message.replace'}}
</button>
{{/if}}
</div>
{{/if}}
</div>

View File

@ -3,8 +3,8 @@
}
.language {
display: block;
margin-bottom: 6px;
display: inline-block;
margin-bottom: 4px;
color: var(--color-black);
font-size: 14px;
text-decoration: none;

View File

@ -2,8 +2,8 @@
transition: 0.2s ease-in-out;
transition-property: background, transform;
display: block;
margin: 4px 0 0;
padding: 10px 10px 4px;
margin: 0;
padding: 4px 10px;
border: 1px solid transparent;
border-radius: var(--border-radius);
@ -15,6 +15,11 @@
transform: translateX(-40px);
opacity: 1;
}
.item-updatedAt {
opacity: 1;
transform: translateX(0);
}
}
&.item--editMode {
@ -82,6 +87,7 @@
display: block;
font-size: 11px;
color: #959595;
flex-shrink: 0;
font-weight: 300;
&::before {
@ -96,12 +102,12 @@
gap: 2px;
transition: 0.2s ease-in-out;
transition-property: color;
margin-right: 15px;
margin-right: 10px;
color: var(--color-primary);
line-height: 1.5;
word-break: break-all;
font-family: var(--font-monospace);
font-size: 12px;
font-size: 11px;
font-weight: bold;
}
@ -113,7 +119,7 @@
.item-text {
display: block;
width: 100%;
font-size: 12px;
font-size: 13px;
color: var(--color-black);
padding: 2px 0;
cursor: text;
@ -140,9 +146,13 @@
}
.item-updatedAt {
opacity: 0;
transform: translateX(-10px);
margin-left: 5px;
color: var(--color-grey);
font-size: 11px;
transition: 0.2s ease-in-out;
transition-property: opacity, transform;
}
.item-textEdit {

View File

@ -7,6 +7,9 @@ export default gql`
id
translation(id: $translationId) {
id
key
text: correctedText
lintMessages(text: $text) {
text
message

View File

@ -27,6 +27,8 @@ export default gql`
lintMessages {
text
check
offset
length
message
replacement {
value

View File

@ -1,7 +1,7 @@
import Service, { inject as service } from '@ember/service';
import Service, {inject as service} from '@ember/service';
import RouterService from '@ember/routing/router-service';
import { ApolloClient } from 'apollo-client';
import { BatchHttpLink } from 'apollo-link-batch-http';
import {ApolloClient} from 'apollo-client';
import {BatchHttpLink} from 'apollo-link-batch-http';
import {
IntrospectionFragmentMatcher,
InMemoryCache,
@ -11,7 +11,7 @@ import {
import Session from 'accent-webapp/services/session';
const dataIdFromObject = (result: { id?: string; __typename: string }) => {
const dataIdFromObject = (result: {id?: string; __typename: string}) => {
if (result.id && result.__typename) return `${result.__typename}${result.id}`;
return null;
@ -25,9 +25,9 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
kind: 'INTERFACE',
name: 'ProjectIntegration',
possibleTypes: [
{ name: 'ProjectIntegrationDiscord' },
{ name: 'ProjectIntegrationSlack' },
{ name: 'ProjectIntegrationGitHub' },
{name: 'ProjectIntegrationDiscord'},
{name: 'ProjectIntegrationSlack'},
{name: 'ProjectIntegrationGitHub'},
],
},
],
@ -36,8 +36,8 @@ const fragmentMatcher = new IntrospectionFragmentMatcher({
});
const uri = '/graphql';
const cache = new InMemoryCache({ dataIdFromObject, fragmentMatcher });
const link = new BatchHttpLink({ uri, batchInterval: 1, batchMax: 1 });
const cache = new InMemoryCache({dataIdFromObject, fragmentMatcher});
const link = new BatchHttpLink({uri, batchInterval: 1, batchMax: 1});
const absintheBatchLink = new ApolloLink((operation, forward) => {
return forward(operation).map((response: any) => response.payload);
@ -48,7 +48,7 @@ const authLink = (getSession: any) => {
const token = getSession().credentials.token;
if (token) {
operation.setContext(({ headers = {} }: any) => ({
operation.setContext(({headers = {}}: any) => ({
headers: {
...headers,
authorization: `Bearer ${token}`,

View File

@ -7,7 +7,7 @@ export default class FromRoute extends Service {
router: RouterService;
transitionTo(from: any | null, current: string, ...fallback: any[]) {
if (from && !from.name.startsWith(current)) {
if (from && from.name && !from.name.startsWith(current)) {
this.router.transitionTo(
from.name,
...Object.values(from.parent.params as object)

213
webapp/package-lock.json generated
View File

@ -40,7 +40,6 @@
"ember-css-modules": "2.0.0",
"ember-css-modules-sass": "1.1.0",
"ember-fetch": "8.1.1",
"ember-in-viewport": "4.0.0",
"ember-inline-svg": "1.0.1",
"ember-intl": "5.7.2",
"ember-keyboard": "7.0.1",
@ -2146,13 +2145,13 @@
}
},
"node_modules/@embroider/macros": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@embroider/macros/-/macros-1.2.0.tgz",
"integrity": "sha512-WD2V3OKXZB73OymI/zC2+MbqIYaAskhjtSOVVY6yG6kWILyVsJ6+fcbNHEnZyGqs4sm0TvHVJfevmA2OXV8Pww==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@embroider/macros/-/macros-1.13.2.tgz",
"integrity": "sha512-AUgJ71xG8kjuTx8XB1AQNBiebJuXRfhcHr318dCwnQz9VRXdYSnEEqf38XRvGYIoCvIyn/3c72LrSwzaJqknOA==",
"dependencies": {
"@embroider/shared-internals": "1.2.0",
"@embroider/shared-internals": "2.5.0",
"assert-never": "^1.2.1",
"babel-import-util": "^1.1.0",
"babel-import-util": "^2.0.0",
"ember-cli-babel": "^7.26.6",
"find-up": "^5.0.0",
"lodash": "^4.17.21",
@ -2161,6 +2160,77 @@
},
"engines": {
"node": "12.* || 14.* || >= 16"
},
"peerDependencies": {
"@glint/template": "^1.0.0"
},
"peerDependenciesMeta": {
"@glint/template": {
"optional": true
}
}
},
"node_modules/@embroider/macros/node_modules/@embroider/shared-internals": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@embroider/shared-internals/-/shared-internals-2.5.0.tgz",
"integrity": "sha512-7qzrb7GVIyNqeY0umxoeIvjDC+ay1b+wb2yCVuYTUYrFfLAkLEy9FNI3iWCi3RdQ9OFjgcAxAnwsAiPIMZZ3pQ==",
"dependencies": {
"babel-import-util": "^2.0.0",
"debug": "^4.3.2",
"ember-rfc176-data": "^0.3.17",
"fs-extra": "^9.1.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.21",
"resolve-package-path": "^4.0.1",
"semver": "^7.3.5",
"typescript-memoize": "^1.0.1"
},
"engines": {
"node": "12.* || 14.* || >= 16"
}
},
"node_modules/@embroider/macros/node_modules/babel-import-util": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/babel-import-util/-/babel-import-util-2.0.1.tgz",
"integrity": "sha512-N1ZfNprtf/37x0R05J0QCW/9pCAcuI+bjZIK9tlu0JEkwEST7ssdD++gxHRbD58AiG5QE5OuNYhRoEFsc1wESw==",
"engines": {
"node": ">= 12.*"
}
},
"node_modules/@embroider/macros/node_modules/fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"dependencies": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@embroider/macros/node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"node_modules/@embroider/macros/node_modules/resolve-package-path": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/resolve-package-path/-/resolve-package-path-4.0.3.tgz",
"integrity": "sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==",
"dependencies": {
"path-root": "^0.1.1"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/@embroider/macros/node_modules/semver": {
@ -2177,6 +2247,14 @@
"node": ">=10"
}
},
"node_modules/@embroider/macros/node_modules/universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/@embroider/shared-internals": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@embroider/shared-internals/-/shared-internals-1.2.0.tgz",
@ -15533,22 +15611,6 @@
"node": "8.* || >= 10.*"
}
},
"node_modules/ember-in-viewport": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ember-in-viewport/-/ember-in-viewport-4.0.0.tgz",
"integrity": "sha512-woEBMovlmRD8nV634GIqalOQV6AnmxleRE6XaWrKNmHMEHcg/IkmivWct+LdVFHCkSXi+kC36eiD/G9TVXo3eQ==",
"dependencies": {
"ember-auto-import": "^2.2.3",
"ember-cli-babel": "^7.26.6",
"ember-modifier": "^2.1.2 || ^3.0.0",
"fast-deep-equal": "^2.0.1",
"intersection-observer-admin": "~0.3.2",
"raf-pool": "~0.1.4"
},
"engines": {
"node": "12.* || 14.* || >= 16"
}
},
"node_modules/ember-inline-svg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ember-inline-svg/-/ember-inline-svg-1.0.1.tgz",
@ -19889,11 +19951,6 @@
"integrity": "sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==",
"dev": true
},
"node_modules/fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"node_modules/fast-glob": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
@ -21937,11 +21994,6 @@
"node": ">= 0.4"
}
},
"node_modules/intersection-observer-admin": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/intersection-observer-admin/-/intersection-observer-admin-0.3.2.tgz",
"integrity": "sha512-kJEcF9iVRuI4thXiJd28UzFDgvFHBNUgwkKA4F0bPMmRdzc+1Eq7/J13n2gSgfZ5tsVxb+wJOV7k3DXcsc7D6Q=="
},
"node_modules/intl-messageformat": {
"version": "9.11.4",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.11.4.tgz",
@ -25411,11 +25463,6 @@
"underscore.string": "~3.3.4"
}
},
"node_modules/raf-pool": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/raf-pool/-/raf-pool-0.1.4.tgz",
"integrity": "sha512-BBPamTVuSprPq7CUmgxc+ycbsYUtUYnQtJYEfMHXMaostPaNpQzipLfSa/rwjmlgjBPiD7G+I+8W340sLOPu6g=="
},
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -30698,13 +30745,13 @@
}
},
"@embroider/macros": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@embroider/macros/-/macros-1.2.0.tgz",
"integrity": "sha512-WD2V3OKXZB73OymI/zC2+MbqIYaAskhjtSOVVY6yG6kWILyVsJ6+fcbNHEnZyGqs4sm0TvHVJfevmA2OXV8Pww==",
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/@embroider/macros/-/macros-1.13.2.tgz",
"integrity": "sha512-AUgJ71xG8kjuTx8XB1AQNBiebJuXRfhcHr318dCwnQz9VRXdYSnEEqf38XRvGYIoCvIyn/3c72LrSwzaJqknOA==",
"requires": {
"@embroider/shared-internals": "1.2.0",
"@embroider/shared-internals": "2.5.0",
"assert-never": "^1.2.1",
"babel-import-util": "^1.1.0",
"babel-import-util": "^2.0.0",
"ember-cli-babel": "^7.26.6",
"find-up": "^5.0.0",
"lodash": "^4.17.21",
@ -30712,6 +30759,55 @@
"semver": "^7.3.2"
},
"dependencies": {
"@embroider/shared-internals": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@embroider/shared-internals/-/shared-internals-2.5.0.tgz",
"integrity": "sha512-7qzrb7GVIyNqeY0umxoeIvjDC+ay1b+wb2yCVuYTUYrFfLAkLEy9FNI3iWCi3RdQ9OFjgcAxAnwsAiPIMZZ3pQ==",
"requires": {
"babel-import-util": "^2.0.0",
"debug": "^4.3.2",
"ember-rfc176-data": "^0.3.17",
"fs-extra": "^9.1.0",
"js-string-escape": "^1.0.1",
"lodash": "^4.17.21",
"resolve-package-path": "^4.0.1",
"semver": "^7.3.5",
"typescript-memoize": "^1.0.1"
}
},
"babel-import-util": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/babel-import-util/-/babel-import-util-2.0.1.tgz",
"integrity": "sha512-N1ZfNprtf/37x0R05J0QCW/9pCAcuI+bjZIK9tlu0JEkwEST7ssdD++gxHRbD58AiG5QE5OuNYhRoEFsc1wESw=="
},
"fs-extra": {
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
"integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==",
"requires": {
"at-least-node": "^1.0.0",
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
}
},
"jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"requires": {
"graceful-fs": "^4.1.6",
"universalify": "^2.0.0"
}
},
"resolve-package-path": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/resolve-package-path/-/resolve-package-path-4.0.3.tgz",
"integrity": "sha512-SRpNAPW4kewOaNUt8VPqhJ0UMxawMwzJD8V7m1cJfdSTK9ieZwS6K7Dabsm4bmLFM96Z5Y/UznrpG5kt1im8yA==",
"requires": {
"path-root": "^0.1.1"
}
},
"semver": {
"version": "7.3.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz",
@ -30719,6 +30815,11 @@
"requires": {
"lru-cache": "^6.0.0"
}
},
"universalify": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz",
"integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ=="
}
}
},
@ -41875,19 +41976,6 @@
}
}
},
"ember-in-viewport": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/ember-in-viewport/-/ember-in-viewport-4.0.0.tgz",
"integrity": "sha512-woEBMovlmRD8nV634GIqalOQV6AnmxleRE6XaWrKNmHMEHcg/IkmivWct+LdVFHCkSXi+kC36eiD/G9TVXo3eQ==",
"requires": {
"ember-auto-import": "^2.2.3",
"ember-cli-babel": "^7.26.6",
"ember-modifier": "^2.1.2 || ^3.0.0",
"fast-deep-equal": "^2.0.1",
"intersection-observer-admin": "~0.3.2",
"raf-pool": "~0.1.4"
}
},
"ember-inline-svg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ember-inline-svg/-/ember-inline-svg-1.0.1.tgz",
@ -45361,11 +45449,6 @@
"integrity": "sha512-HaFMBi7r+oEC9iJNpc3bvcW7Z7iLmM26hPDmlb0mFwyANSsOQAtJxbdWsXITKOzZUyMYK0zYCv3h5yDj9TsiXg==",
"dev": true
},
"fast-deep-equal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz",
"integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk="
},
"fast-glob": {
"version": "2.2.7",
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz",
@ -46992,11 +47075,6 @@
"side-channel": "^1.0.4"
}
},
"intersection-observer-admin": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/intersection-observer-admin/-/intersection-observer-admin-0.3.2.tgz",
"integrity": "sha512-kJEcF9iVRuI4thXiJd28UzFDgvFHBNUgwkKA4F0bPMmRdzc+1Eq7/J13n2gSgfZ5tsVxb+wJOV7k3DXcsc7D6Q=="
},
"intl-messageformat": {
"version": "9.11.4",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.11.4.tgz",
@ -49751,11 +49829,6 @@
"underscore.string": "~3.3.4"
}
},
"raf-pool": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/raf-pool/-/raf-pool-0.1.4.tgz",
"integrity": "sha512-BBPamTVuSprPq7CUmgxc+ycbsYUtUYnQtJYEfMHXMaostPaNpQzipLfSa/rwjmlgjBPiD7G+I+8W340sLOPu6g=="
},
"randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",

View File

@ -59,7 +59,6 @@
"ember-css-modules": "2.0.0",
"ember-css-modules-sass": "1.1.0",
"ember-fetch": "8.1.1",
"ember-in-viewport": "4.0.0",
"ember-inline-svg": "1.0.1",
"ember-intl": "5.7.2",
"ember-keyboard": "7.0.1",