Complete localization and mobile accessibility
This commit is contained in:
parent
13fbeec958
commit
7581283b47
|
|
@ -65,7 +65,7 @@
|
||||||
--color-base-100: oklch(98% 0 0);
|
--color-base-100: oklch(98% 0 0);
|
||||||
--color-base-200: oklch(96% 0.001 286.375);
|
--color-base-200: oklch(96% 0.001 286.375);
|
||||||
--color-base-300: oklch(92% 0.004 286.32);
|
--color-base-300: oklch(92% 0.004 286.32);
|
||||||
--color-base-content: oklch(21% 0.006 285.885);
|
--color-base-content: oklch(18% 0.006 285.885);
|
||||||
--color-primary: #9a3412;
|
--color-primary: #9a3412;
|
||||||
--color-primary-content: #ffffff;
|
--color-primary-content: #ffffff;
|
||||||
--color-secondary: oklch(55% 0.027 264.364);
|
--color-secondary: oklch(55% 0.027 264.364);
|
||||||
|
|
@ -74,13 +74,13 @@
|
||||||
--color-accent-content: oklch(100% 0 0);
|
--color-accent-content: oklch(100% 0 0);
|
||||||
--color-neutral: oklch(44% 0.017 285.786);
|
--color-neutral: oklch(44% 0.017 285.786);
|
||||||
--color-neutral-content: oklch(98% 0 0);
|
--color-neutral-content: oklch(98% 0 0);
|
||||||
--color-info: oklch(62% 0.214 259.815);
|
--color-info: #1d4ed8;
|
||||||
--color-info-content: oklch(97% 0.014 254.604);
|
--color-info-content: oklch(97% 0.014 254.604);
|
||||||
--color-success: oklch(45% 0.12 182.503);
|
--color-success: oklch(45% 0.12 182.503);
|
||||||
--color-success-content: oklch(98% 0.014 180.72);
|
--color-success-content: oklch(98% 0.014 180.72);
|
||||||
--color-warning: #b45309;
|
--color-warning: #b45309;
|
||||||
--color-warning-content: #fffaf0;
|
--color-warning-content: #fffaf0;
|
||||||
--color-error: oklch(58% 0.253 17.585);
|
--color-error: #be123c;
|
||||||
--color-error-content: oklch(96% 0.015 12.422);
|
--color-error-content: oklch(96% 0.015 12.422);
|
||||||
--radius-selector: 0.25rem;
|
--radius-selector: 0.25rem;
|
||||||
--radius-field: 0.25rem;
|
--radius-field: 0.25rem;
|
||||||
|
|
@ -109,6 +109,12 @@ html {
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Keep compact field labels readable on both the light and dark secondary
|
||||||
|
surfaces. The component-library default falls just below WCAG AA at 12px. */
|
||||||
|
.label {
|
||||||
|
color: var(--color-base-content);
|
||||||
|
}
|
||||||
|
|
||||||
.skip-link {
|
.skip-link {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
@ -328,6 +334,17 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 767px) {
|
@media (max-width: 767px) {
|
||||||
|
.aid-map .maplibregl-ctrl-group button,
|
||||||
|
.aid-map .maplibregl-ctrl-attrib-button {
|
||||||
|
width: 2.75rem;
|
||||||
|
height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.request-map-cluster {
|
||||||
|
min-width: 2.75rem;
|
||||||
|
min-height: 2.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
.request-discovery-toolbar {
|
.request-discovery-toolbar {
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -715,7 +732,7 @@ html {
|
||||||
}
|
}
|
||||||
|
|
||||||
.request-location-mode small {
|
.request-location-mode small {
|
||||||
color: color-mix(in oklab, var(--color-base-content) 58%, transparent);
|
color: color-mix(in oklab, var(--color-base-content) 72%, transparent);
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
@ -817,7 +834,7 @@ html {
|
||||||
|
|
||||||
.request-location-radius span {
|
.request-location-radius span {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 2.5rem;
|
min-height: 2.75rem;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: 1px solid var(--color-base-300);
|
border: 1px solid var(--color-base-300);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,80 @@ import maplibregl from "maplibre-gl"
|
||||||
|
|
||||||
const trackingMinTimeMs = 5000
|
const trackingMinTimeMs = 5000
|
||||||
|
|
||||||
|
const uiLocale = () => document.documentElement.lang || undefined
|
||||||
|
|
||||||
|
const mapLocaleByLanguage = {
|
||||||
|
ru: {
|
||||||
|
"AttributionControl.ToggleAttribution": "Показать сведения об источниках",
|
||||||
|
"AttributionControl.MapFeedback": "Сообщить о карте",
|
||||||
|
"FullscreenControl.Enter": "На весь экран",
|
||||||
|
"FullscreenControl.Exit": "Выйти из полноэкранного режима",
|
||||||
|
"GeolocateControl.FindMyLocation": "Найти моё местоположение",
|
||||||
|
"GeolocateControl.LocationNotAvailable": "Местоположение недоступно",
|
||||||
|
"LogoControl.Title": "Логотип MapLibre",
|
||||||
|
"Map.Title": "Карта",
|
||||||
|
"Marker.Title": "Маркер на карте",
|
||||||
|
"NavigationControl.ResetBearing":
|
||||||
|
"Перетащите, чтобы повернуть карту; нажмите, чтобы вернуть север вверх",
|
||||||
|
"NavigationControl.ZoomIn": "Увеличить масштаб",
|
||||||
|
"NavigationControl.ZoomOut": "Уменьшить масштаб",
|
||||||
|
"Popup.Close": "Закрыть",
|
||||||
|
"ScaleControl.Feet": "футы",
|
||||||
|
"ScaleControl.Meters": "метры",
|
||||||
|
"ScaleControl.Kilometers": "километры",
|
||||||
|
"ScaleControl.Miles": "мили",
|
||||||
|
"ScaleControl.NauticalMiles": "морские мили",
|
||||||
|
"GlobeControl.Enable": "Показать глобус",
|
||||||
|
"GlobeControl.Disable": "Показать плоскую карту",
|
||||||
|
"TerrainControl.Enable": "Показать рельеф",
|
||||||
|
"TerrainControl.Disable": "Скрыть рельеф",
|
||||||
|
"CooperativeGesturesHandler.WindowsHelpText":
|
||||||
|
"Удерживайте Ctrl при прокрутке, чтобы изменить масштаб карты",
|
||||||
|
"CooperativeGesturesHandler.MacHelpText":
|
||||||
|
"Удерживайте ⌘ при прокрутке, чтобы изменить масштаб карты",
|
||||||
|
"CooperativeGesturesHandler.MobileHelpText":
|
||||||
|
"Используйте два пальца, чтобы перемещать карту"
|
||||||
|
},
|
||||||
|
uk: {
|
||||||
|
"AttributionControl.ToggleAttribution": "Показати відомості про джерела",
|
||||||
|
"AttributionControl.MapFeedback": "Повідомити про карту",
|
||||||
|
"FullscreenControl.Enter": "На весь екран",
|
||||||
|
"FullscreenControl.Exit": "Вийти з повноекранного режиму",
|
||||||
|
"GeolocateControl.FindMyLocation": "Знайти моє місцезнаходження",
|
||||||
|
"GeolocateControl.LocationNotAvailable": "Місцезнаходження недоступне",
|
||||||
|
"LogoControl.Title": "Логотип MapLibre",
|
||||||
|
"Map.Title": "Карта",
|
||||||
|
"Marker.Title": "Маркер на карті",
|
||||||
|
"NavigationControl.ResetBearing":
|
||||||
|
"Перетягніть, щоб повернути карту; натисніть, щоб повернути північ угору",
|
||||||
|
"NavigationControl.ZoomIn": "Збільшити масштаб",
|
||||||
|
"NavigationControl.ZoomOut": "Зменшити масштаб",
|
||||||
|
"Popup.Close": "Закрити",
|
||||||
|
"ScaleControl.Feet": "фути",
|
||||||
|
"ScaleControl.Meters": "метри",
|
||||||
|
"ScaleControl.Kilometers": "кілометри",
|
||||||
|
"ScaleControl.Miles": "милі",
|
||||||
|
"ScaleControl.NauticalMiles": "морські милі",
|
||||||
|
"GlobeControl.Enable": "Показати глобус",
|
||||||
|
"GlobeControl.Disable": "Показати пласку карту",
|
||||||
|
"TerrainControl.Enable": "Показати рельєф",
|
||||||
|
"TerrainControl.Disable": "Сховати рельєф",
|
||||||
|
"CooperativeGesturesHandler.WindowsHelpText":
|
||||||
|
"Утримуйте Ctrl під час прокручування, щоб змінити масштаб карти",
|
||||||
|
"CooperativeGesturesHandler.MacHelpText":
|
||||||
|
"Утримуйте ⌘ під час прокручування, щоб змінити масштаб карти",
|
||||||
|
"CooperativeGesturesHandler.MobileHelpText":
|
||||||
|
"Використовуйте два пальці, щоб переміщувати карту"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapUiLocale = () => {
|
||||||
|
const language = (uiLocale() || "en").toLowerCase().split("-")[0]
|
||||||
|
return mapLocaleByLanguage[language] || {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapRegionLabel = () => mapUiLocale()["Map.Title"] || "Map"
|
||||||
|
|
||||||
const defaultStyle = {
|
const defaultStyle = {
|
||||||
version: 8,
|
version: 8,
|
||||||
sources: {
|
sources: {
|
||||||
|
|
@ -270,6 +344,8 @@ const createAidMap = element => {
|
||||||
const fallback = document.createElement("div")
|
const fallback = document.createElement("div")
|
||||||
fallback.className = "map-unavailable-panel"
|
fallback.className = "map-unavailable-panel"
|
||||||
fallback.dataset.mapFallback = "true"
|
fallback.dataset.mapFallback = "true"
|
||||||
|
fallback.setAttribute("role", "region")
|
||||||
|
fallback.setAttribute("aria-label", mapRegionLabel())
|
||||||
|
|
||||||
const message = document.createElement("p")
|
const message = document.createElement("p")
|
||||||
message.textContent = state.element.dataset.mapUnavailableLabel
|
message.textContent = state.element.dataset.mapUnavailableLabel
|
||||||
|
|
@ -281,6 +357,8 @@ const createAidMap = element => {
|
||||||
const fallback = document.createElement("div")
|
const fallback = document.createElement("div")
|
||||||
fallback.className = "home-demo-map-fallback"
|
fallback.className = "home-demo-map-fallback"
|
||||||
fallback.dataset.mapFallback = "true"
|
fallback.dataset.mapFallback = "true"
|
||||||
|
fallback.setAttribute("role", "region")
|
||||||
|
fallback.setAttribute("aria-label", mapRegionLabel())
|
||||||
|
|
||||||
const note = document.createElement("p")
|
const note = document.createElement("p")
|
||||||
note.className = "home-demo-map-fallback-note"
|
note.className = "home-demo-map-fallback-note"
|
||||||
|
|
@ -348,7 +426,7 @@ const createAidMap = element => {
|
||||||
const capturedAt = point.captured_at ? new Date(point.captured_at) : null
|
const capturedAt = point.captured_at ? new Date(point.captured_at) : null
|
||||||
const capturedAtLabel =
|
const capturedAtLabel =
|
||||||
capturedAt && !Number.isNaN(capturedAt.getTime())
|
capturedAt && !Number.isNaN(capturedAt.getTime())
|
||||||
? new Intl.DateTimeFormat(undefined, {
|
? new Intl.DateTimeFormat(uiLocale(), {
|
||||||
dateStyle: "short",
|
dateStyle: "short",
|
||||||
timeStyle: "medium"
|
timeStyle: "medium"
|
||||||
}).format(capturedAt)
|
}).format(capturedAt)
|
||||||
|
|
@ -568,6 +646,7 @@ const createAidMap = element => {
|
||||||
style: defaultStyle,
|
style: defaultStyle,
|
||||||
center: [30.5234, 50.4501],
|
center: [30.5234, 50.4501],
|
||||||
zoom: 5,
|
zoom: 5,
|
||||||
|
locale: mapUiLocale(),
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
renderWorldCopies: false,
|
renderWorldCopies: false,
|
||||||
trackResize: false,
|
trackResize: false,
|
||||||
|
|
@ -1171,7 +1250,7 @@ export const Hooks = {
|
||||||
if (dateStyle !== "none") options.dateStyle = dateStyle
|
if (dateStyle !== "none") options.dateStyle = dateStyle
|
||||||
if (timeStyle !== "none") options.timeStyle = timeStyle
|
if (timeStyle !== "none") options.timeStyle = timeStyle
|
||||||
|
|
||||||
this.el.textContent = new Intl.DateTimeFormat(undefined, options).format(date)
|
this.el.textContent = new Intl.DateTimeFormat(uiLocale(), options).format(date)
|
||||||
this.el.title = date.toISOString()
|
this.el.title = date.toISOString()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1481,6 +1560,8 @@ export const Hooks = {
|
||||||
const fallback = document.createElement("div")
|
const fallback = document.createElement("div")
|
||||||
fallback.className = "map-unavailable-panel"
|
fallback.className = "map-unavailable-panel"
|
||||||
fallback.dataset.mapFallback = "true"
|
fallback.dataset.mapFallback = "true"
|
||||||
|
fallback.setAttribute("role", "region")
|
||||||
|
fallback.setAttribute("aria-label", mapRegionLabel())
|
||||||
|
|
||||||
const message = document.createElement("p")
|
const message = document.createElement("p")
|
||||||
message.textContent = this.mapElement.dataset.mapUnavailableLabel
|
message.textContent = this.mapElement.dataset.mapUnavailableLabel
|
||||||
|
|
@ -1794,6 +1875,7 @@ export const Hooks = {
|
||||||
? [initialCoordinates.longitude, initialCoordinates.latitude]
|
? [initialCoordinates.longitude, initialCoordinates.latitude]
|
||||||
: [30.5234, 50.4501],
|
: [30.5234, 50.4501],
|
||||||
zoom: initialCoordinates ? 13 : 5,
|
zoom: initialCoordinates ? 13 : 5,
|
||||||
|
locale: mapUiLocale(),
|
||||||
attributionControl: true,
|
attributionControl: true,
|
||||||
dragRotate: false,
|
dragRotate: false,
|
||||||
pitchWithRotate: false
|
pitchWithRotate: false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import AxeBuilder from "@axe-core/playwright";
|
import AxeBuilder from "@axe-core/playwright";
|
||||||
import { expect, test } from "@playwright/test";
|
import { expect, test } from "@playwright/test";
|
||||||
import { newIsolatedContext } from "./helpers";
|
import { newIsolatedContext, projectEmail, registerAndConfirm } from "./helpers";
|
||||||
|
|
||||||
const publicAccessibilityPaths = [
|
const publicAccessibilityPaths = [
|
||||||
"/",
|
"/",
|
||||||
|
|
@ -111,3 +111,42 @@ test("public navigation does not overflow narrow or wide viewports", async ({ br
|
||||||
await context.close();
|
await context.close();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("authenticated core workflows remain accessible on a phone viewport", async ({
|
||||||
|
browser,
|
||||||
|
request,
|
||||||
|
}, testInfo) => {
|
||||||
|
const user = await registerAndConfirm(
|
||||||
|
browser,
|
||||||
|
request,
|
||||||
|
projectEmail("mobile-accessibility", testInfo.project.name),
|
||||||
|
"Mobile accessibility",
|
||||||
|
);
|
||||||
|
|
||||||
|
await user.page.setViewportSize({ width: 390, height: 844 });
|
||||||
|
|
||||||
|
for (const path of [
|
||||||
|
"/requests",
|
||||||
|
"/requests/new",
|
||||||
|
"/activities",
|
||||||
|
"/notifications",
|
||||||
|
"/profile",
|
||||||
|
"/users/settings",
|
||||||
|
"/support/requests",
|
||||||
|
"/reports",
|
||||||
|
]) {
|
||||||
|
await user.page.goto(path);
|
||||||
|
await expectAccessible(user.page);
|
||||||
|
|
||||||
|
const dimensions = await user.page.evaluate(() => ({
|
||||||
|
clientWidth: document.documentElement.clientWidth,
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(dimensions.scrollWidth, `${path} overflowed at 390px`).toBeLessThanOrEqual(
|
||||||
|
dimensions.clientWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.context.close();
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import {
|
||||||
newIsolatedContext,
|
newIsolatedContext,
|
||||||
projectEmail,
|
projectEmail,
|
||||||
registerAndConfirm,
|
registerAndConfirm,
|
||||||
|
waitForMapReady,
|
||||||
} from "./helpers";
|
} from "./helpers";
|
||||||
|
|
||||||
test("public language selection persists across pages", async ({ browser }) => {
|
test("public language selection persists across pages", async ({ browser }) => {
|
||||||
|
|
@ -46,5 +47,25 @@ test("authenticated LiveView uses the selected browser language", async ({
|
||||||
await expect(user.page.getByRole("heading", { name: "Хто потребує допомоги?" })).toBeVisible();
|
await expect(user.page.getByRole("heading", { name: "Хто потребує допомоги?" })).toBeVisible();
|
||||||
await expect(user.page.getByRole("link", { name: "Створити термінову заявку" })).toBeVisible();
|
await expect(user.page.getByRole("link", { name: "Створити термінову заявку" })).toBeVisible();
|
||||||
|
|
||||||
|
await gotoLiveView(user.page, "/notifications");
|
||||||
|
await expect(user.page.getByRole("heading", { name: "Сповіщення" })).toBeVisible();
|
||||||
|
await expect(user.page.getByText("0 непрочитаних сповіщень")).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
user.page.getByText(
|
||||||
|
"Тут з’являться оновлення заявок, повідомлень і сповіщень поблизу.",
|
||||||
|
),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await gotoLiveView(user.page, "/activities");
|
||||||
|
await waitForMapReady(user.page);
|
||||||
|
await expect(user.page.getByRole("region", { name: "Карта" })).toBeVisible();
|
||||||
|
|
||||||
|
if ((await user.page.locator("#activity-map canvas").count()) > 0) {
|
||||||
|
await expect(user.page.getByRole("button", { name: "Збільшити масштаб" })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
user.page.getByRole("button", { name: "Знайти моє місцезнаходження" }),
|
||||||
|
).toBeVisible();
|
||||||
|
}
|
||||||
|
|
||||||
await user.context.close();
|
await user.context.close();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ defmodule WhoNeedHelpWeb.ProfileLive do
|
||||||
<aside class="space-y-5">
|
<aside class="space-y-5">
|
||||||
<div class="rounded-3xl bg-success p-6 text-success-content">
|
<div class="rounded-3xl bg-success p-6 text-success-content">
|
||||||
<h2 class="font-bold">{gettext("Public reputation")}</h2>
|
<h2 class="font-bold">{gettext("Public reputation")}</h2>
|
||||||
<p class="mt-1 text-xs opacity-75">
|
<p class="mt-1 text-xs">
|
||||||
{gettext(
|
{gettext(
|
||||||
"These are activity signals, not an identity, background, qualification, or safety check."
|
"These are activity signals, not an identity, background, qualification, or safety check."
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -122,7 +122,7 @@ defmodule WhoNeedHelpWeb.PublicProfileLive do
|
||||||
<aside class="space-y-6">
|
<aside class="space-y-6">
|
||||||
<section class="rounded-3xl bg-success p-6 text-success-content">
|
<section class="rounded-3xl bg-success p-6 text-success-content">
|
||||||
<h2 class="font-bold">{gettext("Public reputation")}</h2>
|
<h2 class="font-bold">{gettext("Public reputation")}</h2>
|
||||||
<p class="mt-1 text-xs opacity-75">
|
<p class="mt-1 text-xs">
|
||||||
{gettext(
|
{gettext(
|
||||||
"These are activity signals, not an identity, background, qualification, or safety check."
|
"These are activity signals, not an identity, background, qualification, or safety check."
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -558,7 +558,7 @@ defmodule WhoNeedHelpWeb.RequestLive.Index do
|
||||||
{gettext("Load more in this area")}
|
{gettext("Load more in this area")}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<p class="flex items-start gap-2 rounded-2xl bg-base-200 p-4 text-xs text-base-content/60">
|
<p class="flex items-start gap-2 rounded-2xl bg-base-200 p-4 text-xs text-base-content/70">
|
||||||
<.icon name="hero-eye-slash" class="mt-0.5 size-4 shrink-0" />
|
<.icon name="hero-eye-slash" class="mt-0.5 size-4 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{gettext(
|
{gettext(
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
46
test/who_need_help_web/gettext_catalog_test.exs
Normal file
46
test/who_need_help_web/gettext_catalog_test.exs
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
defmodule WhoNeedHelpWeb.GettextCatalogTest do
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
@locales ~w(ru uk)
|
||||||
|
@domains ~w(default errors)
|
||||||
|
|
||||||
|
test "supported translated catalogs contain no fuzzy or empty messages" do
|
||||||
|
for locale <- @locales, domain <- @domains do
|
||||||
|
path = "priv/gettext/#{locale}/LC_MESSAGES/#{domain}.po"
|
||||||
|
catalog = Expo.PO.parse_file!(path)
|
||||||
|
|
||||||
|
for message <- catalog.messages, not message.obsolete do
|
||||||
|
refute Expo.Message.has_flag?(message, "fuzzy"),
|
||||||
|
"#{path} has a fuzzy translation for #{inspect(message.msgid)}"
|
||||||
|
|
||||||
|
source_variables = variables(message.msgid)
|
||||||
|
|
||||||
|
message
|
||||||
|
|> translations()
|
||||||
|
|> Enum.each(fn translation ->
|
||||||
|
refute empty?(translation),
|
||||||
|
"#{path} has an empty translation for #{inspect(message.msgid)}"
|
||||||
|
|
||||||
|
assert variables(translation) == source_variables,
|
||||||
|
"#{path} changes interpolation variables for #{inspect(message.msgid)}"
|
||||||
|
end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translations(%{msgstr: translations}) when is_map(translations) do
|
||||||
|
Map.values(translations)
|
||||||
|
end
|
||||||
|
|
||||||
|
defp translations(%{msgstr: translation}), do: [translation]
|
||||||
|
|
||||||
|
defp empty?(parts), do: parts |> IO.iodata_to_binary() |> String.trim() == ""
|
||||||
|
|
||||||
|
defp variables(parts) do
|
||||||
|
parts
|
||||||
|
|> IO.iodata_to_binary()
|
||||||
|
|> then(&Regex.scan(~r/%{[a-zA-Z0-9_]+}/, &1))
|
||||||
|
|> List.flatten()
|
||||||
|
|> MapSet.new()
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue
Block a user