polyadmin 0.1.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polyadmin/__init__.py +47 -0
- polyadmin/core/__init__.py +0 -0
- polyadmin/core/_async.py +21 -0
- polyadmin/core/action.py +135 -0
- polyadmin/core/admin.py +129 -0
- polyadmin/core/audit.py +65 -0
- polyadmin/core/auth.py +52 -0
- polyadmin/core/authorization.py +48 -0
- polyadmin/core/csrf.py +70 -0
- polyadmin/core/dashboard.py +41 -0
- polyadmin/core/delete.py +146 -0
- polyadmin/core/exporter.py +136 -0
- polyadmin/core/field.py +201 -0
- polyadmin/core/filter.py +305 -0
- polyadmin/core/inline.py +97 -0
- polyadmin/core/login.py +110 -0
- polyadmin/core/model_admin.py +349 -0
- polyadmin/core/page.py +62 -0
- polyadmin/core/pagination.py +70 -0
- polyadmin/core/query.py +243 -0
- polyadmin/core/relation.py +40 -0
- polyadmin/core/slug.py +57 -0
- polyadmin/core/template_context.py +679 -0
- polyadmin/core/widget.py +280 -0
- polyadmin/fastapi/__init__.py +3 -0
- polyadmin/fastapi/audit.py +52 -0
- polyadmin/fastapi/auth.py +112 -0
- polyadmin/fastapi/csrf.py +93 -0
- polyadmin/fastapi/deletes.py +76 -0
- polyadmin/fastapi/errors.py +93 -0
- polyadmin/fastapi/handlers.py +798 -0
- polyadmin/fastapi/inlines.py +177 -0
- polyadmin/fastapi/locale.py +128 -0
- polyadmin/fastapi/login.py +110 -0
- polyadmin/fastapi/pages.py +97 -0
- polyadmin/fastapi/relations.py +264 -0
- polyadmin/fastapi/responses.py +55 -0
- polyadmin/fastapi/router.py +174 -0
- polyadmin/fastapi/static.py +21 -0
- polyadmin/i18n/__init__.py +50 -0
- polyadmin/i18n/context.py +54 -0
- polyadmin/i18n/negotiation.py +56 -0
- polyadmin/i18n/setup.py +81 -0
- polyadmin/i18n/translator.py +124 -0
- polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +486 -0
- polyadmin/locale/polyadmin.pot +485 -0
- polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
- polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +496 -0
- polyadmin/templates/admin/base.html +91 -0
- polyadmin/templates/admin/components/action_confirm_modal.html +86 -0
- polyadmin/templates/admin/components/csrf-field.html +5 -0
- polyadmin/templates/admin/components/error_fragment.html +6 -0
- polyadmin/templates/admin/components/field.html +60 -0
- polyadmin/templates/admin/components/form_wrapper.html +131 -0
- polyadmin/templates/admin/components/icons.html +67 -0
- polyadmin/templates/admin/components/inline.html +251 -0
- polyadmin/templates/admin/components/inline_fragment.html +2 -0
- polyadmin/templates/admin/components/list_content.html +54 -0
- polyadmin/templates/admin/components/lookup_results.html +19 -0
- polyadmin/templates/admin/components/search.html +19 -0
- polyadmin/templates/admin/components/toasts.html +151 -0
- polyadmin/templates/admin/components/ui/breadcrumb.html +30 -0
- polyadmin/templates/admin/components/ui/bulk-actions.html +69 -0
- polyadmin/templates/admin/components/ui/calendar.html +175 -0
- polyadmin/templates/admin/components/ui/combobox.html +82 -0
- polyadmin/templates/admin/components/ui/delete-preview.html +37 -0
- polyadmin/templates/admin/components/ui/dropdown-menu.html +71 -0
- polyadmin/templates/admin/components/ui/field.html +110 -0
- polyadmin/templates/admin/components/ui/filter-panel.html +155 -0
- polyadmin/templates/admin/components/ui/locale-switcher.html +30 -0
- polyadmin/templates/admin/components/ui/multi-select.html +253 -0
- polyadmin/templates/admin/components/ui/pagination.html +81 -0
- polyadmin/templates/admin/components/ui/radio-group.html +28 -0
- polyadmin/templates/admin/components/ui/select.html +165 -0
- polyadmin/templates/admin/components/ui/sidebar.html +175 -0
- polyadmin/templates/admin/components/ui/slider.html +22 -0
- polyadmin/templates/admin/components/ui/switch.html +36 -0
- polyadmin/templates/admin/components/ui/table.html +221 -0
- polyadmin/templates/admin/components/ui/theme-toggle.html +33 -0
- polyadmin/templates/admin/dashboard.html +35 -0
- polyadmin/templates/admin/error.html +33 -0
- polyadmin/templates/admin/login.html +94 -0
- polyadmin/templates/admin/resource/delete.html +29 -0
- polyadmin/templates/admin/resource/delete_selected.html +49 -0
- polyadmin/templates/admin/resource/detail.html +78 -0
- polyadmin/templates/admin/resource/form.html +5 -0
- polyadmin/templates/admin/resource/list.html +5 -0
- polyadmin/templates/admin/theme.html +372 -0
- polyadmin/templates/admin/widgets/activity.html +8 -0
- polyadmin/templates/admin/widgets/chart.html +15 -0
- polyadmin/templates/admin/widgets/donut.html +59 -0
- polyadmin/templates/admin/widgets/metric.html +1 -0
- polyadmin/templates/admin/widgets/progress.html +7 -0
- polyadmin/templates/admin/widgets/stat.html +22 -0
- polyadmin/templates/admin/widgets/table.html +29 -0
- polyadmin/templates/admin/widgets/tabs.html +34 -0
- polyadmin/templates/admin/widgets/timeline.html +21 -0
- polyadmin/templating.py +528 -0
- polyadmin/ui.py +817 -0
- polyadmin-0.1.0b1.dist-info/METADATA +239 -0
- polyadmin-0.1.0b1.dist-info/RECORD +104 -0
- polyadmin-0.1.0b1.dist-info/WHEEL +4 -0
- polyadmin-0.1.0b1.dist-info/licenses/LICENSE +21 -0
polyadmin/i18n/setup.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""I18n: an Admin's internationalisation setup, built once at mount."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from polyadmin.i18n.negotiation import match_accept_language, match_locale
|
|
10
|
+
from polyadmin.i18n.translator import (
|
|
11
|
+
DEFAULT_LOCALE,
|
|
12
|
+
PSEUDO_LOCALE,
|
|
13
|
+
GettextTranslator,
|
|
14
|
+
PseudoTranslator,
|
|
15
|
+
Translator,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
BUILTIN_LOCALE_NAMES = {
|
|
19
|
+
"en": "English",
|
|
20
|
+
"fr": "Français",
|
|
21
|
+
"ru": "Русский",
|
|
22
|
+
PSEUDO_LOCALE: "Pseudo (en-XA)",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class LocaleOption:
|
|
28
|
+
code: str
|
|
29
|
+
name: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class I18n:
|
|
33
|
+
def __init__(self, *, translator: Translator, default: str, supported: Sequence[str], names: Mapping[str, str]) -> None:
|
|
34
|
+
self.translator = translator
|
|
35
|
+
self.default = default
|
|
36
|
+
self.supported = list(supported)
|
|
37
|
+
self._names = {**BUILTIN_LOCALE_NAMES, **names}
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def from_admin(cls, admin: Any) -> I18n:
|
|
41
|
+
default = admin.default_locale or DEFAULT_LOCALE
|
|
42
|
+
translator = admin.translator
|
|
43
|
+
available = [DEFAULT_LOCALE]
|
|
44
|
+
if translator is None:
|
|
45
|
+
translator = GettextTranslator(admin.catalogs)
|
|
46
|
+
available = translator.locales()
|
|
47
|
+
supported = available
|
|
48
|
+
if admin.locales:
|
|
49
|
+
if admin.translator is None:
|
|
50
|
+
missing = [locale for locale in admin.locales if locale not in available]
|
|
51
|
+
if missing:
|
|
52
|
+
raise ValueError(f"polyadmin: locale {missing[0]!r} has no catalog")
|
|
53
|
+
supported = list(admin.locales)
|
|
54
|
+
if default not in supported:
|
|
55
|
+
raise ValueError(f"polyadmin: default locale {default!r} is not among the supported locales {supported}")
|
|
56
|
+
if admin.pseudo_locale:
|
|
57
|
+
supported = [*supported, PSEUDO_LOCALE]
|
|
58
|
+
translator = PseudoTranslator(translator)
|
|
59
|
+
return cls(translator=translator, default=default, supported=supported, names=admin.locale_names or {})
|
|
60
|
+
|
|
61
|
+
def match(self, candidate: str | None) -> str | None:
|
|
62
|
+
return match_locale(self.supported, candidate)
|
|
63
|
+
|
|
64
|
+
def resolve(self, cookie: str | None, resolver: Callable[[], str | None] | None, accept_language: str | None) -> str:
|
|
65
|
+
"""Cookie, then the host's resolver, then Accept-Language, then the
|
|
66
|
+
default. `resolver` is a thunk so a cookie that decides never costs
|
|
67
|
+
an authentication."""
|
|
68
|
+
locale = self.match(cookie)
|
|
69
|
+
if locale:
|
|
70
|
+
return locale
|
|
71
|
+
if resolver is not None:
|
|
72
|
+
locale = self.match(resolver())
|
|
73
|
+
if locale:
|
|
74
|
+
return locale
|
|
75
|
+
return match_accept_language(self.supported, accept_language) or self.default
|
|
76
|
+
|
|
77
|
+
def name(self, code: str) -> str:
|
|
78
|
+
return self._names.get(code) or code
|
|
79
|
+
|
|
80
|
+
def options(self) -> list[LocaleOption]:
|
|
81
|
+
return [LocaleOption(code, self.name(code)) for code in self.supported]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""The Translator: framework and host strings, keyed by the English text."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gettext as _stdlib
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Protocol
|
|
10
|
+
|
|
11
|
+
DEFAULT_LOCALE = "en"
|
|
12
|
+
PSEUDO_LOCALE = "en-XA"
|
|
13
|
+
DOMAIN = "polyadmin"
|
|
14
|
+
FRAMEWORK_LOCALE_DIR = Path(__file__).resolve().parent.parent / "locale"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Translator(Protocol):
|
|
18
|
+
def gettext(self, locale: str, message: str) -> str: ...
|
|
19
|
+
def ngettext(self, locale: str, singular: str, plural: str, n: int) -> str: ...
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _catalog_locales(localedir: Path, domain: str) -> set[str]:
|
|
23
|
+
if not localedir.is_dir():
|
|
24
|
+
return set()
|
|
25
|
+
return {
|
|
26
|
+
entry.name.replace("_", "-")
|
|
27
|
+
for entry in localedir.iterdir()
|
|
28
|
+
if (entry / "LC_MESSAGES" / f"{domain}.mo").is_file()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class GettextTranslator:
|
|
33
|
+
"""The default Translator: gettext catalogs, host ahead of framework.
|
|
34
|
+
|
|
35
|
+
`catalogs` are (localedir, domain) pairs layered over the framework's
|
|
36
|
+
own: a later entry overrides an earlier one with the same msgid, and
|
|
37
|
+
every host catalog overrides the framework's, so a host can translate
|
|
38
|
+
its strings and reword ours.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, catalogs: Iterable[tuple[str | Path, str]] = ()) -> None:
|
|
42
|
+
# Lookup order: the last host catalog first, the framework's last.
|
|
43
|
+
sources = [(Path(localedir), domain) for localedir, domain in catalogs][::-1]
|
|
44
|
+
sources.append((FRAMEWORK_LOCALE_DIR, DOMAIN))
|
|
45
|
+
found = {DEFAULT_LOCALE}
|
|
46
|
+
for localedir, domain in sources:
|
|
47
|
+
found |= _catalog_locales(localedir, domain)
|
|
48
|
+
self._locales = [DEFAULT_LOCALE, *sorted(found - {DEFAULT_LOCALE})]
|
|
49
|
+
self._chains: dict[str, _stdlib.NullTranslations] = {}
|
|
50
|
+
for locale in self._locales:
|
|
51
|
+
chain = None
|
|
52
|
+
for localedir, domain in sources:
|
|
53
|
+
translation = _stdlib.translation(
|
|
54
|
+
domain, localedir=localedir, languages=[locale.replace("-", "_")], fallback=True
|
|
55
|
+
)
|
|
56
|
+
if chain is None:
|
|
57
|
+
chain = translation
|
|
58
|
+
else:
|
|
59
|
+
chain.add_fallback(translation)
|
|
60
|
+
self._chains[locale] = chain
|
|
61
|
+
|
|
62
|
+
def locales(self) -> list[str]:
|
|
63
|
+
return list(self._locales)
|
|
64
|
+
|
|
65
|
+
def gettext(self, locale: str, message: str) -> str:
|
|
66
|
+
chain = self._chains.get(locale)
|
|
67
|
+
# gettext("") returns the catalog's header block.
|
|
68
|
+
if chain is None or not message:
|
|
69
|
+
return message
|
|
70
|
+
return chain.gettext(message)
|
|
71
|
+
|
|
72
|
+
def ngettext(self, locale: str, singular: str, plural: str, n: int) -> str:
|
|
73
|
+
chain = self._chains.get(locale)
|
|
74
|
+
if chain is None:
|
|
75
|
+
return singular if n == 1 else plural
|
|
76
|
+
return chain.ngettext(singular, plural, n)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
_PLACEHOLDER = re.compile(r"%(\([^)]*\))?[-+# 0]*\d*(\.\d+)?[a-zA-Z%]")
|
|
80
|
+
_ACCENTS = str.maketrans("aeiouycnszAEIOUYCNSZ", "àéîöüýçñšžÀÉÎÖÜÝÇÑŠŽ")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def pseudo(text: str) -> str:
|
|
84
|
+
"""Accent a string's letters and bracket it -- "Save" becomes "[Šàvé]" --
|
|
85
|
+
leaving %-placeholders intact so the result still formats."""
|
|
86
|
+
out, last = ["["], 0
|
|
87
|
+
for match in _PLACEHOLDER.finditer(text):
|
|
88
|
+
out.append(text[last : match.start()].translate(_ACCENTS))
|
|
89
|
+
out.append(match.group())
|
|
90
|
+
last = match.end()
|
|
91
|
+
out.append(text[last:].translate(_ACCENTS))
|
|
92
|
+
out.append("]")
|
|
93
|
+
return "".join(out)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_pseudo(text: str) -> bool:
|
|
97
|
+
"""Whether text already looks like pseudo()'s output -- bracketed
|
|
98
|
+
top to bottom, not merely containing a bracketed run somewhere."""
|
|
99
|
+
return len(text) >= 2 and text.startswith("[") and text.endswith("]")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class PseudoTranslator:
|
|
103
|
+
"""Wraps another Translator, rewriting PSEUDO_LOCALE only."""
|
|
104
|
+
|
|
105
|
+
def __init__(self, inner: Translator) -> None:
|
|
106
|
+
self._inner = inner
|
|
107
|
+
|
|
108
|
+
def gettext(self, locale: str, message: str) -> str:
|
|
109
|
+
if locale != PSEUDO_LOCALE:
|
|
110
|
+
return self._inner.gettext(locale, message)
|
|
111
|
+
if _is_pseudo(message):
|
|
112
|
+
# Already pseudo-translated -- an adapter re-translating a
|
|
113
|
+
# host string returned from code (an action's message
|
|
114
|
+
# becoming a flash, a rendered validation error). Wrapping
|
|
115
|
+
# it again would double-bracket it.
|
|
116
|
+
return message
|
|
117
|
+
return pseudo(self._inner.gettext(DEFAULT_LOCALE, message)) if message else message
|
|
118
|
+
|
|
119
|
+
def ngettext(self, locale: str, singular: str, plural: str, n: int) -> str:
|
|
120
|
+
if locale != PSEUDO_LOCALE:
|
|
121
|
+
return self._inner.ngettext(locale, singular, plural, n)
|
|
122
|
+
if _is_pseudo(singular):
|
|
123
|
+
return singular
|
|
124
|
+
return pseudo(self._inner.ngettext(DEFAULT_LOCALE, singular, plural, n))
|
|
Binary file
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
# French translations of PolyAdmin's framework strings.
|
|
2
|
+
#
|
|
3
|
+
# DRAFT: machine-assisted and not yet reviewed by a native speaker.
|
|
4
|
+
# Corrections are welcome; docs/i18n.md ("Maintaining the framework
|
|
5
|
+
# catalogs") describes how to regenerate and test this file.
|
|
6
|
+
msgid ""
|
|
7
|
+
msgstr ""
|
|
8
|
+
"Project-Id-Version: PolyAdmin 0.1.0\n"
|
|
9
|
+
"Report-Msgid-Bugs-To: https://github.com/MagicRodri/polyadmin/issues\n"
|
|
10
|
+
"POT-Creation-Date: 2026-09-21 18:34+0500\n"
|
|
11
|
+
"PO-Revision-Date: 2026-09-14 22:04+0500\n"
|
|
12
|
+
"Last-Translator: Draft, awaiting native review\n"
|
|
13
|
+
"Language: fr\n"
|
|
14
|
+
"Language-Team: French (unreviewed draft)\n"
|
|
15
|
+
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=utf-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
"Generated-By: Babel 2.18.0\n"
|
|
20
|
+
|
|
21
|
+
#, python-format
|
|
22
|
+
msgid "%(label)s (%(num)d)"
|
|
23
|
+
msgstr "%(label)s (%(num)d)"
|
|
24
|
+
|
|
25
|
+
#, python-format
|
|
26
|
+
msgid "%(label)s applied to %(num)d record."
|
|
27
|
+
msgid_plural "%(label)s applied to %(num)d records."
|
|
28
|
+
msgstr[0] "%(label)s appliqué à %(num)d enregistrement."
|
|
29
|
+
msgstr[1] "%(label)s appliqué à %(num)d enregistrements."
|
|
30
|
+
|
|
31
|
+
#, python-format
|
|
32
|
+
msgid "%(label)s is required."
|
|
33
|
+
msgstr "« %(label)s » est obligatoire."
|
|
34
|
+
|
|
35
|
+
#, python-format
|
|
36
|
+
msgid "%(n)s of %(num)d row selected"
|
|
37
|
+
msgid_plural "%(n)s of %(num)d rows selected"
|
|
38
|
+
msgstr[0] "%(n)s sur %(num)d ligne sélectionnée"
|
|
39
|
+
msgstr[1] "%(n)s sur %(num)d lignes sélectionnées"
|
|
40
|
+
|
|
41
|
+
#. Translators: %(name)s is the model's name. French and Russian
|
|
42
|
+
#. nouns carry gender, so phrase around agreement.
|
|
43
|
+
#, python-format
|
|
44
|
+
msgid "%(name)s created."
|
|
45
|
+
msgstr "« %(name)s » : enregistrement créé."
|
|
46
|
+
|
|
47
|
+
#. Translators: %(name)s is the model's name. French and Russian
|
|
48
|
+
#. nouns carry gender, so phrase around agreement.
|
|
49
|
+
#, python-format
|
|
50
|
+
msgid "%(name)s deleted."
|
|
51
|
+
msgstr "« %(name)s » : enregistrement supprimé."
|
|
52
|
+
|
|
53
|
+
#. Translators: %(name)s is the model's name. French and Russian
|
|
54
|
+
#. nouns carry gender, so phrase around agreement.
|
|
55
|
+
#, python-format
|
|
56
|
+
msgid "%(name)s updated."
|
|
57
|
+
msgstr "« %(name)s » : enregistrement mis à jour."
|
|
58
|
+
|
|
59
|
+
#, python-format
|
|
60
|
+
msgid "%(num)d row"
|
|
61
|
+
msgid_plural "%(num)d rows"
|
|
62
|
+
msgstr[0] "%(num)d ligne"
|
|
63
|
+
msgstr[1] "%(num)d lignes"
|
|
64
|
+
|
|
65
|
+
#, python-format
|
|
66
|
+
msgid "%(num)d you can't view"
|
|
67
|
+
msgid_plural "%(num)d you can't view"
|
|
68
|
+
msgstr[0] "%(num)d enregistrement que vous ne pouvez pas voir"
|
|
69
|
+
msgstr[1] "%(num)d enregistrements que vous ne pouvez pas voir"
|
|
70
|
+
|
|
71
|
+
msgid "Action…"
|
|
72
|
+
msgstr "Action…"
|
|
73
|
+
|
|
74
|
+
msgid "Add"
|
|
75
|
+
msgstr "Ajouter"
|
|
76
|
+
|
|
77
|
+
msgid "Admin"
|
|
78
|
+
msgstr "Administration"
|
|
79
|
+
|
|
80
|
+
msgid "All"
|
|
81
|
+
msgstr "Tous"
|
|
82
|
+
|
|
83
|
+
msgid "Anonymous"
|
|
84
|
+
msgstr "Anonyme"
|
|
85
|
+
|
|
86
|
+
msgid "Any date"
|
|
87
|
+
msgstr "Toutes les dates"
|
|
88
|
+
|
|
89
|
+
msgid "Apply"
|
|
90
|
+
msgstr "Appliquer"
|
|
91
|
+
|
|
92
|
+
msgid "Asc"
|
|
93
|
+
msgstr "Croissant"
|
|
94
|
+
|
|
95
|
+
msgid "Back to the admin"
|
|
96
|
+
msgstr "Retour à l'administration"
|
|
97
|
+
|
|
98
|
+
msgid "Breadcrumb"
|
|
99
|
+
msgstr "Fil d'Ariane"
|
|
100
|
+
|
|
101
|
+
msgid "CSV"
|
|
102
|
+
msgstr "CSV"
|
|
103
|
+
|
|
104
|
+
msgid "Cancel"
|
|
105
|
+
msgstr "Annuler"
|
|
106
|
+
|
|
107
|
+
msgid "Clear all"
|
|
108
|
+
msgstr "Tout effacer"
|
|
109
|
+
|
|
110
|
+
msgid "Clear selection"
|
|
111
|
+
msgstr "Effacer la sélection"
|
|
112
|
+
|
|
113
|
+
msgid "Close"
|
|
114
|
+
msgstr "Fermer"
|
|
115
|
+
|
|
116
|
+
msgid "Close filters"
|
|
117
|
+
msgstr "Fermer les filtres"
|
|
118
|
+
|
|
119
|
+
msgid "Confirm"
|
|
120
|
+
msgstr "Confirmer"
|
|
121
|
+
|
|
122
|
+
#, python-format
|
|
123
|
+
msgid "Couldn't save %(name)s"
|
|
124
|
+
msgstr "Impossible d'enregistrer « %(name)s »"
|
|
125
|
+
|
|
126
|
+
msgid "Custom range"
|
|
127
|
+
msgstr "Période personnalisée"
|
|
128
|
+
|
|
129
|
+
msgid "Dark mode"
|
|
130
|
+
msgstr "Mode sombre"
|
|
131
|
+
|
|
132
|
+
msgid "Dashboard"
|
|
133
|
+
msgstr "Tableau de bord"
|
|
134
|
+
|
|
135
|
+
msgid "Delete"
|
|
136
|
+
msgstr "Supprimer"
|
|
137
|
+
|
|
138
|
+
#, python-format
|
|
139
|
+
msgid "Delete %(name)s"
|
|
140
|
+
msgstr "Supprimer %(name)s"
|
|
141
|
+
|
|
142
|
+
#, python-format
|
|
143
|
+
msgid "Delete %(num)d record?"
|
|
144
|
+
msgid_plural "Delete %(num)d records?"
|
|
145
|
+
msgstr[0] "Supprimer %(num)d enregistrement ?"
|
|
146
|
+
msgstr[1] "Supprimer %(num)d enregistrements ?"
|
|
147
|
+
|
|
148
|
+
msgid "Delete selected"
|
|
149
|
+
msgstr "Supprimer la sélection"
|
|
150
|
+
|
|
151
|
+
msgid "Delete the selected records? This cannot be undone."
|
|
152
|
+
msgstr "Supprimer les enregistrements sélectionnés ? Cette action est irréversible."
|
|
153
|
+
|
|
154
|
+
#, python-format
|
|
155
|
+
msgid "Delete this %(name)s?"
|
|
156
|
+
msgstr "Supprimer « %(name)s » ?"
|
|
157
|
+
|
|
158
|
+
#, python-format
|
|
159
|
+
msgid "Delete «%(label)s»?"
|
|
160
|
+
msgstr "Supprimer « %(label)s » ?"
|
|
161
|
+
|
|
162
|
+
#. Translators: a bulk delete stopped part-way. %(deleted)d of
|
|
163
|
+
#. %(total)d records were deleted; %(error)s is the underlying error.
|
|
164
|
+
#, python-format
|
|
165
|
+
msgid "Deleted %(deleted)d of %(total)d, then failed: %(error)s"
|
|
166
|
+
msgstr "%(deleted)d sur %(total)d supprimé(s), puis échec : %(error)s"
|
|
167
|
+
|
|
168
|
+
#, python-format
|
|
169
|
+
msgid "Deleted %(num)d record."
|
|
170
|
+
msgid_plural "Deleted %(num)d records."
|
|
171
|
+
msgstr[0] "%(num)d enregistrement supprimé."
|
|
172
|
+
msgstr[1] "%(num)d enregistrements supprimés."
|
|
173
|
+
|
|
174
|
+
msgid "Desc"
|
|
175
|
+
msgstr "Décroissant"
|
|
176
|
+
|
|
177
|
+
msgid "Drag to reorder"
|
|
178
|
+
msgstr "Glisser pour réorganiser"
|
|
179
|
+
|
|
180
|
+
msgid "Drag to reorder (not saved)"
|
|
181
|
+
msgstr "Glisser pour réorganiser (non enregistré)"
|
|
182
|
+
|
|
183
|
+
msgid "Edit"
|
|
184
|
+
msgstr "Modifier"
|
|
185
|
+
|
|
186
|
+
msgid "Email"
|
|
187
|
+
msgstr "E-mail"
|
|
188
|
+
|
|
189
|
+
msgid "Empty"
|
|
190
|
+
msgstr "Vide"
|
|
191
|
+
|
|
192
|
+
msgid "Everything is selected."
|
|
193
|
+
msgstr "Tout est sélectionné."
|
|
194
|
+
|
|
195
|
+
msgid "Export"
|
|
196
|
+
msgstr "Exporter"
|
|
197
|
+
|
|
198
|
+
msgid "Filters"
|
|
199
|
+
msgstr "Filtres"
|
|
200
|
+
|
|
201
|
+
msgid "First page"
|
|
202
|
+
msgstr "Première page"
|
|
203
|
+
|
|
204
|
+
msgid "From"
|
|
205
|
+
msgstr "Du"
|
|
206
|
+
|
|
207
|
+
msgid "History"
|
|
208
|
+
msgstr "Historique"
|
|
209
|
+
|
|
210
|
+
msgid "Language"
|
|
211
|
+
msgstr "Langue"
|
|
212
|
+
|
|
213
|
+
msgid "Last page"
|
|
214
|
+
msgstr "Dernière page"
|
|
215
|
+
|
|
216
|
+
msgid "Light mode"
|
|
217
|
+
msgstr "Mode clair"
|
|
218
|
+
|
|
219
|
+
msgid "New"
|
|
220
|
+
msgstr "Nouveau"
|
|
221
|
+
|
|
222
|
+
msgid "Next month"
|
|
223
|
+
msgstr "Mois suivant"
|
|
224
|
+
|
|
225
|
+
msgid "Next page"
|
|
226
|
+
msgstr "Page suivante"
|
|
227
|
+
|
|
228
|
+
msgid "No"
|
|
229
|
+
msgstr "Non"
|
|
230
|
+
|
|
231
|
+
#, python-format
|
|
232
|
+
msgid "No %(label)s."
|
|
233
|
+
msgstr "Aucun %(label)s."
|
|
234
|
+
|
|
235
|
+
msgid "No data."
|
|
236
|
+
msgstr "Aucune donnée."
|
|
237
|
+
|
|
238
|
+
msgid "No items selected."
|
|
239
|
+
msgstr "Aucun élément sélectionné."
|
|
240
|
+
|
|
241
|
+
msgid "No matches."
|
|
242
|
+
msgstr "Aucun résultat."
|
|
243
|
+
|
|
244
|
+
msgid "No options."
|
|
245
|
+
msgstr "Aucune option."
|
|
246
|
+
|
|
247
|
+
msgid "No panels configured."
|
|
248
|
+
msgstr "Aucun panneau configuré."
|
|
249
|
+
|
|
250
|
+
msgid "No recent activity."
|
|
251
|
+
msgstr "Aucune activité récente."
|
|
252
|
+
|
|
253
|
+
msgid "No records."
|
|
254
|
+
msgstr "Aucun enregistrement."
|
|
255
|
+
|
|
256
|
+
msgid "No resources registered."
|
|
257
|
+
msgstr "Aucune ressource enregistrée."
|
|
258
|
+
|
|
259
|
+
msgid "No widgets configured."
|
|
260
|
+
msgstr "Aucun widget configuré."
|
|
261
|
+
|
|
262
|
+
msgid "Not empty"
|
|
263
|
+
msgstr "Non vide"
|
|
264
|
+
|
|
265
|
+
msgid "Not found"
|
|
266
|
+
msgstr "Introuvable"
|
|
267
|
+
|
|
268
|
+
msgid "Notifications"
|
|
269
|
+
msgstr "Notifications"
|
|
270
|
+
|
|
271
|
+
msgid "Open calendar"
|
|
272
|
+
msgstr "Ouvrir le calendrier"
|
|
273
|
+
|
|
274
|
+
msgid "Open menu"
|
|
275
|
+
msgstr "Ouvrir le menu"
|
|
276
|
+
|
|
277
|
+
msgid "Page"
|
|
278
|
+
msgstr "Page"
|
|
279
|
+
|
|
280
|
+
#, python-format
|
|
281
|
+
msgid "Page %(page)d of %(pages)d"
|
|
282
|
+
msgstr "Page %(page)d sur %(pages)d"
|
|
283
|
+
|
|
284
|
+
msgid "Password"
|
|
285
|
+
msgstr "Mot de passe"
|
|
286
|
+
|
|
287
|
+
msgid "Past 7 days"
|
|
288
|
+
msgstr "7 derniers jours"
|
|
289
|
+
|
|
290
|
+
msgid "Permission denied"
|
|
291
|
+
msgstr "Accès refusé"
|
|
292
|
+
|
|
293
|
+
msgid "Platform"
|
|
294
|
+
msgstr "Plateforme"
|
|
295
|
+
|
|
296
|
+
msgid "PolyAdmin"
|
|
297
|
+
msgstr "PolyAdmin"
|
|
298
|
+
|
|
299
|
+
msgid "Previous month"
|
|
300
|
+
msgstr "Mois précédent"
|
|
301
|
+
|
|
302
|
+
msgid "Previous page"
|
|
303
|
+
msgstr "Page précédente"
|
|
304
|
+
|
|
305
|
+
msgid "Remove"
|
|
306
|
+
msgstr "Retirer"
|
|
307
|
+
|
|
308
|
+
#, python-format
|
|
309
|
+
msgid "Remove %(label)s"
|
|
310
|
+
msgstr "Retirer %(label)s"
|
|
311
|
+
|
|
312
|
+
#, python-format
|
|
313
|
+
msgid "Remove this %(label)s?"
|
|
314
|
+
msgstr "Retirer « %(label)s » ?"
|
|
315
|
+
|
|
316
|
+
msgid "Rows per page"
|
|
317
|
+
msgstr "Lignes par page"
|
|
318
|
+
|
|
319
|
+
msgid "Save"
|
|
320
|
+
msgstr "Enregistrer"
|
|
321
|
+
|
|
322
|
+
msgid "Save and add another"
|
|
323
|
+
msgstr "Enregistrer et ajouter un autre"
|
|
324
|
+
|
|
325
|
+
msgid "Save and continue editing"
|
|
326
|
+
msgstr "Enregistrer et continuer la modification"
|
|
327
|
+
|
|
328
|
+
msgid "Save as new"
|
|
329
|
+
msgstr "Enregistrer comme nouveau"
|
|
330
|
+
|
|
331
|
+
#, python-format
|
|
332
|
+
msgid "Save to add %(label)s."
|
|
333
|
+
msgstr "Enregistrer pour ajouter %(label)s."
|
|
334
|
+
|
|
335
|
+
msgid "Search…"
|
|
336
|
+
msgstr "Rechercher…"
|
|
337
|
+
|
|
338
|
+
msgid "Security check failed"
|
|
339
|
+
msgstr "Échec de la vérification de sécurité"
|
|
340
|
+
|
|
341
|
+
msgid "Select all"
|
|
342
|
+
msgstr "Tout sélectionner"
|
|
343
|
+
|
|
344
|
+
msgid "Select row"
|
|
345
|
+
msgstr "Sélectionner la ligne"
|
|
346
|
+
|
|
347
|
+
#, python-format
|
|
348
|
+
msgid "Select the %(num)d matching"
|
|
349
|
+
msgid_plural "Select all %(num)d matching"
|
|
350
|
+
msgstr[0] "Sélectionner le %(num)d élément correspondant"
|
|
351
|
+
msgstr[1] "Sélectionner tous les %(num)d éléments correspondants"
|
|
352
|
+
|
|
353
|
+
msgid "Selected records"
|
|
354
|
+
msgstr "Enregistrements sélectionnés"
|
|
355
|
+
|
|
356
|
+
msgid "Select…"
|
|
357
|
+
msgstr "Sélectionner…"
|
|
358
|
+
|
|
359
|
+
msgid "Sign in"
|
|
360
|
+
msgstr "Se connecter"
|
|
361
|
+
|
|
362
|
+
#, python-format
|
|
363
|
+
msgid "Sign in to %(site)s"
|
|
364
|
+
msgstr "Se connecter à %(site)s"
|
|
365
|
+
|
|
366
|
+
msgid "Sign out"
|
|
367
|
+
msgstr "Se déconnecter"
|
|
368
|
+
|
|
369
|
+
msgid "Sign-in could not be completed. Please try again."
|
|
370
|
+
msgstr "La connexion n'a pas pu aboutir. Veuillez réessayer."
|
|
371
|
+
|
|
372
|
+
msgid "Sign-in required"
|
|
373
|
+
msgstr "Connexion requise"
|
|
374
|
+
|
|
375
|
+
msgid "Signed in"
|
|
376
|
+
msgstr "Connecté"
|
|
377
|
+
|
|
378
|
+
msgid "Superuser"
|
|
379
|
+
msgstr "Superutilisateur"
|
|
380
|
+
|
|
381
|
+
msgid "That email and password don't match an account."
|
|
382
|
+
msgstr "Cette adresse e-mail et ce mot de passe ne correspondent à aucun compte."
|
|
383
|
+
|
|
384
|
+
msgid "That record doesn't exist, or it was deleted."
|
|
385
|
+
msgstr "Cet enregistrement n'existe pas, ou a été supprimé."
|
|
386
|
+
|
|
387
|
+
#, python-format
|
|
388
|
+
msgid "The %(num)d matching this filter is selected."
|
|
389
|
+
msgid_plural "All %(num)d matching this filter are selected."
|
|
390
|
+
msgstr[0] "Le %(num)d élément correspondant à ce filtre est sélectionné."
|
|
391
|
+
msgstr[1] "Tous les %(num)d éléments correspondant à ce filtre sont sélectionnés."
|
|
392
|
+
|
|
393
|
+
#, python-format
|
|
394
|
+
msgid "The %(num)d on this page is selected."
|
|
395
|
+
msgid_plural "All %(num)d on this page are selected."
|
|
396
|
+
msgstr[0] "Le %(num)d de cette page est sélectionné."
|
|
397
|
+
msgstr[1] "Tous les %(num)d de cette page sont sélectionnés."
|
|
398
|
+
|
|
399
|
+
msgid "The selection changed since you reviewed it."
|
|
400
|
+
msgstr "La sélection a changé depuis votre vérification."
|
|
401
|
+
|
|
402
|
+
msgid "These records must be removed first:"
|
|
403
|
+
msgstr "Ces enregistrements doivent d'abord être supprimés :"
|
|
404
|
+
|
|
405
|
+
msgid "This action cannot be undone."
|
|
406
|
+
msgstr "Cette action est irréversible."
|
|
407
|
+
|
|
408
|
+
msgid "This can't be deleted"
|
|
409
|
+
msgstr "Suppression impossible"
|
|
410
|
+
|
|
411
|
+
msgid "This month"
|
|
412
|
+
msgstr "Ce mois-ci"
|
|
413
|
+
|
|
414
|
+
msgid "This page expired before the form was submitted. Reload and try again."
|
|
415
|
+
msgstr "Cette page a expiré avant l'envoi du formulaire. Rechargez la page et réessayez."
|
|
416
|
+
|
|
417
|
+
msgid "This will also delete"
|
|
418
|
+
msgstr "Seront aussi supprimés"
|
|
419
|
+
|
|
420
|
+
msgid "This year"
|
|
421
|
+
msgstr "Cette année"
|
|
422
|
+
|
|
423
|
+
msgid "To"
|
|
424
|
+
msgstr "Au"
|
|
425
|
+
|
|
426
|
+
msgid "Today"
|
|
427
|
+
msgstr "Aujourd'hui"
|
|
428
|
+
|
|
429
|
+
msgid "Toggle dark mode"
|
|
430
|
+
msgstr "Basculer le mode sombre"
|
|
431
|
+
|
|
432
|
+
msgid "Toggle sidebar"
|
|
433
|
+
msgstr "Basculer la barre latérale"
|
|
434
|
+
|
|
435
|
+
msgid "Total"
|
|
436
|
+
msgstr "Total"
|
|
437
|
+
|
|
438
|
+
msgid "View"
|
|
439
|
+
msgstr "Voir"
|
|
440
|
+
|
|
441
|
+
msgid "Welcome back"
|
|
442
|
+
msgstr "Bon retour"
|
|
443
|
+
|
|
444
|
+
msgid "XLSX"
|
|
445
|
+
msgstr "XLSX"
|
|
446
|
+
|
|
447
|
+
msgid "Yes"
|
|
448
|
+
msgstr "Oui"
|
|
449
|
+
|
|
450
|
+
msgid "You have been signed out."
|
|
451
|
+
msgstr "Vous avez été déconnecté."
|
|
452
|
+
|
|
453
|
+
msgid "You need to be signed in to view this page."
|
|
454
|
+
msgstr "Vous devez être connecté pour voir cette page."
|
|
455
|
+
|
|
456
|
+
#, python-format
|
|
457
|
+
msgid "Your account doesn't have permission to delete: %(types)s"
|
|
458
|
+
msgstr "Votre compte n'a pas le droit de supprimer : %(types)s"
|
|
459
|
+
|
|
460
|
+
msgid "Your account doesn't have permission to do that."
|
|
461
|
+
msgstr "Votre compte n'a pas la permission d'effectuer cette action."
|
|
462
|
+
|
|
463
|
+
msgid "create"
|
|
464
|
+
msgstr "création"
|
|
465
|
+
|
|
466
|
+
msgid "delete"
|
|
467
|
+
msgstr "suppression"
|
|
468
|
+
|
|
469
|
+
msgid "m@example.com"
|
|
470
|
+
msgstr "m@example.com"
|
|
471
|
+
|
|
472
|
+
msgid "no change"
|
|
473
|
+
msgstr "aucun changement"
|
|
474
|
+
|
|
475
|
+
msgid "system"
|
|
476
|
+
msgstr "système"
|
|
477
|
+
|
|
478
|
+
msgid "update"
|
|
479
|
+
msgstr "modification"
|
|
480
|
+
|
|
481
|
+
#, python-format
|
|
482
|
+
msgid "…and %(num)d more"
|
|
483
|
+
msgid_plural "…and %(num)d more"
|
|
484
|
+
msgstr[0] "…et %(num)d autre"
|
|
485
|
+
msgstr[1] "…et %(num)d autres"
|
|
486
|
+
|