@justanarthur/payload-plugin-translator 1.3.20 → 3.0.3

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 (135) hide show
  1. package/README.md +132 -215
  2. package/dist/client.css +41 -0
  3. package/dist/client.d.ts +6 -0
  4. package/dist/client.js +320 -0
  5. package/dist/index.d.ts +63 -11
  6. package/dist/index.js +275 -82
  7. package/dist/jobs.d.ts +50 -0
  8. package/dist/jobs.js +28 -0
  9. package/dist/resolvers/copy.d.ts +19 -3
  10. package/dist/resolvers/copy.js +9 -13
  11. package/dist/resolvers/google.d.ts +23 -11
  12. package/dist/resolvers/google.js +10 -46
  13. package/dist/resolvers/libreTranslate.d.ts +23 -15
  14. package/dist/resolvers/libreTranslate.js +10 -47
  15. package/dist/resolvers/openAI.d.ts +29 -21
  16. package/dist/resolvers/openAI.js +9 -134
  17. package/dist/resolvers/types.d.ts +14 -16
  18. package/dist/resolvers/types.js +0 -3
  19. package/dist/shared/chunk-97ktc51q.js +15 -0
  20. package/dist/shared/chunk-ecnph6hw.js +205 -0
  21. package/dist/shared/chunk-frnemj69.js +6 -0
  22. package/dist/shared/chunk-fsveh29m.js +55 -0
  23. package/dist/shared/chunk-q51q8rd2.js +855 -0
  24. package/dist/shared/chunk-zhrw0jnz.js +57 -0
  25. package/dist/types.d.ts +32 -22
  26. package/dist/types.js +0 -3
  27. package/package.json +89 -63
  28. package/dist/client/api/index.d.ts +0 -8
  29. package/dist/client/api/index.d.ts.map +0 -1
  30. package/dist/client/api/index.js +0 -28
  31. package/dist/client/api/index.js.map +0 -1
  32. package/dist/client/components/CustomButton/CustomButtonWithTranslator.d.ts +0 -5
  33. package/dist/client/components/CustomButton/CustomButtonWithTranslator.d.ts.map +0 -1
  34. package/dist/client/components/CustomButton/CustomButtonWithTranslator.js +0 -28
  35. package/dist/client/components/CustomButton/CustomButtonWithTranslator.js.map +0 -1
  36. package/dist/client/components/CustomButton/index.d.ts +0 -3
  37. package/dist/client/components/CustomButton/index.d.ts.map +0 -1
  38. package/dist/client/components/CustomButton/index.js +0 -10
  39. package/dist/client/components/CustomButton/index.js.map +0 -1
  40. package/dist/client/components/CustomButton/styles.scss +0 -5
  41. package/dist/client/components/LocaleLabel/LocaleLabel.d.ts +0 -5
  42. package/dist/client/components/LocaleLabel/LocaleLabel.d.ts.map +0 -1
  43. package/dist/client/components/LocaleLabel/LocaleLabel.js +0 -28
  44. package/dist/client/components/LocaleLabel/LocaleLabel.js.map +0 -1
  45. package/dist/client/components/LocaleLabel/index.d.ts +0 -2
  46. package/dist/client/components/LocaleLabel/index.d.ts.map +0 -1
  47. package/dist/client/components/LocaleLabel/index.js +0 -3
  48. package/dist/client/components/LocaleLabel/index.js.map +0 -1
  49. package/dist/client/components/ResolverButton/ResolverButton.d.ts +0 -5
  50. package/dist/client/components/ResolverButton/ResolverButton.d.ts.map +0 -1
  51. package/dist/client/components/ResolverButton/ResolverButton.js +0 -16
  52. package/dist/client/components/ResolverButton/ResolverButton.js.map +0 -1
  53. package/dist/client/components/ResolverButton/index.d.ts +0 -2
  54. package/dist/client/components/ResolverButton/index.d.ts.map +0 -1
  55. package/dist/client/components/ResolverButton/index.js +0 -3
  56. package/dist/client/components/ResolverButton/index.js.map +0 -1
  57. package/dist/client/components/TranslatorModal/Content.d.ts +0 -2
  58. package/dist/client/components/TranslatorModal/Content.d.ts.map +0 -1
  59. package/dist/client/components/TranslatorModal/Content.js +0 -60
  60. package/dist/client/components/TranslatorModal/Content.js.map +0 -1
  61. package/dist/client/components/TranslatorModal/TranslatorModal.d.ts +0 -3
  62. package/dist/client/components/TranslatorModal/TranslatorModal.d.ts.map +0 -1
  63. package/dist/client/components/TranslatorModal/TranslatorModal.js +0 -26
  64. package/dist/client/components/TranslatorModal/TranslatorModal.js.map +0 -1
  65. package/dist/client/components/TranslatorModal/index.d.ts +0 -2
  66. package/dist/client/components/TranslatorModal/index.d.ts.map +0 -1
  67. package/dist/client/components/TranslatorModal/index.js +0 -3
  68. package/dist/client/components/TranslatorModal/index.js.map +0 -1
  69. package/dist/client/components/TranslatorModal/styles.scss +0 -76
  70. package/dist/client/providers/Translator/TranslatorProvider.d.ts +0 -5
  71. package/dist/client/providers/Translator/TranslatorProvider.d.ts.map +0 -1
  72. package/dist/client/providers/Translator/TranslatorProvider.js +0 -109
  73. package/dist/client/providers/Translator/TranslatorProvider.js.map +0 -1
  74. package/dist/client/providers/Translator/context.d.ts +0 -21
  75. package/dist/client/providers/Translator/context.d.ts.map +0 -1
  76. package/dist/client/providers/Translator/context.js +0 -9
  77. package/dist/client/providers/Translator/context.js.map +0 -1
  78. package/dist/exports/client.d.ts +0 -2
  79. package/dist/exports/client.d.ts.map +0 -1
  80. package/dist/exports/client.js +0 -3
  81. package/dist/exports/client.js.map +0 -1
  82. package/dist/i18n-translations.d.ts +0 -87
  83. package/dist/i18n-translations.d.ts.map +0 -1
  84. package/dist/i18n-translations.js +0 -88
  85. package/dist/i18n-translations.js.map +0 -1
  86. package/dist/index.d.ts.map +0 -1
  87. package/dist/index.js.map +0 -1
  88. package/dist/resolvers/copy.d.ts.map +0 -1
  89. package/dist/resolvers/copy.js.map +0 -1
  90. package/dist/resolvers/google.d.ts.map +0 -1
  91. package/dist/resolvers/google.js.map +0 -1
  92. package/dist/resolvers/libreTranslate.d.ts.map +0 -1
  93. package/dist/resolvers/libreTranslate.js.map +0 -1
  94. package/dist/resolvers/openAI.d.ts.map +0 -1
  95. package/dist/resolvers/openAI.js.map +0 -1
  96. package/dist/resolvers/types.d.ts.map +0 -1
  97. package/dist/resolvers/types.js.map +0 -1
  98. package/dist/translate/endpoint.d.ts +0 -3
  99. package/dist/translate/endpoint.d.ts.map +0 -1
  100. package/dist/translate/endpoint.js +0 -23
  101. package/dist/translate/endpoint.js.map +0 -1
  102. package/dist/translate/findEntityWithConfig.d.ts +0 -16
  103. package/dist/translate/findEntityWithConfig.d.ts.map +0 -1
  104. package/dist/translate/findEntityWithConfig.js +0 -34
  105. package/dist/translate/findEntityWithConfig.js.map +0 -1
  106. package/dist/translate/operation.d.ts +0 -9
  107. package/dist/translate/operation.d.ts.map +0 -1
  108. package/dist/translate/operation.js +0 -78
  109. package/dist/translate/operation.js.map +0 -1
  110. package/dist/translate/traverseFields.d.ts +0 -17
  111. package/dist/translate/traverseFields.d.ts.map +0 -1
  112. package/dist/translate/traverseFields.js +0 -233
  113. package/dist/translate/traverseFields.js.map +0 -1
  114. package/dist/translate/traverseRichText.d.ts +0 -10
  115. package/dist/translate/traverseRichText.d.ts.map +0 -1
  116. package/dist/translate/traverseRichText.js +0 -24
  117. package/dist/translate/traverseRichText.js.map +0 -1
  118. package/dist/translate/types.d.ts +0 -27
  119. package/dist/translate/types.d.ts.map +0 -1
  120. package/dist/translate/types.js +0 -3
  121. package/dist/translate/types.js.map +0 -1
  122. package/dist/translate/updateEntity.d.ts +0 -14
  123. package/dist/translate/updateEntity.d.ts.map +0 -1
  124. package/dist/translate/updateEntity.js +0 -26
  125. package/dist/translate/updateEntity.js.map +0 -1
  126. package/dist/types.d.ts.map +0 -1
  127. package/dist/types.js.map +0 -1
  128. package/dist/utils/chunkArray.d.ts +0 -2
  129. package/dist/utils/chunkArray.d.ts.map +0 -1
  130. package/dist/utils/chunkArray.js +0 -7
  131. package/dist/utils/chunkArray.js.map +0 -1
  132. package/dist/utils/isEmpty.d.ts +0 -2
  133. package/dist/utils/isEmpty.d.ts.map +0 -1
  134. package/dist/utils/isEmpty.js +0 -8
  135. package/dist/utils/isEmpty.js.map +0 -1
package/README.md CHANGED
@@ -1,215 +1,132 @@
1
- # Translator plugin for Payload
2
-
3
- ## Install
4
-
5
- `pnpm add @justanarthur/payload-plugin-translator`
6
-
7
- ## Video
8
-
9
- https://github.com/r1tsuu/payload-plugin-translator/assets/64744993/d39aeba4-bafc-4c3b-838e-9abc5cf1d64a
10
-
11
- ## Features:
12
-
13
- 1. A flexible structure with [resolvers](https://github.com/r1tsuu/payload-plugin-translator/tree/6d0c8098467f9b5e757bf9fd8cfe63ff5da68d5b/plugin/src/resolvers) that allows you to apply any kind of transformation to your localizated data.
14
- 2. Can be used not only from the admin panel, but within Local API as well. [Example of the hook](#example-of-the-hook-that-uses-local-operation-to-copy-the-doc-data-to-other-locales) that automatically fills the other locales data on create
15
- 3. Out of the box supports 3 resolvers - Copy, Google Translate, OpenAI and your own can be written easily.
16
- 4. Works with any nested document structure and 2 Rich Text editor adapters - Lexical and Slate.
17
- 5. You can omit some fields to translate. [Documentation](#omitting-fields)
18
-
19
- ## Usage:
20
-
21
- ```ts
22
- import { buildConfig } from 'payload/config';
23
- import {
24
- translator,
25
- copyResolver,
26
- googleResolver,
27
- openAIResolver,
28
- libreResolver,
29
- } from '@payload-enchants/translator';
30
-
31
- export default buildConfig({
32
- plugins: [
33
- translator({
34
- // collections with the enabled translator in the admin UI
35
- collections: ['posts', 'small-posts'],
36
- // globals with the enabled translator in the admin UI
37
- globals: [],
38
- // add resolvers that you want to include, examples on how to write your own in ./plugin/src/resolvers
39
- resolvers: [
40
- copyResolver(),
41
- googleResolver({
42
- apiKey: process.env.GOOGLE_API_KEY!,
43
- }),
44
- openAIResolver({
45
- apiKey: process.env.OPENAI_KEY!,
46
- }),
47
- libreResolver({
48
- apiKey: process.env.LIBRE_KEY!,
49
- }),
50
- ],
51
- }),
52
- ],
53
- });
54
- ```
55
-
56
- ## Resolvers
57
-
58
- ### OpenAI
59
-
60
- #### Config:
61
-
62
- ```ts
63
- export type OpenAIResolverConfig = {
64
- apiKey: string; // API key
65
- chunkLength?: number; // How many texts to include into 1 request, default: 100
66
- model?: string; // model, default: 'gpt-3.5-turbo'
67
- promt?: OpenAIPrompt; // custom prompt
68
- };
69
- ```
70
-
71
- ### Custom prompt:
72
-
73
- ```ts
74
- export type OpenAIPrompt = (args: {
75
- localeFrom: string;
76
- localeTo: string;
77
- texts: string[];
78
- }) => string;
79
-
80
- // Default
81
- const defaultPromt: OpenAIPrompt = ({ localeFrom, localeTo, texts }) => {
82
- return `Translate me the following array: ${JSON.stringify(texts)} in locale=${localeFrom} to locale ${localeTo}, respond me with the same array structure`;
83
- };
84
- ```
85
-
86
- ### Google
87
-
88
- #### Config:
89
-
90
- ```ts
91
- export type GoogleResolverConfig = {
92
- apiKey: string; // API key
93
- chunkLength?: number; / /How many texts to include into 1 request, default: 100
94
- };
95
- ```
96
-
97
- ### Writing your own
98
-
99
- ```ts
100
- import type { TranslateResolver } from '@payload-enchants/translator/resolvers/types';
101
-
102
- const myResolver: TranslateResolver = {
103
- key: 'my',
104
- resolve: async (args) => {
105
- const { localeFrom, localeTo, req, texts } = args;
106
- // here you can apply any kind of transformation to incoming texts, could be as well API call to a service.
107
- const transformed = texts.map((each) => `${each} translated to ${localeTo}`);
108
-
109
- return {
110
- success: true,
111
- translatedTexts: transformed,
112
- };
113
- },
114
- };
115
-
116
- export default buildConfig({
117
- plugins: [
118
- payloadPluginTranslator({
119
- collections: ['posts', 'small-posts'],
120
- globals: [],
121
- resolvers: [myResolver],
122
- }),
123
- ],
124
- // apply translations that will be used for your resolver in the admin UI
125
- i18n: {
126
- supportedLanguages: { en },
127
- translations: {
128
- en: {
129
- 'plugin-translator': {
130
- resolver_my_buttonLabel: 'Google Translate',
131
- resolver_my_errorMessage: 'An error occurred when trying to translate the data',
132
- resolver_my_modalTitle: 'Choose the locale to translate from',
133
- resolver_my_submitButtonLabelEmpty: 'Translate only empty fields',
134
- resolver_my_submitButtonLabelFull: 'Translate all',
135
- resolver_my_successMessage: 'Successfully translated. Press "Save" to apply the changes.',
136
- },
137
- },
138
- },
139
- },
140
- });
141
- ```
142
-
143
- ## Using the Local API
144
-
145
- ```ts
146
- import { translateOperation } from '@payload-enchants/translator';
147
-
148
- const translateResult = await translateOperation({
149
- collectionSlug: 'posts', // or globalSlug if globals,
150
- emptyOnly: false, // optional, should translate all the fields values or only fields that are empty, by default false.
151
- id: postDefaultLocale.id, // pass the doc id if it's a collection
152
- locale: 'de', // locale to translate to
153
- localeFrom: 'en', // locale to translate from
154
- payload, // payload instance, you can get it with getPayload or req.payload in hooks
155
- req, // PayloadRequest, can be used instead of payload, more appreciable than payload if you have it
156
- overrideAccess: false, //
157
- resolver: 'copy', // pass resolver key
158
- update: true, // optional, should update immediately or just return the translated result, default false
159
- data: {}, // Optional, if you want to translate from your data passed here instead of the current doc, should be in "localeFrom" locale, for example { title: "Hello" }
160
- });
161
- ```
162
-
163
- ### Example of the hook that uses local operation to copy the doc data to other locales
164
-
165
- ```ts
166
- import type { CollectionAfterChangeHook } from 'payload/types';
167
- import { translateOperation } from '@payload-enchants/translator';
168
-
169
- export const copyOtherLocales: CollectionAfterChangeHook = async ({
170
- collection,
171
- doc,
172
- operation,
173
- req,
174
- }) => {
175
- if (operation !== 'create') return;
176
-
177
- const locale = req.locale;
178
-
179
- if (!locale || !req.payload.config.localization) return;
180
-
181
- const otherLocales = req.payload.config.localization.locales.filter(
182
- (each) => each.code !== locale,
183
- );
184
-
185
- const { id } = doc;
186
-
187
- for (const { code } of otherLocales) {
188
- await translateOperation({
189
- collectionSlug: collection.slug,
190
- data: doc,
191
- id,
192
- locale: code,
193
- localeFrom: locale,
194
- req,
195
- resolver: 'copy',
196
- update: true,
197
- });
198
- }
199
- };
200
- ```
201
-
202
- ### Omitting fields
203
-
204
- To omit a specific field for translation, simply add `custom.translatorSkip = true` to the field's config.
205
-
206
- ```ts
207
- const field = {
208
- custom: {
209
- translatorSkip: true,
210
- },
211
- localized: true,
212
- name: 'skip',
213
- type: 'text',
214
- };
215
- ```
1
+ # @justanarthur/payload-plugin-translator
2
+
3
+ [Payload CMS](https://payloadcms.com) plugin that keeps localized docs in sync. On every save of a
4
+ listed collection or global, it schedules background jobs that translate each field from the source
5
+ locale into every other declared locale, using a configurable resolver (Google Translate, OpenAI,
6
+ LibreTranslate, or your own).
7
+
8
+ Editor UX: the publish and save buttons in the affected collections/globals are swapped for a
9
+ custom variant that surfaces the job status. Locales without translations stay empty until the job
10
+ finishes — the plugin never blocks the editor's save.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add @justanarthur/payload-plugin-translator
16
+ ```
17
+
18
+ Requires `payload@^3.85.0` and `react@^19`. The plugin reads your `config.localization.locales` —
19
+ it short-circuits (returns the config unchanged) when there's only one locale or no localization.
20
+
21
+ ## Quick start
22
+
23
+ ```ts
24
+ // payload.config.ts
25
+ import { buildConfig } from 'payload'
26
+ import { translator, openAIResolver } from '@justanarthur/payload-plugin-translator'
27
+
28
+ export default buildConfig({
29
+ plugins: [
30
+ translator({
31
+ collections: ['pages', 'posts'],
32
+ globals: ['header', 'footer'],
33
+ resolvers: [
34
+ openAIResolver({ apiKey: process.env.OPENAI_API_KEY! })
35
+ ]
36
+ })
37
+ ]
38
+ })
39
+ ```
40
+
41
+ That's the whole integration. The first save of a Page doc schedules one translate job per non-source
42
+ locale; the job runs against your resolver and writes each translated field back into the
43
+ collection's `_locales` row.
44
+
45
+ ## Built-in resolvers
46
+
47
+ | Resolver | Import | Env / config | Notes |
48
+ |--------------------|-------------------------------------------------|---------------------------------|------------------------------------------------------------------|
49
+ | `googleResolver` | `@justanarthur/payload-plugin-translator/resolvers/google` | `apiKey` (Google Cloud Translation API) | Cheap, good for bulk text. Locale codes are remapped where useful (e.g. `ua` → `uk`). |
50
+ | `openAIResolver` | `@justanarthur/payload-plugin-translator/resolvers/openAI` | `apiKey`, optional `model`/`baseUrl`/`prompt`/`chunkLength` | Best fidelity. Default model `gpt-4o-mini`. gpt-5.x uses `max_completion_tokens` + `reasoning_effort: 'low'` automatically. Slugs get a transliteration rule baked into the prompt. |
51
+ | `libreResolver` | `@justanarthur/payload-plugin-translator/resolvers/libreTranslate` | `apiKey`, optional `url`/`chunkLength` | Self-host friendly. Same locale remap as Google. |
52
+ | `copyResolver` | `@justanarthur/payload-plugin-translator/resolvers/copy` | none | Returns the source text verbatim. Useful as a fallback or for testing. |
53
+
54
+ You can supply multiple resolvers — they run in the order declared, and the first one to return
55
+ `success: true` wins for the chunk.
56
+
57
+ ## Writing your own resolver
58
+
59
+ ```ts
60
+ import type { TranslateResolver } from '@justanarthur/payload-plugin-translator/resolvers/types'
61
+
62
+ export const myResolver: TranslateResolver = {
63
+ key: 'my-translator',
64
+ resolve: async ({ localeFrom, localeTo, texts, req }) => {
65
+ // call your translator of choice
66
+ return { success: true, translatedTexts: ['...'] }
67
+ }
68
+ }
69
+ ```
70
+
71
+ `texts` is the flat string array extracted from the doc; you return the same-length array in
72
+ `translatedTexts`. Returning `{ success: false }` makes the job retry.
73
+
74
+ ## Options reference
75
+
76
+ | Option | Type | Required | Notes |
77
+ |---------------|-----------------------|----------|------------------------------------------------------------------------|
78
+ | `collections` | `CollectionSlug[]` | yes | Collections to auto-translate. |
79
+ | `globals` | `GlobalSlug[]` | yes | Globals to auto-translate. |
80
+ | `resolvers` | `TranslateResolver[]` | yes | Tried in order; first to succeed wins per chunk. |
81
+ | `autoTranslate` | `boolean` | no | Default `true`. Set `false` to opt out of the auto-translate afterChange hook — useful when you drive translation manually via the `translateOperation` export. |
82
+ | `disabled` | `boolean` | no | Skip the plugin entirely (no overrides, no jobs). Useful in tests. |
83
+ | `_options.additionalTraverseRichText` | function | no | Hook to extend rich-text traversal — see below. |
84
+
85
+ ### Rich-text traversal hook
86
+
87
+ The plugin walks Lexical richText fields itself, but custom Lexical nodes (e.g. blocks the host
88
+ defines for landing pages) won't be visited. `_options.additionalTraverseRichText` lets you register
89
+ a custom walker:
90
+
91
+ ```ts
92
+ translator({
93
+ // ...
94
+ _options: {
95
+ additionalTraverseRichText: ({ root, onText }) => {
96
+ // call onText(siblingData, attribute?) for every leaf string you find
97
+ // siblingData mutates the doc tree in-place
98
+ }
99
+ }
100
+ })
101
+ ```
102
+
103
+ The hook receives `onText` which mutates the data tree, plus the root node. It's called for every
104
+ richText field before translation begins.
105
+
106
+ ## How it works under the hood
107
+
108
+ - On plugin init: registers a `translate` Payload **task** and a `translate` **workflow**, swapped
109
+ Publish/Save buttons per collection/global, and an admin-only endpoint at `/api/translator/translate`.
110
+ - On `afterChange` (when `autoTranslate: true`): the plugin enqueues one workflow run per non-source
111
+ locale. The job extracts translatable fields, asks each resolver in turn, writes the result back
112
+ into the `_locales` row.
113
+ - The plugin de-duplicates via `AUTO_TRANSLATE_MARKER` — re-running it on an already-attached hook
114
+ is a no-op, so it's safe to wrap multiple plugins around the same collection.
115
+
116
+ ## Manual translation
117
+
118
+ Two escape hatches when you don't want the auto-hook:
119
+
120
+ ```ts
121
+ // the operation (Payload `operation` you can call from custom endpoints)
122
+ import { translateOperation } from '@justanarthur/payload-plugin-translator'
123
+
124
+ // the job factories (advanced you usually don't need these directly)
125
+ import { createTranslateTask, createTranslateWorkflow } from '@justanarthur/payload-plugin-translator/jobs'
126
+ ```
127
+
128
+ For most hosts the auto-hook + resolver list is enough.
129
+
130
+ ## Licence
131
+
132
+ MIT
@@ -0,0 +1,41 @@
1
+ /* src/client/components/CustomButton/styles.css */
2
+ .translator__custom-save-button {
3
+ display: inline-flex;
4
+ align-items: center;
5
+ gap: .5rem;
6
+ }
7
+
8
+ /* src/client/components/TranslatorModal/styles.css */
9
+ .translator__modal {
10
+ position: relative;
11
+ }
12
+
13
+ .translator__wrapper {
14
+ display: flex;
15
+ flex-direction: column;
16
+ gap: 1rem;
17
+ padding: 1rem;
18
+ }
19
+
20
+ .translator__close {
21
+ position: absolute;
22
+ cursor: pointer;
23
+ background: none;
24
+ border: 0;
25
+ font-size: 1.25rem;
26
+ line-height: 1;
27
+ top: .5rem;
28
+ right: .5rem;
29
+ }
30
+
31
+ .translator__content {
32
+ display: flex;
33
+ flex-direction: column;
34
+ gap: .75rem;
35
+ }
36
+
37
+ .translator__buttons {
38
+ display: flex;
39
+ flex-wrap: wrap;
40
+ gap: .5rem;
41
+ }
@@ -0,0 +1,6 @@
1
+ import { JSX as JSX_1kxb } from "react/jsx-runtime";
2
+ type Element_12xgc = JSX_1kxb["Element"];
3
+ declare const CustomButtonWithTranslator: ({ type }: {
4
+ type: "publish" | "save";
5
+ }) => Element_12xgc;
6
+ export { CustomButtonWithTranslator as default, CustomButtonWithTranslator };