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.
Files changed (104) hide show
  1. polyadmin/__init__.py +47 -0
  2. polyadmin/core/__init__.py +0 -0
  3. polyadmin/core/_async.py +21 -0
  4. polyadmin/core/action.py +135 -0
  5. polyadmin/core/admin.py +129 -0
  6. polyadmin/core/audit.py +65 -0
  7. polyadmin/core/auth.py +52 -0
  8. polyadmin/core/authorization.py +48 -0
  9. polyadmin/core/csrf.py +70 -0
  10. polyadmin/core/dashboard.py +41 -0
  11. polyadmin/core/delete.py +146 -0
  12. polyadmin/core/exporter.py +136 -0
  13. polyadmin/core/field.py +201 -0
  14. polyadmin/core/filter.py +305 -0
  15. polyadmin/core/inline.py +97 -0
  16. polyadmin/core/login.py +110 -0
  17. polyadmin/core/model_admin.py +349 -0
  18. polyadmin/core/page.py +62 -0
  19. polyadmin/core/pagination.py +70 -0
  20. polyadmin/core/query.py +243 -0
  21. polyadmin/core/relation.py +40 -0
  22. polyadmin/core/slug.py +57 -0
  23. polyadmin/core/template_context.py +679 -0
  24. polyadmin/core/widget.py +280 -0
  25. polyadmin/fastapi/__init__.py +3 -0
  26. polyadmin/fastapi/audit.py +52 -0
  27. polyadmin/fastapi/auth.py +112 -0
  28. polyadmin/fastapi/csrf.py +93 -0
  29. polyadmin/fastapi/deletes.py +76 -0
  30. polyadmin/fastapi/errors.py +93 -0
  31. polyadmin/fastapi/handlers.py +798 -0
  32. polyadmin/fastapi/inlines.py +177 -0
  33. polyadmin/fastapi/locale.py +128 -0
  34. polyadmin/fastapi/login.py +110 -0
  35. polyadmin/fastapi/pages.py +97 -0
  36. polyadmin/fastapi/relations.py +264 -0
  37. polyadmin/fastapi/responses.py +55 -0
  38. polyadmin/fastapi/router.py +174 -0
  39. polyadmin/fastapi/static.py +21 -0
  40. polyadmin/i18n/__init__.py +50 -0
  41. polyadmin/i18n/context.py +54 -0
  42. polyadmin/i18n/negotiation.py +56 -0
  43. polyadmin/i18n/setup.py +81 -0
  44. polyadmin/i18n/translator.py +124 -0
  45. polyadmin/locale/fr/LC_MESSAGES/polyadmin.mo +0 -0
  46. polyadmin/locale/fr/LC_MESSAGES/polyadmin.po +486 -0
  47. polyadmin/locale/polyadmin.pot +485 -0
  48. polyadmin/locale/ru/LC_MESSAGES/polyadmin.mo +0 -0
  49. polyadmin/locale/ru/LC_MESSAGES/polyadmin.po +496 -0
  50. polyadmin/templates/admin/base.html +91 -0
  51. polyadmin/templates/admin/components/action_confirm_modal.html +86 -0
  52. polyadmin/templates/admin/components/csrf-field.html +5 -0
  53. polyadmin/templates/admin/components/error_fragment.html +6 -0
  54. polyadmin/templates/admin/components/field.html +60 -0
  55. polyadmin/templates/admin/components/form_wrapper.html +131 -0
  56. polyadmin/templates/admin/components/icons.html +67 -0
  57. polyadmin/templates/admin/components/inline.html +251 -0
  58. polyadmin/templates/admin/components/inline_fragment.html +2 -0
  59. polyadmin/templates/admin/components/list_content.html +54 -0
  60. polyadmin/templates/admin/components/lookup_results.html +19 -0
  61. polyadmin/templates/admin/components/search.html +19 -0
  62. polyadmin/templates/admin/components/toasts.html +151 -0
  63. polyadmin/templates/admin/components/ui/breadcrumb.html +30 -0
  64. polyadmin/templates/admin/components/ui/bulk-actions.html +69 -0
  65. polyadmin/templates/admin/components/ui/calendar.html +175 -0
  66. polyadmin/templates/admin/components/ui/combobox.html +82 -0
  67. polyadmin/templates/admin/components/ui/delete-preview.html +37 -0
  68. polyadmin/templates/admin/components/ui/dropdown-menu.html +71 -0
  69. polyadmin/templates/admin/components/ui/field.html +110 -0
  70. polyadmin/templates/admin/components/ui/filter-panel.html +155 -0
  71. polyadmin/templates/admin/components/ui/locale-switcher.html +30 -0
  72. polyadmin/templates/admin/components/ui/multi-select.html +253 -0
  73. polyadmin/templates/admin/components/ui/pagination.html +81 -0
  74. polyadmin/templates/admin/components/ui/radio-group.html +28 -0
  75. polyadmin/templates/admin/components/ui/select.html +165 -0
  76. polyadmin/templates/admin/components/ui/sidebar.html +175 -0
  77. polyadmin/templates/admin/components/ui/slider.html +22 -0
  78. polyadmin/templates/admin/components/ui/switch.html +36 -0
  79. polyadmin/templates/admin/components/ui/table.html +221 -0
  80. polyadmin/templates/admin/components/ui/theme-toggle.html +33 -0
  81. polyadmin/templates/admin/dashboard.html +35 -0
  82. polyadmin/templates/admin/error.html +33 -0
  83. polyadmin/templates/admin/login.html +94 -0
  84. polyadmin/templates/admin/resource/delete.html +29 -0
  85. polyadmin/templates/admin/resource/delete_selected.html +49 -0
  86. polyadmin/templates/admin/resource/detail.html +78 -0
  87. polyadmin/templates/admin/resource/form.html +5 -0
  88. polyadmin/templates/admin/resource/list.html +5 -0
  89. polyadmin/templates/admin/theme.html +372 -0
  90. polyadmin/templates/admin/widgets/activity.html +8 -0
  91. polyadmin/templates/admin/widgets/chart.html +15 -0
  92. polyadmin/templates/admin/widgets/donut.html +59 -0
  93. polyadmin/templates/admin/widgets/metric.html +1 -0
  94. polyadmin/templates/admin/widgets/progress.html +7 -0
  95. polyadmin/templates/admin/widgets/stat.html +22 -0
  96. polyadmin/templates/admin/widgets/table.html +29 -0
  97. polyadmin/templates/admin/widgets/tabs.html +34 -0
  98. polyadmin/templates/admin/widgets/timeline.html +21 -0
  99. polyadmin/templating.py +528 -0
  100. polyadmin/ui.py +817 -0
  101. polyadmin-0.1.0b1.dist-info/METADATA +239 -0
  102. polyadmin-0.1.0b1.dist-info/RECORD +104 -0
  103. polyadmin-0.1.0b1.dist-info/WHEEL +4 -0
  104. polyadmin-0.1.0b1.dist-info/licenses/LICENSE +21 -0
@@ -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))
@@ -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
+