Füge Unterstützung für Datenquellen-Synonyme hinzu.

Ermöglicht benutzerdefinierte Synonyme für Datenquellen basierend auf Locale. Synonyme werden in verschiedenen Bereichen wie Bäumen, Übersetzungen und Vorlagen genutzt, um anpassbare Namen anzuzeigen.
This commit is contained in:
Marcel Diegelmann 2025-07-07 10:08:19 +02:00
parent 175f664082
commit 9d9cedd222
28 changed files with 272 additions and 36 deletions

View file

@ -16,6 +16,20 @@ parameters:
partdb.create_assembly_use_ipn_placeholder_in_name: '%env(bool:CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME)%' # Use an %%ipn%% placeholder in the name of an assembly. Placeholder is replaced with the ipn input while saving.
partdb.data_sources.synonyms: # Define your own synonyms for the given data sources
# Possible datasources: category, storagelocation, footprint, manufacturer, supplier, project, assembly
# Possible locales like the ones in 'partdb.locale_menu': en, de, it, fr, ru, ja, cs, da, zh, pl
#category:
#de: 'Bauteil Kategorien'
#en: 'Part categories'
#project:
#de: 'Geräte'
#en: 'Devices'
#assembly:
#de: 'Zusammengestellte Baugruppe'
#en: 'Combined assembly'
######################################################################################################################
# Users and Privacy
######################################################################################################################

View file

@ -198,6 +198,25 @@ services:
$fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/'
$tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/'
####################################################################################################################
# Trees
####################################################################################################################
App\Services\Trees\TreeViewGenerator:
arguments:
$dataSourceSynonyms: '%partdb.data_sources.synonyms%'
App\Services\Trees\ToolsTreeBuilder:
arguments:
$dataSourceSynonyms: '%partdb.data_sources.synonyms%'
####################################################################################################################
# Twig Extensions
####################################################################################################################
App\Twig\DataSourceNameExtension:
arguments:
$dataSourceSynonyms: '%partdb.data_sources.synonyms%'
tags: [ 'twig.extension' ]
####################################################################################################################
# Part info provider system
####################################################################################################################

View file

@ -29,6 +29,7 @@ use App\DataTables\Filters\AssemblyFilter;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Entity\Parts\Part;
use App\Entity\UserSystem\User;
use App\Exceptions\InvalidRegexException;
use App\Form\AssemblySystem\AssemblyAddPartsType;
use App\Form\AssemblySystem\AssemblyBuildType;

View file

@ -50,8 +50,15 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class ToolsTreeBuilder
{
public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security)
{
public function __construct(
protected TranslatorInterface $translator,
protected UrlGeneratorInterface $urlGenerator,
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected Security $security,
protected ?array $dataSourceSynonyms = [],
) {
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
}
/**
@ -166,43 +173,43 @@ class ToolsTreeBuilder
}
if ($this->security->isGranted('read', new Category())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.categories'),
$this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()),
$this->urlGenerator->generate('category_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
}
if ($this->security->isGranted('read', new Project())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.projects'),
$this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()),
$this->urlGenerator->generate('project_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
}
if ($this->security->isGranted('read', new Assembly())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.assemblies'),
$this->getTranslatedDataSourceOrSynonym('assembly', 'tree.tools.edit.assemblies', $this->translator->getLocale()),
$this->urlGenerator->generate('assembly_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-list');
}
if ($this->security->isGranted('read', new Supplier())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.suppliers'),
$this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()),
$this->urlGenerator->generate('supplier_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
}
if ($this->security->isGranted('read', new Manufacturer())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.manufacturer'),
$this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()),
$this->urlGenerator->generate('manufacturer_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
}
if ($this->security->isGranted('read', new StorageLocation())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.storelocation'),
$this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()),
$this->urlGenerator->generate('store_location_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
}
if ($this->security->isGranted('read', new Footprint())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.footprint'),
$this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()),
$this->urlGenerator->generate('footprint_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
}
@ -310,4 +317,22 @@ class ToolsTreeBuilder
return $nodes;
}
protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string
{
$currentTranslation = $this->translator->trans($translationKey);
// Call alternatives from DataSourcesynonyms (if available)
if (!empty($this->dataSourceSynonyms[$dataSource][$locale])) {
$alternativeTranslation = $this->dataSourceSynonyms[$dataSource][$locale];
// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}
// Otherwise return the standard translation
return $currentTranslation;
}
}

View file

@ -68,9 +68,11 @@ class TreeViewGenerator
protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings,
protected ?array $dataSourceSynonyms = [],
) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
}
/**
@ -226,14 +228,16 @@ class TreeViewGenerator
protected function entityClassToRootNodeString(string $class): string
{
$locale = $this->translator->getLocale();
return match ($class) {
Category::class => $this->translator->trans('category.labelp'),
StorageLocation::class => $this->translator->trans('storelocation.labelp'),
Footprint::class => $this->translator->trans('footprint.labelp'),
Manufacturer::class => $this->translator->trans('manufacturer.labelp'),
Supplier::class => $this->translator->trans('supplier.labelp'),
Project::class => $this->translator->trans('project.labelp'),
Assembly::class => $this->translator->trans('assembly.labelp'),
Category::class => $this->getTranslatedOrSynonym('category', $locale),
StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
Project::class => $this->getTranslatedOrSynonym('project', $locale),
Assembly::class => $this->getTranslatedOrSynonym('assembly', $locale),
default => $this->translator->trans('tree.root_node.text'),
};
}
@ -290,4 +294,22 @@ class TreeViewGenerator
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line
});
}
protected function getTranslatedOrSynonym(string $key, string $locale): string
{
$currentTranslation = $this->translator->trans($key . '.labelp');
// Call alternatives from DataSourcesynonyms (if available)
if (!empty($this->dataSourceSynonyms[$key][$locale])) {
$alternativeTranslation = $this->dataSourceSynonyms[$key][$locale];
// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}
// Otherwise return the standard translation
return $currentTranslation;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Twig;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class DataSourceNameExtension extends AbstractExtension
{
private TranslatorInterface $translator;
private array $dataSourceSynonyms;
public function __construct(TranslatorInterface $translator, ?array $dataSourceSynonyms)
{
$this->translator = $translator;
$this->dataSourceSynonyms = $dataSourceSynonyms ?? [];
}
public function getFunctions(): array
{
return [
new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']),
];
}
/**
* Based on the locale and data source names, gives the right synonym value back or the default translator value.
*/
public function getDataSourceName(string $dataSourceName, string $defaultKey): string
{
$locale = $this->translator->getLocale();
// Use alternative dataSource synonym (if available)
if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) {
return $this->dataSourceSynonyms[$dataSourceName][$locale];
}
// Otherwise return the standard translation
return $this->translator->trans($defaultKey);
}
}

View file

@ -3,7 +3,9 @@
{# @var entity App\Entity\AssemblySystem\Assembly #}
{% block card_title %}
<i class="fas fa-archive fa-fw"></i> {% trans %}assembly.caption{% endtrans %}
{% set dataSourceName = get_data_source_name('assembly', 'assembly.caption') %}
{% set translatedSource = 'assembly.caption'|trans %}
<i class="fas fa-archive fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block edit_title %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-tags fa-fw"></i> {% trans %}category.labelp{% endtrans %}
{% set dataSourceName = get_data_source_name('category', 'category.labelp') %}
{% set translatedSource = 'category.labelp'|trans %}
<i class="fas fa-tags fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block additional_pills %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-microchip fa-fw"></i> {% trans %}footprint.labelp{% endtrans %}
{% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %}
{% set translatedSource = 'footprint.labelp'|trans %}
<i class="fas fa-microchip fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block master_picture_block %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-industry fa-fw"></i> {% trans %}manufacturer.caption{% endtrans %}
{% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %}
{% set translatedSource = 'manufacturer.caption'|trans %}
<i class="fas fa-industry fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block edit_title %}

View file

@ -3,7 +3,9 @@
{# @var entity App\Entity\ProjectSystem\Project #}
{% block card_title %}
<i class="fas fa-archive fa-fw"></i> {% trans %}project.caption{% endtrans %}
{% set dataSourceName = get_data_source_name('project', 'project.caption') %}
{% set translatedSource = 'project.caption'|trans %}
<i class="fas fa-archive fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block edit_title %}

View file

@ -2,7 +2,9 @@
{% import "label_system/dropdown_macro.html.twig" as dropdown %}
{% block card_title %}
<i class="fas fa-cube fa-fw"></i> {% trans %}storelocation.labelp{% endtrans %}
{% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %}
{% set translatedSource = 'storelocation.labelp'|trans %}
<i class="fas fa-cube fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block additional_controls %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-truck fa-fw"></i> {% trans %}supplier.caption{% endtrans %}
{% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %}
{% set translatedSource = 'supplier.caption'|trans %}
<i class="fas fa-truck fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
{% endblock %}
{% block additional_panes %}

View file

@ -1,14 +1,16 @@
{% macro sidebar_dropdown() %}
{% set currentLocale = app.request.locale %}
{# Format is [mode, route, label, show_condition] #}
{% set data_sources = [
['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')],
['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')],
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')],
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')],
['tools', path('tree_tools'), 'tools.label', true],
['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'],
['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'],
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'],
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'],
['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read'), 'assembly'],
['tools', path('tree_tools'), 'tools.label', true, 'tool'],
] %}
<li class="dropdown-header">{% trans %}actions{% endtrans %}</li>
@ -19,9 +21,9 @@
{% for source in data_sources %}
{% if source[3] %} {# show_condition #}
<li><button class="tree-btns dropdown-item" data-mode="{{ source[0] }}" data-url="{{ source[1] }}" data-text="{{ source[2] | trans }}"
<li><button class="tree-btns dropdown-item" data-mode="{{ source[0] }}" data-url="{{ source[1] }}" data-text="{{ get_data_source_name(source[4], source[2]) }}"
{{ stimulus_action('elements/sidebar_tree', 'changeDataSource') }}
>{{ source[2] | trans }}</button></li>
>{{ get_data_source_name(source[4], source[2]) }}</button></li>
{% endif %}
{% endfor %}
{% endmacro %}
@ -62,4 +64,4 @@
<div class="treeview-sm mt-2" {{ stimulus_target('elements/tree', 'tree') }}></div>
</div>
{% endmacro %}
{% endmacro %}

View file

@ -6,12 +6,31 @@
<div class="form-check">
<input type="checkbox" class="form-check-input tristate permission-checkbox permission_multicheckbox" id="mulit_check_{{ form.vars.label }}">
<label class="form-check-label" for="mulit_check_{{ form.vars.label }}">
<b>{{ form.vars.label | trans }}</b>
{% set dataSource = '' %}
{% if (form.vars.label == 'perm.categories') %}
{% set dataSource = 'category' %}
{% elseif (form.vars.label == 'perm.storelocations') %}
{% set dataSource = 'storagelocation' %}
{% elseif (form.vars.label == 'perm.footprints') %}
{% set dataSource = 'footprint' %}
{% elseif (form.vars.label == 'perm.manufacturers') %}
{% set dataSource = 'manufacturer' %}
{% elseif (form.vars.label == 'perm.suppliers') %}
{% set dataSource = 'supplier' %}
{% elseif (form.vars.label == 'perm.projects') %}
{% set dataSource = 'project' %}
{% elseif (form.vars.label == 'perm.assemblies') %}
{% set dataSource = 'assembly' %}
{% endif %}
{% set dataSourceName = get_data_source_name(dataSource, form.vars.label) %}
{% set translatedSource = form.vars.label|trans %}
{% if dataSourceName != translatedSource %}<b>{{ 'datasource.synonym'|trans({'%name%': translatedSource ~ '<br>', '%synonym%': dataSourceName})|raw }}{% else %}{{ translatedSource }}</b>{% endif %}
</label>
</div>
{% else %}
<b>{{ form.vars.label | trans }}</b>
<b>def{{ form.vars.label | trans }}</b>
{% endif %}

View file

@ -14745,5 +14745,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
<target>Neplatný regulární výraz (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Váš synonymum: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13462,5 +13462,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Ugyldigt regulært udtryk (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Dit synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -15455,5 +15455,11 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<target>Ungültiger regulärer Ausdruck (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ihr Synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -2470,5 +2470,11 @@
<target>Μη έγκυρη κανονική έκφραση (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Το συνώνυμό σας: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -15456,5 +15456,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>Invalid regular expression (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Your synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13634,5 +13634,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
<target>Expresión regular no válida (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Tu sinónimo: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -10044,5 +10044,11 @@ exemple de ville</target>
<target>Expression régulière invalide (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Votre synonyme : %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13636,5 +13636,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
<target>Espressione regolare non valida (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Il tuo sinonimo: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -9757,5 +9757,11 @@ Exampletown</target>
<target>無効な正規表現regex</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (あなたの同義語: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -1695,5 +1695,11 @@
<target>Ongeldige reguliere expressie (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Uw synoniem: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13489,5 +13489,11 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
<target>Nieprawidłowe wyrażenie regularne (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Twój synonim: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13589,5 +13589,11 @@
<target>Неверное регулярное выражение (regex)</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ваш синоним: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13474,5 +13474,11 @@ Element 3</target>
<target>无效的正则表达式regex</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (您的同义词: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>