Compare commits

..

7 Commits

Author SHA1 Message Date
Kevin Papst
2d809b4f06 Release 1.30.8 (#3854)
* fix weekly timesheets can delete wrong records
* pin phpunit to prevent deprecations
2023-02-17 21:06:05 +01:00
Kevin Papst
ced1883b6f activate deprecation logs in dev and test 2023-02-09 13:02:33 +01:00
Kevin Papst
2e54a9a267 bump version to 1.30.7 2023-02-09 13:01:36 +01:00
Kevin Papst
57722f25fd fix call on null 2023-02-06 15:57:30 +01:00
Kevin Papst
539a7e280c clarify requirement in option description 2023-02-06 15:56:48 +01:00
Kevin Papst
db4179df19 release 1.30.5 (#3783) 2023-01-20 12:55:50 +01:00
Kevin Papst
7ea3eb1e3c Release 1.30.4 (#3770)
* prevent empty invoice number
* prevent broken invoice filename
* fix recent timesheets for user
* allow to upload twig invoice template
2023-01-19 15:13:44 +01:00
50 changed files with 724 additions and 534 deletions

View File

@@ -86,8 +86,8 @@
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-doctrine": "^1.0",
"phpstan/phpstan-phpunit": "^1.0",
"phpstan/phpstan-symfony": "^1.0",
"phpunit/phpunit": "^9.0",
"phpstan/phpstan-symfony": "1.2.18",
"phpunit/phpunit": "9.5.*",
"symfony/browser-kit": "^4.4",
"symfony/css-selector": "^4.4",
"symfony/debug-bundle": "^4.4",

945
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,3 +17,11 @@ monolog:
type: console
process_psr_3_messages: false
channels: ["!event", "!doctrine", "!console"]
deprecation:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
deprecation_filter:
type: filter
handler: deprecation
max_level: info
channels: ["php"]

View File

@@ -5,3 +5,11 @@ monolog:
path: "%kernel.logs_dir%/%kernel.environment%.log"
level: debug
channels: ["!event"]
deprecation:
type: stream
path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log"
deprecation_filter:
type: filter
handler: deprecation
max_level: info
channels: ["php"]

View File

@@ -21,6 +21,7 @@ use App\Form\API\TimesheetApiEditForm;
use App\Repository\Query\TimesheetQuery;
use App\Repository\TagRepository;
use App\Repository\TimesheetRepository;
use App\Repository\UserRepository;
use App\Timesheet\TimesheetService;
use App\Timesheet\TrackingMode\TrackingModeInterface;
use App\Utils\SearchTerm;
@@ -75,19 +76,22 @@ class TimesheetController extends BaseApiController
* @var TimesheetService
*/
private $service;
private $userRepository;
public function __construct(
ViewHandlerInterface $viewHandler,
TimesheetRepository $repository,
TagRepository $tagRepository,
EventDispatcherInterface $dispatcher,
TimesheetService $service
TimesheetService $service,
UserRepository $userRepository
) {
$this->viewHandler = $viewHandler;
$this->repository = $repository;
$this->tagRepository = $tagRepository;
$this->dispatcher = $dispatcher;
$this->service = $service;
$this->userRepository = $userRepository;
}
protected function getTrackingMode(): TrackingModeInterface
@@ -491,9 +495,13 @@ class TimesheetController extends BaseApiController
if ($this->isGranted('view_other_timesheet') && null !== ($reqUser = $paramFetcher->get('user'))) {
if ('all' === $reqUser) {
$reqUser = null;
$user = null;
} else {
$user = $this->userRepository->getUserById($reqUser);
if ($user === null) {
throw $this->createNotFoundException('Unknown User ID');
}
}
$user = $reqUser;
}
if (null !== ($reqLimit = $paramFetcher->get('size'))) {

View File

@@ -104,6 +104,10 @@ class ActivityService
public function findActivityByName(string $name, ?Project $project = null): ?Activity
{
if ($project === null) {
return $this->repository->findOneBy(['project' => null, 'name' => $name]);
}
return $this->repository->findOneBy(['project' => $project->getId(), 'name' => $name]);
}
}

View File

@@ -76,7 +76,7 @@ class ExportCreateCommand extends Command
->addOption('customer', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Customer IDs to filter', null)
->addOption('project', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Project IDs to filter', null)
->addOption('team', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Team IDs to filter', null)
->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'User IDs to filter', null)
->addOption('user', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'User IDs to filter ("username" needs to be in the same Team)', null)
->addOption('set-exported', null, InputOption::VALUE_NONE, 'Whether the included items should be marked as exported (default: false)')
->addOption('template', null, InputOption::VALUE_REQUIRED, 'Export template', null)
->addOption('exported', null, InputOption::VALUE_OPTIONAL, 'Exported filter for export entries. By default only "not exported" items are fetched (possible values: exported, all)', null)

View File

@@ -1579,7 +1579,7 @@ final class KimaiImporterCommand extends Command
continue;
}
if (empty($oldRecord['end']) || $oldRecord['end'] === 0) {
if (empty($oldRecord['end'])) {
$io->error('Cannot import running timesheet record, skipping: ' . $oldRecord['timeEntryID']);
$failed++;
continue;

View File

@@ -17,11 +17,11 @@ class Constants
/**
* The current release version
*/
public const VERSION = '1.30.3';
public const VERSION = '1.30.8';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 13003;
public const VERSION_ID = 13008;
/**
* The current release status, either "stable" or "dev"
*/

View File

@@ -44,6 +44,7 @@ use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
use Twig\Environment;
/**
* Controller used to create invoices and manage invoice templates.
@@ -409,7 +410,7 @@ final class InvoiceController extends AbstractController
* @Route(path="/document_upload", name="admin_invoice_document_upload", methods={"GET", "POST"})
* @Security("is_granted('upload_invoice_template')")
*/
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository)
public function uploadDocumentAction(Request $request, string $projectDirectory, InvoiceDocumentRepository $documentRepository, Environment $twig, SystemConfiguration $systemConfiguration)
{
$dir = $documentRepository->getUploadDirectory();
$invoiceDir = $dir;
@@ -418,6 +419,7 @@ final class InvoiceController extends AbstractController
if ($invoiceDir[0] !== '/') {
$invoiceDir = $projectDirectory . DIRECTORY_SEPARATOR . $dir;
}
$invoiceDir = rtrim($invoiceDir, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$used = [];
foreach ($this->templateRepository->findAll() as $template) {
@@ -473,23 +475,56 @@ final class InvoiceController extends AbstractController
/** @var UploadedFile $uploadedFile */
$uploadedFile = $form->get('document')->getData();
$originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = transliterator_transliterate(
'Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()',
$originalFilename
);
$originalName = $uploadedFile->getClientOriginalName();
$safeFilename = null;
$extension = null;
$success = true;
$extension = $uploadedFile->guessExtension();
$allowed = InvoiceDocumentUploadForm::EXTENSIONS_NO_TWIG;
if ((bool) $systemConfiguration->find('invoice.upload_twig') === true) {
$allowed = InvoiceDocumentUploadForm::EXTENSIONS;
}
$newFilename = substr($safeFilename, 0, 20) . '.' . $extension;
foreach ($allowed as $ext) {
$len = \strlen($ext);
if (substr_compare($originalName, $ext, -$len) === 0) {
$extension = $ext;
$withoutExtension = str_replace($ext, '', $originalName);
$safeFilename = transliterator_transliterate(InvoiceDocumentUploadForm::FILENAME_RULE, $withoutExtension);
break;
}
}
try {
$uploadedFile->move($invoiceDir, $newFilename);
if ($safeFilename === null || $extension === null) {
$success = false;
$this->flashError('Invalid file given');
} else {
$newFilename = substr($safeFilename, 0, 20) . $extension;
try {
$uploadedFile->move($invoiceDir, $newFilename);
// if this is a twig file, we directly try to compile the template
if (stripos($newFilename, '.twig') !== false) {
try {
$twig->enableAutoReload();
$twig->load('@invoice/' . $newFilename);
$twig->disableAutoReload();
} catch (Exception $ex) {
unlink($invoiceDir . $newFilename);
$success = false;
$this->flashException($ex, 'File was deleted, as Twig template is broken: ' . $ex->getMessage());
}
}
} catch (Exception $ex) {
$this->flashException($ex, 'action.upload.error');
}
}
if ($success) {
$this->flashSuccess('action.update.success');
return $this->redirectToRoute('admin_invoice_document_upload');
} catch (Exception $ex) {
$this->flashException($ex, 'action.upload.error');
}
}
}

View File

@@ -495,6 +495,7 @@ final class SystemConfigurationController extends AbstractController
->setLabel('invoice.number_format')
->setRequired(true)
->setType(TextType::class)
->setConstraints([new NotBlank()])
->setTranslationDomain('system-configuration'),
(new Configuration())
->setName('invoice.simple_form')

View File

@@ -95,14 +95,12 @@ class TeamFixtures extends Fixture
$team->setName($faker->company . ' ' . $i);
$team->addTeamlead($allUsers[array_rand($allUsers)]);
if ($userCount > 0) {
$userKeys = array_rand($allUsers, $userCount);
if (!\is_array($userKeys)) {
$userKeys = [$userKeys];
}
foreach ($userKeys as $userKey) {
$team->addUser($allUsers[$userKey]);
}
$userKeys = array_rand($allUsers, $userCount);
if (!\is_array($userKeys)) {
$userKeys = [$userKeys];
}
foreach ($userKeys as $userKey) {
$team->addUser($allUsers[$userKey]);
}
if ($projectCount > 0) {

View File

@@ -333,6 +333,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('number_format')
->defaultValue('{Y}/{cy,3}')
->end()
->booleanNode('upload_twig')
->defaultTrue()
->end()
->end()
;

View File

@@ -437,12 +437,21 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
{
// only auto calculate if manually set duration is null - the result is important for eg. validations
if ($calculate && $this->duration === null && $this->begin !== null && $this->end !== null) {
return $this->end->getTimestamp() - $this->begin->getTimestamp();
return $this->getCalculatedDuration();
}
return $this->duration;
}
public function getCalculatedDuration(): ?int
{
if ($this->begin !== null && $this->end !== null) {
return $this->end->getTimestamp() - $this->begin->getTimestamp();
}
return null;
}
/**
* @param User $user
* @return Timesheet

View File

@@ -9,6 +9,7 @@
namespace App\Form;
use App\Configuration\SystemConfiguration;
use App\Repository\InvoiceDocumentRepository;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
@@ -21,11 +22,18 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
class InvoiceDocumentUploadForm extends AbstractType
{
private $repository;
public const EXTENSIONS = ['.html.twig', '.pdf.twig', '.docx', '.xlsx', '.ods'];
public const EXTENSIONS_NO_TWIG = ['.docx', '.xlsx', '.ods'];
public const FILENAME_RULE = 'Any-Latin; Latin-ASCII; [^A-Za-z0-9_\-] remove; Lower()';
public function __construct(InvoiceDocumentRepository $repository)
private $repository;
private $systemConfiguration;
private $extensions = [];
public function __construct(InvoiceDocumentRepository $repository, SystemConfiguration $systemConfiguration)
{
$this->repository = $repository;
$this->systemConfiguration = $systemConfiguration;
}
/**
@@ -33,22 +41,32 @@ class InvoiceDocumentUploadForm extends AbstractType
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$this->extensions = self::EXTENSIONS_NO_TWIG;
$extensions = 'DOCX, ODS, XLSX';
$mimetypes = [
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.oasis.opendocument.spreadsheet',
];
if ((bool) $this->systemConfiguration->find('invoice.upload_twig') === true) {
$this->extensions = self::EXTENSIONS;
$extensions = 'DOCX, ODS, XLSX, TWIG (PDF & HTML)';
$mimetypes = array_merge($mimetypes, [
'application/octet-stream', // needed for twig templates
'text/html', // needed for twig templates
'text/plain', // needed for twig templates
]);
}
$builder
->add('document', FileType::class, [
'label' => 'label.invoice_renderer',
'translation_domain' => 'invoice-renderer',
'help' => 'help.upload',
'help_translation_parameters' => ['%extensions%' => $extensions],
'mapped' => false,
'required' => true,
'attr' => [
'accept' => implode(',', $mimetypes)
],
'constraints' => [
new File([
'mimeTypes' => $mimetypes,
@@ -77,6 +95,48 @@ class InvoiceDocumentUploadForm extends AbstractType
->setTranslationDomain('validators')
->setCode('kimai-invoice-document-upload-01')
->addViolation();
return;
}
$extension = null;
$nameWithoutExtension = null;
foreach ($this->extensions as $ext) {
$len = \strlen($ext);
if (substr_compare($name, $ext, -$len) === 0) {
$extension = $ext;
$nameWithoutExtension = str_replace($ext, '', $name);
break;
}
}
if ($extension === null) {
$context->buildViolation('This invoice document cannot be used, allowed file extensions are: %extensions%')
->setParameters(['%extensions%' => implode(', ', $this->extensions)])
->setTranslationDomain('validators')
->setCode('kimai-invoice-document-upload-02')
->addViolation();
return;
}
$safeFilename = transliterator_transliterate(self::FILENAME_RULE, $nameWithoutExtension);
if ($safeFilename !== $nameWithoutExtension) {
$context->buildViolation('This invoice document cannot be used, filename may only contain the following ascii character: %character%')
->setParameters(['%character%' => 'A-Z a-z 0-9 _ -'])
->setTranslationDomain('validators')
->setCode('kimai-invoice-document-upload-03')
->addViolation();
}
if (mb_strlen($nameWithoutExtension) > 20) {
$context->buildViolation('This invoice document cannot be used, allowed filename length without extension is %character% character.')
->setParameters(['%character%' => 20])
->setTranslationDomain('validators')
->setCode('kimai-invoice-document-upload-04')
->addViolation();
}
}

View File

@@ -15,6 +15,7 @@ use App\Export\Base\DispositionInlineTrait;
use App\Export\ExportContext;
use App\Invoice\InvoiceFilename;
use App\Invoice\InvoiceModel;
use App\Utils\FileHelper;
use App\Utils\HtmlToPdfConverter;
use Symfony\Component\HttpFoundation\Response;
use Twig\Environment;
@@ -61,6 +62,8 @@ final class PdfRenderer extends AbstractTwigRenderer implements DispositionInlin
$response = new Response($content);
$filename = FileHelper::convertToAsciiFilename($filename);
$disposition = $response->headers->makeDisposition($this->getDisposition(), $filename . '.pdf');
$response->headers->set('Content-Type', 'application/pdf');

View File

@@ -81,8 +81,20 @@ final class TimesheetBudgetUsedValidator extends ConstraintValidator
// when changing the date via the calendar and/or the API, the duration will not be reset by the
// duration calculator (which runs after validation!) so we manually reset the duration before
$timesheet->setDuration(null);
$duration = $timesheet->getDuration();
// ------------------------------------------------------------------------
// Old solution (buggy) - duration MAY NOT BE RESET!
// this will cause the timesheet to be deleted in the Weekly-QuickEntry-Flow
// ------------------------------------------------------------------------
// $timesheet->setDuration(null);
// $duration = $timesheet->getDuration();
// another possible solution is cloning the timesheet
// $timesheet = clone $timesheet;
// $timesheet->setDuration(null);
// $duration = $timesheet->getDuration();
$duration = $timesheet->getCalculatedDuration();
$timeRate = $this->rateService->calculate($timesheet);
$rate = $timeRate->getRate();

View File

@@ -773,6 +773,36 @@ class TimesheetControllerTest extends APIControllerBaseTest
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
public function testGetRecentActionForUser(): void
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
$start = new \DateTime('-10 days');
$user = $this->getUserByRole(User::ROLE_ADMIN);
$fixture = new TimesheetFixtures();
$fixture->setFixedRate(true);
$fixture->setHourlyRate(true);
$fixture->setAmount(10);
$fixture->setUser($user);
$fixture->setStartDate($start);
$this->importFixture($fixture);
$query = [
'user' => $user->getId(),
'size' => 2,
'begin' => $start->format(self::DATE_FORMAT_HTML5),
];
$this->assertAccessIsGranted($client, '/api/timesheets/recent', 'GET', $query);
$result = json_decode($client->getResponse()->getContent(), true);
$this->assertIsArray($result);
$this->assertNotEmpty($result);
$this->assertEquals(1, \count($result));
self::assertApiResponseTypeStructure('TimesheetCollectionFull', $result[0]);
}
public function testActiveAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);

View File

@@ -325,6 +325,7 @@ class ConfigurationTest extends TestCase
],
'simple_form' => false,
'number_format' => '{Y}/{cy,3}',
'upload_twig' => true,
],
'export' => [
'documents' => [

View File

@@ -31,7 +31,6 @@ class TimesheetExportRepositoryTest extends TestCase
$sut = new TimesheetExportRepository($repository);
/* @phpstan-ignore-next-line */
$sut->setExported([new Timesheet(), null, new \stdClass(), new Timesheet(), new Activity()]);
// test else for empty array
/* @phpstan-ignore-next-line */

View File

@@ -31,7 +31,6 @@ class TimesheetInvoiceItemRepositoryTest extends TestCase
$sut = new TimesheetInvoiceItemRepository($repository);
/* @phpstan-ignore-next-line */
$sut->setExported([new Timesheet(), null, new \stdClass(), new Timesheet(), new Activity()]);
// test else for empty array
/* @phpstan-ignore-next-line */

View File

@@ -412,6 +412,7 @@ class TimesheetBudgetUsedValidatorTest extends ConstraintValidatorTestCase
$timesheet->method('getBegin')->willReturn($begin);
$timesheet->method('getEnd')->willReturn($end);
$timesheet->method('getDuration')->willReturn($end->getTimestamp() - $begin->getTimestamp());
$timesheet->method('getCalculatedDuration')->willReturn($end->getTimestamp() - $begin->getTimestamp());
$timesheet->method('getUser')->willReturn(new User());
$timesheet->method('getProject')->willReturn($project);
$timesheet->method('getActivity')->willReturn($activity);

View File

@@ -60,7 +60,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: DOCX و ODS و XLSX.</target>
<target state="translated">انتباه: سيتم الكتابة فوق الملفات الموجودة. أنواع الملفات المسموح بها هي: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: DOCX, ODS, XLSX.</target>
<target state="translated">Pozor: existující soubory budou přepsány. Povolené typy jsou: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX.</target>
<target state="translated">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: DOCX, ODS, XLSX</target>
<target state="needs-translation">Achtung: existierende Dateien werden überschrieben. Erlaubte Dateitypen sind: %extensions%</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: DOCX, ODS, XLSX.</target>
<target state="translated">Προσοχή: τα υπάρχοντα αρχεία θα αντικατασταθούν. Επιτρεπόμενοι τύποι αρχείων είναι: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
<target>Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: DOCX, ODS, XLSX.</target>
<target>Atentu: ekzistantaj dosieroj estos anstataŭigitaj. Permesitaj tipoj de dosieroj estas: %extensions%.</target>
</trans-unit>
</body>
</file>

View File

@@ -44,7 +44,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: DOCX, ODS, XLSX.</target>
<target state="translated">Atención: Los archivos existentes serán sobrescritos. Los tipos de archivo permitidos son: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: DOCX، ODS، XLSX.</target>
<target state="translated">توجه: فایل های موجود رونویسی خواهند شد. فایل های مجاز عبارتند از: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: DOCX, ODS, XLSX.</target>
<target state="translated">Huomioitavaa: olemassa olevat tiedostot korvataan. Sallitut tiedosto tyypit on: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: DOCX, ODS, XLSX.</target>
<target state="needs-translation">Attention: existing files will be overwritten. Allowed file types are: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -48,7 +48,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : DOCX, ODS, XLSX.</target>
<target>Attention : les fichiers existants seront écrasés. Les types de fichiers autorisés sont : %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: DOCX, ODS, XLSX.</target>
<target>שים לב: קבצים קיימים ידרסו וימחקו. סוגי קבצים מותרים: %extensions%.</target>
</trans-unit>
<trans-unit id="sjwwrr8" resname="Xlsx">
<source>Microsoft Excel (XLSX)</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: DOCX, ODS, XLSX.</target>
<target>Oprez: postojeće datoteke će se prepisati. Dopuštene vrste datoteka su: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -40,7 +40,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: DOCX, ODS, XLSX.</target>
<target>Figyelem: a meglévő fájlok felülíródnak. A megengedett fájltípusok: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -44,7 +44,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 DOCX, ODS, XLSX입니다.</target>
<target>주의: 기존 파일을 덮어씁니다. 허용되는 파일 형식은 %extensions%입니다.</target>
</trans-unit>
<trans-unit id="nA.KOyM" resname="Html">
<source>Html</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene DOCX, ODS og XLSX støttes.</target>
<target>Obs: Eksisterende filer vil bli overskrevet. Filtypene %extensions% støttes.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: DOCX, ODS, XLSX.</target>
<target>Attentie: Bestaande bestanden zullen overschreven worden. Toegestaande bestandstypes zijn: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -44,7 +44,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: DOCX, ODS, XLSX.</target>
<target state="translated">Uwaga: istniejące pliki zostaną nadpisane. Dozwolone typu plików to: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: DOCX, ODS, XLSX.</target>
<target state="translated">Atenção: os ficheiros já existentes serão substituídos. Os tipos de ficheiros permitidos são: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: DOCX, ODS, XLSX.</target>
<target>Atenção: os arquivos já existentes serão substituídos. Os tipos dos arquivos permitidos são: %extensions%.</target>
</trans-unit>
<trans-unit id="nA.KOyM" resname="Html">
<source>Html</source>

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: DOCX, ODS, XLSX.</target>
<target>Atenție: fișierele existente vor fi suprascrise. Tipuri de fișiere permise: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -20,7 +20,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: DOCX, ODS, XLSX.</target>
<target state="translated">Внимание: существующие файлы будут перезаписаны. Допустимые типы файлов: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: DOCX, ODS, XLSX.</target>
<target state="translated">Obs: redan exsiterande filer kommer att skrivas över. Tillåtna fil typer är: %extensions%.</target>
</trans-unit>
<trans-unit id="N6juwc4" resname="default">
<source>default</source>

View File

@@ -60,7 +60,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: DOCX, ODS, XLSX.</target>
<target>Dikkat: mevcut dosyaların üzerine yazılacaktır. İzin verilen dosya türleri: %extensions%.</target>
</trans-unit>
<trans-unit id="nA.KOyM" resname="Html">
<source>Html</source>

View File

@@ -8,7 +8,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: DOCX, ODS, XLSX.</target>
<target state="translated">Увага: існуючі файли будуть перезаписані. Дозволені типи файлів: %extensions%.</target>
</trans-unit>
<!-- Optgroups -->
<trans-unit id="E5BwZHT" resname="programmatic">

View File

@@ -36,7 +36,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: DOCX, ODS, XLSX.</target>
<target>Chú ý: các tệp hiện có sẽ bị ghi đè. Các loại tệp được phép là: %extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>

View File

@@ -60,7 +60,7 @@
</trans-unit>
<trans-unit id="7mEUv6C" resname="help.upload">
<source>help.upload</source>
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:DOCX, ODS, XLSX.</target>
<target state="translated">注意:现有文件将被覆盖。允许的文件类型是:%extensions%.</target>
</trans-unit>
<trans-unit id="E5BwZHT" resname="programmatic">
<source>programmatic</source>