diff --git a/CHANGELOG.md b/CHANGELOG.md index c6bc0f8..6e238fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,9 @@ See [keep a changelog](https://keepachangelog.com/en/1.0.0/) for information abo ## [Unreleased] +- [PR-39](https://github.com/itk-dev/aapodwalk_api/pull/39) + Changed distance and duration to integer values. + Added "Value with unit" field - [PR-38](https://github.com/itk-dev/aapodwalk_api/pull/38) Added mailer config - [PR-34](https://github.com/itk-dev/aapodwalk_api/pull/34) diff --git a/Taskfile.yml b/Taskfile.yml index f737b7f..503c480 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -137,3 +137,14 @@ tasks: update-translations: cmds: - task composer -- update-translations + - + cmd: | + BOLD='\033[1m' + RESET='\033[0m' + echo && echo -e "${BOLD}Note: You may have to run this command again to make some markup in translations fall into place.${RESET}" && echo + silent: true + + update-api-spec: + cmds: + - task composer -- update-api-spec + silent: true diff --git a/assets/styles/admin.css b/assets/styles/admin.css index a3df6f6..f46c5b7 100644 --- a/assets/styles/admin.css +++ b/assets/styles/admin.css @@ -82,3 +82,8 @@ white-space: nowrap; } } + +/* Make the unit take up a little less space than the numeric value. */ +.field-value-with-unit.form-group input.form-control { + width: 70%; +} diff --git a/fixtures/route.yml b/fixtures/route.yml index 14fd5be..1226d89 100644 --- a/fixtures/route.yml +++ b/fixtures/route.yml @@ -3,17 +3,26 @@ App\Entity\Route: route_pizza: name: 'Pizzaruten' description: 'Denne rute er overordentligt fokuseret på pizza' - distance: '3 meter' + distance: 3 imageFile: - total_duration: '16' + total_duration: 960 # 16 minutes (16 * 60) tags: ['@pizza', '@mad'] createdBy: '@user' route_havnen: name: 'Havneruten' description: 'Denne rute er overordentligt fokuseret på havnen' - distance: '103 meter' + distance: 103 imageFile: - total_duration: '25' + total_duration: 1500 # 25 minutes (25 * 60) + tags: ['@mad', '@arkitektur'] + createdBy: '@another-user' + + route_long: + name: 'Den lange rute' + description: 'Denne rute meget lang' + distance: 12345 + imageFile: + total_duration: 9000 # 2.5 hours (2.5 * 60 * 60) tags: ['@mad', '@arkitektur'] createdBy: '@another-user' diff --git a/migrations/Version20241218100641.php b/migrations/Version20241218100641.php new file mode 100644 index 0000000..0026494 --- /dev/null +++ b/migrations/Version20241218100641.php @@ -0,0 +1,80 @@ +addSql(<<<'SQL' +UPDATE + route +SET + distance = + CAST( + -- Convert string to decimal value in three steps: + -- + -- 1. Convert comma (assumed used as decimal separator) to dot (.) + -- 2. Remove all characters that are not a digit or a dot + -- 3. Convert to decimal + + -- (3) + CAST( + -- (2) + REGEXP_REPLACE( + -- (1) + REPLACE(distance, ',', '.'), + '[^[:digit:].]+', + '' + ) + AS DECIMAL(16, 8) + ) + -- 4. Multiple by scale to convert to meters. If value contains 'km' use 1000 as scale. Otherwise, use 1. + * (SELECT CASE WHEN distance LIKE '%km%' THEN 1000 ELSE 1 END) + AS INT), + total_duration = + CAST( + -- Convert string to decimal value in three steps: + -- + -- 1. Convert comma (assumed used as decimal separator) to dot (.) + -- 2. Remove all characters that are not a digit or a dot + -- 3. Convert to decimal + + -- (3) + CAST( + -- (2) + REGEXP_REPLACE( + -- (1) + REPLACE(total_duration, ',', '.'), + '[^[:digit:].]+', + '' + ) + AS DECIMAL(16, 8) + ) + -- 4. Multiple by scale to convert to seconds. If value contains 'time' use 3600 as scale. Otherwise, use 60. + * (SELECT CASE WHEN total_duration LIKE '%time%' THEN 60 * 60 ELSE 60 END) + AS INT) +SQL); + $this->addSql('ALTER TABLE route CHANGE distance distance INT NOT NULL, CHANGE total_duration total_duration INT NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE route CHANGE distance distance VARCHAR(255) NOT NULL, CHANGE total_duration total_duration VARCHAR(255) NOT NULL'); + } +} diff --git a/public/api-spec-v1.json b/public/api-spec-v1.json index 688a2c6..6322468 100644 --- a/public/api-spec-v1.json +++ b/public/api-spec-v1.json @@ -442,6 +442,7 @@ "type": "string" }, "proximityToUnlock": { + "description": "Proximity to unlock in meters.", "type": [ "integer", "null" @@ -542,6 +543,7 @@ "type": "string" }, "proximityToUnlock": { + "description": "Proximity to unlock in meters.", "type": [ "integer", "null" @@ -594,7 +596,8 @@ "type": "string" }, "distance": { - "type": "string" + "description": "Distance in meters.", + "type": "integer" }, "image": { "type": [ @@ -609,7 +612,8 @@ } }, "totalDuration": { - "type": "string" + "description": "Total duration in seconds.", + "type": "integer" }, "points": { "type": "array", @@ -670,7 +674,8 @@ "type": "string" }, "distance": { - "type": "string" + "description": "Distance in meters.", + "type": "integer" }, "image": { "type": [ @@ -685,7 +690,8 @@ } }, "totalDuration": { - "type": "string" + "description": "Total duration in seconds.", + "type": "integer" }, "points": { "type": "array", diff --git a/public/api-spec-v1.yaml b/public/api-spec-v1.yaml index aac4152..cf7d8d5 100644 --- a/public/api-spec-v1.yaml +++ b/public/api-spec-v1.yaml @@ -208,6 +208,7 @@ components: longitude: type: string proximityToUnlock: + description: 'Proximity to unlock in meters.' type: - integer - 'null' @@ -278,6 +279,7 @@ components: longitude: type: string proximityToUnlock: + description: 'Proximity to unlock in meters.' type: - integer - 'null' @@ -316,7 +318,8 @@ components: description: type: string distance: - type: string + description: 'Distance in meters.' + type: integer image: type: - string @@ -326,7 +329,8 @@ components: items: $ref: '#/components/schemas/Tag-read' totalDuration: - type: string + description: 'Total duration in seconds.' + type: integer points: type: array items: @@ -367,7 +371,8 @@ components: description: type: string distance: - type: string + description: 'Distance in meters.' + type: integer image: type: - string @@ -377,7 +382,8 @@ components: items: $ref: '#/components/schemas/Tag.jsonld-read' totalDuration: - type: string + description: 'Total duration in seconds.' + type: integer points: type: array items: diff --git a/src/Admin/Field/ValueWithUnitField.php b/src/Admin/Field/ValueWithUnitField.php new file mode 100644 index 0000000..5bea65f --- /dev/null +++ b/src/Admin/Field/ValueWithUnitField.php @@ -0,0 +1,31 @@ +setProperty($propertyName) + ->setLabel($label) + + // this template is used in 'index' and 'detail' pages + ->setTemplatePath('admin/field/value_with_unit.html.twig') + + // this is used in 'edit' and 'new' pages to edit the field contents + // you can use your own form types too + ->setFormType(ValueWithUnitType::class) + ->addCssClass('field-value-with-unit'); + } +} diff --git a/src/Controller/Admin/PointOfInterestCrudController.php b/src/Controller/Admin/PointOfInterestCrudController.php index 60ad309..0d9077a 100644 --- a/src/Controller/Admin/PointOfInterestCrudController.php +++ b/src/Controller/Admin/PointOfInterestCrudController.php @@ -3,10 +3,12 @@ namespace App\Controller\Admin; use App\Admin\Field\LocationField; +use App\Admin\Field\ValueWithUnitField; use App\Entity\PointOfInterest; use App\Entity\Role; use App\Entity\Route; use App\Field\VichImageField; +use App\Form\ValueWithUnitType; use App\Service\EasyAdminHelper; use EasyCorp\Bundle\EasyAdminBundle\Config\Crud; use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField; @@ -91,8 +93,15 @@ public function configureFields(string $pageName): iterable ->setRequired(true) ->setVirtual(true)->setColumns(12); - yield NumberField::new('proximityToUnlock', new TranslatableMessage('Proximity to unlock', [], 'admin')) - ->setHelp(new TranslatableMessage('The proximity that allows unlocking this point of interest (in m).', [], 'admin'))->setColumns(12); + yield ValueWithUnitField::new('proximityToUnlock', new TranslatableMessage('Proximity to unlock', [], 'admin')) + ->setFormTypeOption('units', [ + 'm' => [ + ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('meter', [], 'admin'), + ValueWithUnitType::OPTION_SCALE => 1, + ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'), + ], + ]) + ->setHelp(new TranslatableMessage('The proximity that allows unlocking this point of interest.', [], 'admin'))->setColumns(12); yield DateField::new('createdAt', new TranslatableMessage('Created at', [], 'admin'))->hideOnForm(); yield DateField::new('updatedAt', new TranslatableMessage('Updated at', [], 'admin'))->hideOnForm(); diff --git a/src/Controller/Admin/RouteCrudController.php b/src/Controller/Admin/RouteCrudController.php index f20dcca..a354261 100644 --- a/src/Controller/Admin/RouteCrudController.php +++ b/src/Controller/Admin/RouteCrudController.php @@ -2,9 +2,11 @@ namespace App\Controller\Admin; +use App\Admin\Field\ValueWithUnitField; use App\Entity\Role; use App\Entity\Route; use App\Field\VichImageField; +use App\Form\ValueWithUnitType; use App\Service\AppManager; use App\Service\EasyAdminHelper; use EasyCorp\Bundle\EasyAdminBundle\Config\Action; @@ -77,10 +79,37 @@ public function configureFields(string $pageName): iterable yield VichImageField::new('image')->setColumns(6); } - yield TextField::new('distance', new TranslatableMessage('Distance', [], 'admin'))->setColumns(6) - ->setHelp(new TranslatableMessage('The distance should be how far the route is with all points of interests included, e.g. "840m"', [], 'admin')); - yield TextField::new('totalDuration', new TranslatableMessage('Total duration', [], 'admin'))->setColumns(6) - ->setHelp(new TranslatableMessage('The total duration of the route, i.e. the total duration of audio tracks in the route plus the time needed for moving along the route.', [], 'admin')); + yield ValueWithUnitField::new('distance', new TranslatableMessage('Distance', [], 'admin')) + ->setFormTypeOption('units', [ + 'km' => [ + ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('kilometer', [], 'admin'), + ValueWithUnitType::OPTION_SCALE => 1000, + ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.km', [], 'admin'), + ], + 'm' => [ + ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('meter', [], 'admin'), + ValueWithUnitType::OPTION_SCALE => 1, + ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.m', [], 'admin'), + ], + ]) + ->setColumns(6) + ->setHelp(new TranslatableMessage('The total distance of the route with all points of interests included.', [], 'admin')); + + yield ValueWithUnitField::new('totalDuration', new TranslatableMessage('Total duration', [], 'admin')) + ->setFormTypeOption('units', [ + 'hour' => [ + ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('hours', [], 'admin'), + ValueWithUnitType::OPTION_SCALE => 60 * 60, + ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.hour', [], 'admin'), + ], + 'minute' => [ + ValueWithUnitType::OPTION_LABEL => new TranslatableMessage('minutes', [], 'admin'), + ValueWithUnitType::OPTION_SCALE => 60, + ValueWithUnitType::OPTION_LOCALIZED_UNIT => new TranslatableMessage('unit.minute', [], 'admin'), + ], + ]) + ->setColumns(6) + ->setHelp(new TranslatableMessage('The total duration of the route, i.e. the total duration of audio tracks in the route plus the time needed for moving along the route.', [], 'admin')); yield CollectionField::new('points', new TranslatableMessage('Points', [], 'admin'))->addCssClass('field-collection') ->setEntryIsComplex() diff --git a/src/Entity/PointOfInterest.php b/src/Entity/PointOfInterest.php index 4e5b144..798a2de 100644 --- a/src/Entity/PointOfInterest.php +++ b/src/Entity/PointOfInterest.php @@ -81,6 +81,9 @@ class PointOfInterest implements BlameableInterface, \JsonSerializable #[Groups(['read'])] private ?string $longitude = null; + /** + * Proximity to unlock in meters. + */ #[ORM\Column(length: 255, nullable: true)] #[Assert\NotBlank] #[Groups(['read'])] diff --git a/src/Entity/Route.php b/src/Entity/Route.php index 9d8fa0d..f582924 100644 --- a/src/Entity/Route.php +++ b/src/Entity/Route.php @@ -48,9 +48,12 @@ class Route implements BlameableInterface #[Groups(['read'])] private ?string $description = null; + /** + * Distance in meters. + */ #[ORM\Column(length: 255)] #[Groups(['read'])] - private ?string $distance = null; + private ?int $distance = null; #[ORM\Column(length: 255)] private ?string $image = null; @@ -73,9 +76,12 @@ class Route implements BlameableInterface #[Groups(['read'])] private Collection $tags; + /** + * Total duration in seconds. + */ #[ORM\Column(length: 255)] #[Groups(['read'])] - private ?string $totalDuration = null; + private ?int $totalDuration = null; /** * @var Collection @@ -125,12 +131,12 @@ public function setDescription(string $description): static return $this; } - public function getDistance(): ?string + public function getDistance(): ?int { return $this->distance; } - public function setDistance(string $distance): static + public function setDistance(int $distance): static { $this->distance = $distance; @@ -201,12 +207,12 @@ public function getImageFile(): ?File return $this->imageFile; } - public function getTotalDuration(): ?string + public function getTotalDuration(): ?int { return $this->totalDuration; } - public function setTotalDuration(string $totalDuration): static + public function setTotalDuration(int $totalDuration): static { $this->totalDuration = $totalDuration; diff --git a/src/Form/ValueWithUnitType.php b/src/Form/ValueWithUnitType.php new file mode 100644 index 0000000..d8bb7b6 --- /dev/null +++ b/src/Form/ValueWithUnitType.php @@ -0,0 +1,146 @@ + + */ +final class ValueWithUnitType extends AbstractType +{ + public const string FIELD_VALUE = 'value'; + public const string FIELD_UNIT = 'unit'; + + public const string OPTION_LABEL = 'label'; + public const string OPTION_SCALE = 'scale'; + public const string OPTION_LOCALIZED_UNIT = 'localized_unit'; + + private const int SCALE = 1; + + private \NumberFormatter $numberFormatter; + + public function __construct( + private readonly TranslatorInterface $translator, + LocaleSwitcher $localeSwitcher, + ) { + $this->numberFormatter = new \NumberFormatter($localeSwitcher->getLocale(), \NumberFormatter::DECIMAL); + $this->numberFormatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, self::SCALE); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $units = array_keys($options['units']); + $unitChoices = array_combine($units, $units); + $builder + ->add(self::FIELD_VALUE, NumberType::class, [ + 'scale' => self::SCALE, + ]) + ->add(self::FIELD_UNIT, ChoiceType::class, [ + 'choices' => $unitChoices, + 'choice_label' => function ($choice, string $key, mixed $value) use ($options): TranslatableMessage|string { + return $options['units'][$key]['label'] ?? $key; + }, + ]); + + $builder + ->addModelTransformer(new CallbackTransformer( + fn ($value) => $this->transform($value, $options), + fn ($value) => $this->reverseTransform($value, $options), + )) + ; + } + + public function transform(?int $value, array $options): array + { + try { + return $this->getMatchingUnit($value, $options); + } catch (\Exception $exception) { + throw new TransformationFailedException(invalidMessage: 'Error transforming value: {value}.', invalidMessageParameters: ['value' => $value, /* @todo Make this work! */ 'translation_domain' => 'admin']); + } + } + + public function reverseTransform(array $values, array $options): int + { + [self::FIELD_VALUE => $value, self::FIELD_UNIT => $unit] = $values; + $units = $this->getUnits($options); + $info = $units[$unit] ?? null; + + if (null === $info) { + throw new TransformationFailedException(invalidMessage: 'Invalid unit: {unit}.', invalidMessageParameters: ['unit' => $unit, /* @todo Make this work! */ 'translation_domain' => 'admin']); + } + + return $value * $info[self::OPTION_SCALE]; + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver + ->setRequired('units') + ->setAllowedTypes('units', 'array') + ->setDefault('units', function (OptionsResolver $resolver): void { + $resolver + ->setPrototype(true) + ->setDefault(self::OPTION_SCALE, 1) + ->setAllowedTypes(self::OPTION_SCALE, 'int') + ->setAllowedValues(self::OPTION_SCALE, Validation::createIsValidCallable( + new Positive(), + )) + ->setRequired(self::OPTION_LABEL) + ->setAllowedTypes(self::OPTION_LABEL, ['string', TranslatableMessage::class]) + ->setRequired(self::OPTION_LOCALIZED_UNIT); + }) + // Make units required. + ->setAllowedValues('units', static fn (array $value) => !empty($value)) + ; + } + + /** + * Get units sorted descending by scale. + */ + private function getUnits(array $options): array + { + $units = $options['units']; + + uasort($units, static fn ($a, $b) => -($a[self::OPTION_SCALE] <=> $b[self::OPTION_SCALE])); + + return $units; + } + + public function getMatchingUnit(?int $value, array $options): array + { + $units = $this->getUnits($options); + foreach ($units as $unit => $info) { + $scale = $info[self::OPTION_SCALE]; + if ($value >= $scale || array_key_last($units) === $unit) { + return [ + self::FIELD_VALUE => null === $value ? null : ($scale > 1 ? $value / $scale : $value), + self::FIELD_UNIT => $unit, + self::OPTION_LOCALIZED_UNIT => $info[self::OPTION_LOCALIZED_UNIT], + ]; + } + } + + throw new \RuntimeException('This should never be called.'); + } + + public function getFormattedValue(int $value, array $options): string + { + $unit = $this->getMatchingUnit($value, $options); + + // @todo There must be a better way to do this! + return sprintf('%s %s', $this->numberFormatter->format($unit['value']), + $unit[self::OPTION_LOCALIZED_UNIT]->trans($this->translator)); + } +} diff --git a/src/Twig/Extension/AppExtension.php b/src/Twig/Extension/AppExtension.php index 0f2f7e7..627354b 100644 --- a/src/Twig/Extension/AppExtension.php +++ b/src/Twig/Extension/AppExtension.php @@ -13,6 +13,7 @@ public function getFunctions(): array return [ new TwigFunction('get_media_embed_code', [AppExtensionRuntime::class, 'getMediaEmbedCode']), new TwigFunction('get_media_templates', [AppExtensionRuntime::class, 'getMediaTemplates']), + new TwigFunction('format_value_with_unit', [AppExtensionRuntime::class, 'formatValueWithUnit']), ]; } } diff --git a/src/Twig/Runtime/AppExtensionRuntime.php b/src/Twig/Runtime/AppExtensionRuntime.php index e40b7f8..f854714 100644 --- a/src/Twig/Runtime/AppExtensionRuntime.php +++ b/src/Twig/Runtime/AppExtensionRuntime.php @@ -2,14 +2,18 @@ namespace App\Twig\Runtime; +use App\Admin\Field\ValueWithUnitField; use App\Entity\PointOfInterest; +use App\Form\ValueWithUnitType; use App\Service\MediaProcessorInterface; +use EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto; use Twig\Extension\RuntimeExtensionInterface; class AppExtensionRuntime implements RuntimeExtensionInterface { public function __construct( private readonly MediaProcessorInterface $mediaProcessor, + private readonly ValueWithUnitType $valueWithUnitType, ) { } @@ -30,4 +34,16 @@ public function getMediaTemplates(): array { return $this->mediaProcessor->getTemplates(); } + + public function formatValueWithUnit(FieldDto $field): string + { + if (ValueWithUnitField::class !== $field->getFieldFqcn()) { + throw new \InvalidArgumentException(sprintf("Field's FQCN must be %s (or a subclass). Found %s.", ValueWithUnitField::class, $field->getFieldFqcn() ?? '')); + } + if (ValueWithUnitType::class !== $field->getFormType()) { + throw new \InvalidArgumentException(sprintf("Field's form type must be %s. Found %s.", ValueWithUnitType::class, $field->getFormType() ?? '')); + } + + return $this->valueWithUnitType->getFormattedValue($field->getValue(), $field->getFormTypeOptions()); + } } diff --git a/templates/admin/_messages.html.twig b/templates/admin/_messages.html.twig index b649699..5ff2cda 100644 --- a/templates/admin/_messages.html.twig +++ b/templates/admin/_messages.html.twig @@ -11,3 +11,5 @@ {# src/Form/LocationType.php #} {{ 'The value must contain two numbers separated by comma.'|trans }} {{ '{value} is not a valid number'|trans }} + +{{ 'Please enter a number.'|trans({}, 'validators') }} diff --git a/templates/admin/field/value_with_unit.html.twig b/templates/admin/field/value_with_unit.html.twig new file mode 100644 index 0000000..b6fdfff --- /dev/null +++ b/templates/admin/field/value_with_unit.html.twig @@ -0,0 +1 @@ +{{ format_value_with_unit(field) }} diff --git a/templates/admin/form.html.twig b/templates/admin/form.html.twig index 5d4fa22..e433367 100644 --- a/templates/admin/form.html.twig +++ b/templates/admin/form.html.twig @@ -168,3 +168,53 @@ {% endif %} {% endblock %} + +{# Adapted from block form_row in parent template #} +{% block value_with_unit_row %} + {% set row_attr = row_attr|merge({ + class: row_attr.class|default('') ~ ' form-group', + }) %} + {% set field = form.vars.ea_vars.field %} + +
+ {%- set row_class = row_class|default(row_attr.class|default('mb-3')) -%} +
+ {{- form_label(form) -}} +
+ {% set has_prepend_html = field.prepend_html|default(null) is not null %} + {# % set has_append_html = field.append_html|default(null) is not null % #} + {% set has_append_html = true %} + {% set has_input_groups = has_prepend_html or has_append_html %} + + {% if has_input_groups %}
{% endif %} + {% if has_prepend_html %} +
+ {{ field.prepend_html|raw }} +
+ {% endif %} + + {{ form_widget(form.value) }} + + {# % if has_append_html % #} + {# {{ field.append_html|raw }} #} + {{ form_widget(form.unit, {attr: {_class: 'x-input-group-addon'}}) }} + {# % endif % #} + {% if has_input_groups %}
{% endif %} + + {% if field.help ?? false %} + {{ field.help|trans(label_translation_parameters, translation_domain)|raw }} + {% elseif form.vars.help ?? false %} + {{ form.vars.help|trans(form.vars.help_translation_parameters, form.vars.translation_domain)|raw }} + {% endif %} + + {{- form_errors(form.value) -}} + {{- form_errors(form.unit) -}} +
+
+
+ + {# if a field doesn't define its columns explicitly, insert a fill element to make the field take the entire row space #} + {% if field.columns|default(null) is null %} +
+ {% endif %} +{% endblock %} diff --git a/translations/admin+intl-icu.da.xlf b/translations/admin+intl-icu.da.xlf index 8fb52ab..4fd85ed 100644 --- a/translations/admin+intl-icu.da.xlf +++ b/translations/admin+intl-icu.da.xlf @@ -5,10 +5,6 @@ - - The distance should be how far the route is with all points of interests included, e.g. "840m" - Distancen skal angive hvor lang hele ruten inkl. seværdigheder er, fx "840m” - Tags are used in the frontend to organize the routes. Tags bruges til at organisere ruter i app’en. @@ -25,10 +21,6 @@ A text version of the podcast, for people with hearing disabilities. En tekstuel udgave af podcasten til mennesker med hørenedsættelse. - - The proximity that allows unlocking this point of interest (in m). - Nærheden for at afspille seværdigheden (i meter). - Users mail address, which is also used as login name E-mailadresse som også bruges som brugernavn @@ -309,6 +301,46 @@ New password Ny adgangskode + + unit.km + km + + + unit.m + m + + + unit.hour + time + + + unit.minute + minut + + + kilometer + kilometer + + + meter + meter + + + The total distance of the route with all points of interests included. + Den samlede længde af ruten inklusive alle punkter. + + + hours + timer + + + minutes + minutter + + + The proximity that allows unlocking this point of interest. + Nærheden for at afspille seværdigheden. + diff --git a/translations/admin+intl-icu.en.xlf b/translations/admin+intl-icu.en.xlf index 3d87d5d..837a6e2 100644 --- a/translations/admin+intl-icu.en.xlf +++ b/translations/admin+intl-icu.en.xlf @@ -5,10 +5,6 @@ - - The distance should be how far the route is with all points of interests included, e.g. "840m" - The distance should be how far the route is with all points of interests included, e.g. "840m" - Tags are used in the frontend to organize the routes. Tags are used in the frontend to organize the routes. @@ -25,10 +21,6 @@ A text version of the podcast, for people with hearing disabilities. A text version of the podcast, for people with hearing disabilities. - - The proximity that allows unlocking this point of interest (in m). - The proximity that allows unlocking this point of interest (in m). - Users mail address, which is also used as login name Users mail address, which is also used as login name @@ -309,6 +301,46 @@ New password New password + + unit.km + km + + + unit.m + m + + + unit.hour + hour + + + unit.minute + minute + + + kilometer + kilometer + + + meter + meter + + + The total distance of the route with all points of interests included. + The total distance of the route with all points of interests included. + + + hours + hours + + + minutes + minutes + + + The proximity that allows unlocking this point of interest. + The proximity that allows unlocking this point of interest. + diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index be35a8f..499a0e4 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -9,6 +9,10 @@ Error Fejl + + Please enter a number. + Indtast venligst et tal. + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 2c90883..9c569b5 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -9,6 +9,10 @@ Error Error + + Please enter a number. + Please enter a number. +