Merge branch 'add-languages-to-admin-settings' into 'master'

Add languages to admin settings

See merge request framasoft/mobilizon!587
This commit is contained in:
Thomas Citharel 2020-10-07 17:01:35 +02:00
commit 07ab35ab87
15 changed files with 179 additions and 22 deletions

View file

@ -18,6 +18,7 @@ config :mobilizon, :instance,
hostname: "localhost", hostname: "localhost",
registrations_open: false, registrations_open: false,
registration_email_allowlist: [], registration_email_allowlist: [],
languages: [],
demo: false, demo: false,
repository: Mix.Project.config()[:source_url], repository: Mix.Project.config()[:source_url],
allow_relay: true, allow_relay: true,

View file

@ -104,6 +104,15 @@ export const REJECT_RELAY = gql`
${RELAY_FRAGMENT} ${RELAY_FRAGMENT}
`; `;
export const LANGUAGES = gql`
query {
languages {
code
name
}
}
`;
export const ADMIN_SETTINGS_FRAGMENT = gql` export const ADMIN_SETTINGS_FRAGMENT = gql`
fragment adminSettingsFragment on AdminSettings { fragment adminSettingsFragment on AdminSettings {
instanceName instanceName
@ -118,6 +127,7 @@ export const ADMIN_SETTINGS_FRAGMENT = gql`
instancePrivacyPolicyUrl instancePrivacyPolicyUrl
instanceRules instanceRules
registrationsOpen registrationsOpen
instanceLanguages
} }
`; `;
@ -144,6 +154,7 @@ export const SAVE_ADMIN_SETTINGS = gql`
$instancePrivacyPolicyUrl: String $instancePrivacyPolicyUrl: String
$instanceRules: String $instanceRules: String
$registrationsOpen: Boolean $registrationsOpen: Boolean
$instanceLanguages: [String]
) { ) {
saveAdminSettings( saveAdminSettings(
instanceName: $instanceName instanceName: $instanceName
@ -158,6 +169,7 @@ export const SAVE_ADMIN_SETTINGS = gql`
instancePrivacyPolicyUrl: $instancePrivacyPolicyUrl instancePrivacyPolicyUrl: $instancePrivacyPolicyUrl
instanceRules: $instanceRules instanceRules: $instanceRules
registrationsOpen: $registrationsOpen registrationsOpen: $registrationsOpen
instanceLanguages: $instanceLanguages
) { ) {
...adminSettingsFragment ...adminSettingsFragment
} }

View file

@ -789,5 +789,9 @@
"Congratulations, your account is now created!": "Congratulations, your account is now created!", "Congratulations, your account is now created!": "Congratulations, your account is now created!",
"Now, create your first profile:": "Now, create your first profile:", "Now, create your first profile:": "Now, create your first profile:",
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.", "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.",
"You will be able to add an avatar and set other options in your account settings.": "You will be able to add an avatar and set other options in your account settings." "You will be able to add an avatar and set other options in your account settings.": "You will be able to add an avatar and set other options in your account settings.",
"Instance languages": "Instance languages",
"Main languages you/your moderators speak": "Main languages you/your moderators speak",
"Select languages": "Select languages",
"No languages found": "No languages found"
} }

View file

@ -388,7 +388,7 @@
"My events": "Mes évènements", "My events": "Mes évènements",
"My groups": "Mes groupes", "My groups": "Mes groupes",
"My identities": "Mes identités", "My identities": "Mes identités",
"NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "REMARQUE : les conditions par défaut n'ont pas été vérifiées par un juriste et ne sont donc susceptibles de ne pas offrir une protection juridique complète dans toutes les situations pour un·e administrateur·ice d'instance qui les utilise. Elles ne sont pas non plus spécifiques à tous les pays et juridictions. Si vous n'êtes pas sûr, veuillez consulter un juriste.", "NOTE! The default terms have not been checked over by a lawyer and thus are unlikely to provide full legal protection for all situations for an instance admin using them. They are also not specific to all countries and jurisdictions. If you are unsure, please check with a lawyer.": "REMARQUE : les conditions par défaut n'ont pas été vérifiées par un·e juriste et ne sont donc susceptibles de ne pas offrir une protection juridique complète dans toutes les situations pour un·e administrateur·ice d'instance qui les utilise. Elles ne sont pas non plus spécifiques à tous les pays et juridictions. Si vous n'êtes pas sûr, veuillez consulter un·e juriste.",
"Name": "Nom", "Name": "Nom",
"New discussion": "Nouvelle discussion", "New discussion": "Nouvelle discussion",
"New email": "Nouvelle adresse e-mail", "New email": "Nouvelle adresse e-mail",
@ -831,5 +831,9 @@
"Congratulations, your account is now created!": "Bravo, votre compte est dorénavant créé !", "Congratulations, your account is now created!": "Bravo, votre compte est dorénavant créé !",
"Now, create your first profile:": "Maintenant, créez votre premier profil :", "Now, create your first profile:": "Maintenant, créez votre premier profil :",
"If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.", "If you have opted for manual validation of participants, Mobilizon will send you an email to inform you of new participations to be processed. You can choose the frequency of these notifications below.": "Si vous avez opté pour la validation manuelle des participantes, Mobilizon vous enverra un email pour vous informer des nouvelles participations à traiter. Vous pouvez choisir la fréquence de ces notifications ci-dessous.",
"You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte." "You will be able to add an avatar and set other options in your account settings.": "Vous pourrez ajouter un avatar et définir d'autres options dans les paramètres de votre compte.",
"Instance languages": "Langues de l'instance",
"Main languages you/your moderators speak": "Principales langues parlées par vous / vos modérateurs",
"Select languages": "Choisissez une langue",
"No languages found": "Aucune langue trouvée"
} }

View file

@ -20,6 +20,11 @@ export enum InstancePrivacyType {
CUSTOM = "CUSTOM", CUSTOM = "CUSTOM",
} }
export interface ILanguage {
code: string;
name: string;
}
export interface IAdminSettings { export interface IAdminSettings {
instanceName: string; instanceName: string;
instanceDescription: string; instanceDescription: string;
@ -33,4 +38,5 @@ export interface IAdminSettings {
instancePrivacyPolicyUrl: string | null; instancePrivacyPolicyUrl: string | null;
instanceRules: string; instanceRules: string;
registrationsOpen: boolean; registrationsOpen: boolean;
instanceLanguages: string[];
} }

View file

@ -43,6 +43,24 @@
<p class="content" v-else>{{ $t("Registration is closed.") }}</p> <p class="content" v-else>{{ $t("Registration is closed.") }}</p>
</b-switch> </b-switch>
</b-field> </b-field>
<div class="field">
<label class="label has-help">{{ $t("Instance languages") }}</label>
<small>
{{ $t("Main languages you/your moderators speak") }}
</small>
<b-taginput
v-model="adminSettings.instanceLanguages"
:data="filteredLanguages"
autocomplete
:open-on-focus="true"
field="name"
icon="label"
:placeholder="$t('Select languages')"
@typing="getFilteredLanguages"
>
<template slot="empty">{{ $t("No languages found") }}</template>
</b-taginput>
</div>
<div class="field"> <div class="field">
<label class="label has-help">{{ $t("Instance Long Description") }}</label> <label class="label has-help">{{ $t("Instance Long Description") }}</label>
<small> <small>
@ -256,32 +274,55 @@
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from "vue-property-decorator"; import { Component, Vue, Watch } from "vue-property-decorator";
import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS } from "@/graphql/admin"; import { ADMIN_SETTINGS, SAVE_ADMIN_SETTINGS, LANGUAGES } from "@/graphql/admin";
import { IAdminSettings, InstanceTermsType, InstancePrivacyType } from "../../types/admin.model"; import {
IAdminSettings,
InstanceTermsType,
InstancePrivacyType,
ILanguage,
} from "../../types/admin.model";
import RouteName from "../../router/name"; import RouteName from "../../router/name";
@Component({ @Component({
apollo: { apollo: {
adminSettings: ADMIN_SETTINGS, adminSettings: ADMIN_SETTINGS,
languages: LANGUAGES,
}, },
}) })
export default class Settings extends Vue { export default class Settings extends Vue {
adminSettings!: IAdminSettings; adminSettings!: IAdminSettings;
languages!: ILanguage[];
filteredLanguages: string[] = [];
InstanceTermsType = InstanceTermsType; InstanceTermsType = InstanceTermsType;
InstancePrivacyType = InstancePrivacyType; InstancePrivacyType = InstancePrivacyType;
RouteName = RouteName; RouteName = RouteName;
async updateSettings() { @Watch("languages")
setCorrectLanguagesNames(): void {
if (this.languages && this.adminSettings) {
this.adminSettings.instanceLanguages = this.adminSettings.instanceLanguages
.map((code) => this.languageForCode(code))
.filter((language) => language) as string[];
}
}
async updateSettings(): Promise<void> {
const variables = { ...this.adminSettings };
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
variables.instanceLanguages = variables.instanceLanguages.map((language) => {
return this.codeForLanguage(language);
});
try { try {
await this.$apollo.mutate({ await this.$apollo.mutate({
mutation: SAVE_ADMIN_SETTINGS, mutation: SAVE_ADMIN_SETTINGS,
variables: { variables,
...this.adminSettings,
},
}); });
this.$notifier.success(this.$t("Admin settings successfully saved.") as string); this.$notifier.success(this.$t("Admin settings successfully saved.") as string);
} catch (e) { } catch (e) {
@ -289,6 +330,32 @@ export default class Settings extends Vue {
this.$notifier.error(this.$t("Failed to save admin settings") as string); this.$notifier.error(this.$t("Failed to save admin settings") as string);
} }
} }
getFilteredLanguages(text: string): void {
this.filteredLanguages = this.languages
? this.languages
.filter((language: ILanguage) => {
return language.name.toString().toLowerCase().indexOf(text.toLowerCase()) >= 0;
})
.map(({ name }) => name)
: [];
}
codeForLanguage(language: string): string | undefined {
if (this.languages) {
const lang = this.languages.find(({ name }) => name === language);
if (lang) return lang.code;
}
return undefined;
}
languageForCode(codeGiven: string): string | undefined {
if (this.languages) {
const lang = this.languages.find(({ code }) => code === codeGiven);
if (lang) return lang.name;
}
return undefined;
}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -8,6 +8,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
alias Mobilizon.{Actors, Admin, Config, Events} alias Mobilizon.{Actors, Admin, Config, Events}
alias Mobilizon.Actors.Actor alias Mobilizon.Actors.Actor
alias Mobilizon.Admin.{ActionLog, Setting} alias Mobilizon.Admin.{ActionLog, Setting}
alias Mobilizon.Cldr.Language
alias Mobilizon.Config alias Mobilizon.Config
alias Mobilizon.Discussions.Comment alias Mobilizon.Discussions.Comment
alias Mobilizon.Events.Event alias Mobilizon.Events.Event
@ -156,6 +157,19 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
end end
end end
def get_list_of_languages(_parent, _args, _resolution) do
locale = Gettext.get_locale()
case Language.known_languages(locale) do
data when is_map(data) ->
data = Enum.map(data, fn {code, elem} -> %{code: code, name: elem.standard} end)
{:ok, data}
{:error, err} ->
{:error, err}
end
end
def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}}) def get_dashboard(_parent, _args, %{context: %{current_user: %User{role: role}}})
when is_admin(role) do when is_admin(role) do
last_public_event_published = last_public_event_published =
@ -202,11 +216,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Admin do
res <- res <-
res res
|> Enum.map(fn {key, %Setting{value: value}} -> |> Enum.map(fn {key, %Setting{value: value}} ->
case value do {key, Admin.get_setting_value(value)}
"true" -> {key, true}
"false" -> {key, false}
value -> {key, value}
end
end) end)
|> Enum.into(%{}), |> Enum.into(%{}),
:ok <- eventually_update_instance_actor(res) do :ok <- eventually_update_instance_actor(res) do

View file

@ -75,6 +75,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Config do
demo_mode: Config.instance_demo_mode?(), demo_mode: Config.instance_demo_mode?(),
description: Config.instance_description(), description: Config.instance_description(),
long_description: Config.instance_long_description(), long_description: Config.instance_long_description(),
languages: Config.instance_languages(),
anonymous: %{ anonymous: %{
participation: %{ participation: %{
allowed: Config.anonymous_participation?(), allowed: Config.anonymous_participation?(),

View file

@ -64,6 +64,11 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
end) end)
end end
object :language do
field(:code, :string, description: "The iso-639-3 language code")
field(:name, :string, description: "The language name")
end
object :dashboard do object :dashboard do
field(:last_public_event_published, :event, description: "Last public event publish") field(:last_public_event_published, :event, description: "Last public event publish")
field(:number_of_users, :integer, description: "The number of local users") field(:number_of_users, :integer, description: "The number of local users")
@ -85,6 +90,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
field(:instance_privacy_policy_url, :string) field(:instance_privacy_policy_url, :string)
field(:instance_rules, :string) field(:instance_rules, :string)
field(:registrations_open, :boolean) field(:registrations_open, :boolean)
field(:instance_languages, list_of(:string))
end end
enum :instance_terms_type do enum :instance_terms_type do
@ -107,6 +113,10 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
resolve(&Admin.list_action_logs/3) resolve(&Admin.list_action_logs/3)
end end
field :languages, type: list_of(:language) do
resolve(&Admin.get_list_of_languages/3)
end
field :dashboard, type: :dashboard do field :dashboard, type: :dashboard do
resolve(&Admin.get_dashboard/3) resolve(&Admin.get_dashboard/3)
end end
@ -172,6 +182,7 @@ defmodule Mobilizon.GraphQL.Schema.AdminType do
arg(:instance_privacy_policy_url, :string) arg(:instance_privacy_policy_url, :string)
arg(:instance_rules, :string) arg(:instance_rules, :string)
arg(:registrations_open, :boolean) arg(:registrations_open, :boolean)
arg(:instance_languages, list_of(:string))
resolve(&Admin.save_settings/3) resolve(&Admin.save_settings/3)
end end

View file

@ -14,6 +14,7 @@ defmodule Mobilizon.GraphQL.Schema.ConfigType do
field(:long_description, :string) field(:long_description, :string)
field(:contact, :string) field(:contact, :string)
field(:languages, list_of(:string))
field(:registrations_open, :boolean) field(:registrations_open, :boolean)
field(:registrations_allowlist, :boolean) field(:registrations_allowlist, :boolean)
field(:demo_mode, :boolean) field(:demo_mode, :boolean)

View file

@ -80,10 +80,31 @@ defmodule Mobilizon.Admin do
def get_admin_setting_value(group, name, fallback \\ nil) def get_admin_setting_value(group, name, fallback \\ nil)
when is_bitstring(group) and is_bitstring(name) do when is_bitstring(group) and is_bitstring(name) do
case Repo.get_by(Setting, group: group, name: name) do case Repo.get_by(Setting, group: group, name: name) do
nil -> fallback nil ->
%Setting{value: ""} -> fallback fallback
%Setting{value: nil} -> fallback
%Setting{value: value} -> value %Setting{value: ""} ->
fallback
%Setting{value: nil} ->
fallback
%Setting{value: value} ->
get_setting_value(value)
end
end
def get_setting_value(value) do
case Jason.decode(value) do
{:ok, val} ->
val
{:error, _} ->
case value do
"true" -> true
"false" -> false
value -> value
end
end end
end end
@ -116,7 +137,7 @@ defmodule Mobilizon.Admin do
Setting.changeset(%Setting{}, %{ Setting.changeset(%Setting{}, %{
group: group, group: group,
name: Atom.to_string(key), name: Atom.to_string(key),
value: to_string(val) value: convert_to_string(val)
}), }),
on_conflict: :replace_all, on_conflict: :replace_all,
conflict_target: [:group, :name] conflict_target: [:group, :name]
@ -124,4 +145,11 @@ defmodule Mobilizon.Admin do
do_save_setting(transaction, group, rest) do_save_setting(transaction, group, rest)
end end
defp convert_to_string(val) do
case val do
val when is_list(val) -> Jason.encode!(val)
val -> to_string(val)
end
end
end end

View file

@ -5,5 +5,5 @@ defmodule Mobilizon.Cldr do
use Cldr, use Cldr,
locales: ["cs", "de", "en", "es", "fr", "it", "ja", "nl", "pl", "pt", "ru"], locales: ["cs", "de", "en", "es", "fr", "it", "ja", "nl", "pl", "pt", "ru"],
providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime] providers: [Cldr.Number, Cldr.Calendar, Cldr.DateTime, Cldr.Language]
end end

View file

@ -99,6 +99,15 @@ defmodule Mobilizon.Config do
) )
) )
@spec instance_languages :: list(String.t())
def instance_languages,
do:
Mobilizon.Admin.get_admin_setting_value(
"instance",
"instance_languages",
instance_config()[:languages]
)
@spec instance_registrations_allowlist :: list(String.t()) @spec instance_registrations_allowlist :: list(String.t())
def instance_registrations_allowlist, do: instance_config()[:registration_email_allowlist] def instance_registrations_allowlist, do: instance_config()[:registration_email_allowlist]
@ -319,7 +328,8 @@ defmodule Mobilizon.Config do
instance_privacy_policy: instance_privacy(), instance_privacy_policy: instance_privacy(),
instance_privacy_policy_type: instance_privacy_type(), instance_privacy_policy_type: instance_privacy_type(),
instance_privacy_policy_url: instance_privacy_url(), instance_privacy_policy_url: instance_privacy_url(),
instance_rules: instance_rules() instance_rules: instance_rules(),
instance_languages: instance_languages()
} }
end end

View file

@ -135,6 +135,7 @@ defmodule Mobilizon.Mixfile do
{:sitemapper, "~> 0.4.0"}, {:sitemapper, "~> 0.4.0"},
{:xml_builder, "~> 2.1.1", override: true}, {:xml_builder, "~> 2.1.1", override: true},
{:remote_ip, "~> 0.2.0"}, {:remote_ip, "~> 0.2.0"},
{:ex_cldr_languages, "~> 0.2.1"},
# Dev and test dependencies # Dev and test dependencies
{:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]}, {:phoenix_live_reload, "~> 1.2", only: [:dev, :e2e]},
{:ex_machina, "~> 2.3", only: [:dev, :test]}, {:ex_machina, "~> 2.3", only: [:dev, :test]},

View file

@ -39,6 +39,7 @@
"ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.10.1", "7e45fd1a26711d644c599e06df132669117bee2ff8e54b93f15e14720f821a9b", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "82a024b160719050e1245a67c62c3be535ddeac70d8c25e0ff1ae69f860da4d1"}, "ex_cldr_calendars": {:hex, :ex_cldr_calendars, "1.10.1", "7e45fd1a26711d644c599e06df132669117bee2ff8e54b93f15e14720f821a9b", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:earmark, "~> 1.0", [hex: :earmark, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.13", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_cldr_units, "~> 3.0", [hex: :ex_cldr_units, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "82a024b160719050e1245a67c62c3be535ddeac70d8c25e0ff1ae69f860da4d1"},
"ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.7.0", "d302aa589b4c2a28bc274b1944f879c2505aaf52f9427b04687420d55e317b8b", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b47efd833d5297b3644ff2b59a1d3a0c4b90214847626fbd9e52bcc3db7900b2"}, "ex_cldr_currencies": {:hex, :ex_cldr_currencies, "2.7.0", "d302aa589b4c2a28bc274b1944f879c2505aaf52f9427b04687420d55e317b8b", [:mix], [{:ex_cldr, "~> 2.14", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "b47efd833d5297b3644ff2b59a1d3a0c4b90214847626fbd9e52bcc3db7900b2"},
"ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.4", "18143155be8146a3a8b3def620dc4e54b9676143e381519370b2b3fcf8deefbc", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9cf13e6c4a42005a817cd20f82b06697595edee741b60379fbf6a207ca6134b"}, "ex_cldr_dates_times": {:hex, :ex_cldr_dates_times, "2.5.4", "18143155be8146a3a8b3def620dc4e54b9676143e381519370b2b3fcf8deefbc", [:mix], [{:calendar_interval, "~> 0.2", [hex: :calendar_interval, repo: "hexpm", optional: true]}, {:ex_cldr_calendars, "~> 1.8", [hex: :ex_cldr_calendars, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.15", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "a9cf13e6c4a42005a817cd20f82b06697595edee741b60379fbf6a207ca6134b"},
"ex_cldr_languages": {:hex, :ex_cldr_languages, "0.2.1", "4b7d011fc76c563b9d680fb1d1cbc46e485acd50472a22850b7642f463a750f4", [:mix], [{:ex_cldr, "~> 2.2 and >= 2.2.1", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "052bf33032229300d0c1b82bee277611b13ab03d0f1c15f6ce4dd60da0cfd30a"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.4", "1cdeb1f6a22f31e7155edde7d51b3c95ddf6ccf60252a175c10967d4031baebd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.6", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1765b8579b4df5b9fec09bb5506841bf4a51d3dba2c51046e6af05a08fbe7ee1"}, "ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.15.4", "1cdeb1f6a22f31e7155edde7d51b3c95ddf6ccf60252a175c10967d4031baebd", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.15", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.6", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "1765b8579b4df5b9fec09bb5506841bf4a51d3dba2c51046e6af05a08fbe7ee1"},
"ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"}, "ex_crypto": {:hex, :ex_crypto, "0.10.0", "af600a89b784b36613a989da6e998c1b200ff1214c3cfbaf8deca4aa2f0a1739", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm", "ccc7472cfe8a0f4565f97dce7e9280119bf15a5ea51c6535e5b65f00660cde1c"},
"ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"}, "ex_doc": {:hex, :ex_doc, "0.22.6", "0fb1e09a3e8b69af0ae94c8b4e4df36995d8c88d5ec7dbd35617929144b62c00", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "1e0aceda15faf71f1b0983165e6e7313be628a460e22a031e32913b98edbd638"},