avo 4.1.0 → 4.1.2

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/builds/avo/application.css +30 -0
  4. data/lib/avo/base_action.rb +1 -1
  5. data/lib/avo/reloader.rb +9 -0
  6. data/lib/avo/skills/avo-actions/SKILL.md +255 -0
  7. data/lib/avo/skills/avo-admin-config/SKILL.md +163 -0
  8. data/lib/avo/skills/avo-associations/SKILL.md +168 -0
  9. data/lib/avo/skills/avo-authentication/SKILL.md +193 -0
  10. data/lib/avo/skills/avo-aware/SKILL.md +74 -0
  11. data/lib/avo/skills/avo-branding-appearance/SKILL.md +270 -0
  12. data/lib/avo/skills/avo-controllers/SKILL.md +236 -0
  13. data/lib/avo/skills/avo-custom-fields/SKILL.md +197 -0
  14. data/lib/avo/skills/avo-custom-ui/SKILL.md +460 -0
  15. data/lib/avo/skills/avo-engine-internals/SKILL.md +245 -0
  16. data/lib/avo/skills/avo-fields/SKILL.md +219 -0
  17. data/lib/avo/skills/avo-filters/SKILL.md +196 -0
  18. data/lib/avo/skills/avo-i18n/SKILL.md +254 -0
  19. data/lib/avo/skills/avo-index-views/SKILL.md +254 -0
  20. data/lib/avo/skills/avo-media-library/SKILL.md +122 -0
  21. data/lib/avo/skills/avo-menu-icons/SKILL.md +135 -0
  22. data/lib/avo/skills/avo-menu-icons/scripts/list_icons.rb +49 -0
  23. data/lib/avo/skills/avo-multitenancy/SKILL.md +186 -0
  24. data/lib/avo/skills/avo-navigation-search/SKILL.md +255 -0
  25. data/lib/avo/skills/avo-performance/SKILL.md +190 -0
  26. data/lib/avo/skills/avo-resources/SKILL.md +273 -0
  27. data/lib/avo/skills/avo-setup/SKILL.md +288 -0
  28. data/lib/avo/skills/avo-testing/SKILL.md +188 -0
  29. data/lib/avo/skills/avo-troubleshoot/SKILL.md +325 -0
  30. data/lib/avo/skills/avo-update/SKILL.md +179 -0
  31. data/lib/avo/skills/bin/avo-skills-resolve +242 -0
  32. data/lib/avo/skills/index.md +53 -0
  33. data/lib/avo/skills/package-map.md +30 -0
  34. data/lib/avo/version.rb +1 -1
  35. data/lib/avo.rb +4 -0
  36. data/lib/generators/avo/skills_generator.rb +231 -0
  37. data/lib/generators/avo/skills_install_panel.rb +169 -0
  38. data/lib/generators/avo/templates/skills/SKILL.md +111 -0
  39. metadata +32 -1
@@ -0,0 +1,196 @@
1
+ ---
2
+ name: avo-filters
3
+ description: Route a filtering request to the right Avo system, then implement basic filters (developer-written Ruby filter classes) on a resource's Index view. Use when the user wants to filter, segment, or set a default view for the records on a resource index — e.g. "filter projects by status", "filter users by role", "filter by a date range", "let users build their own ad-hoc filters", "filter by an association", "only show active records by default", "add a tab for admin/active users", "segment orders into paid/unpaid tabs", or "show a count next to each tab". Covers the avo:filter generator, def filters, and the routing table that decides between basic filters, dynamic filters, and scopes. Dynamic filters and scopes are implemented by their own gems and ship their own skills — this skill points at them.
4
+ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
5
+ metadata:
6
+ requires-gem: none — basic filters are Community; this skill routes to avo-dynamic_filters and avo-scopes, which ship their own skills
7
+ ---
8
+
9
+ > **These instructions ship inside the `avo` gem this app has locked, so they describe the version you are actually running.** Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
10
+
11
+ # Filter and segment an Avo resource index
12
+
13
+ Avo has three separate systems for narrowing down the records on a resource's `Index` view. This skill's leading job is **routing** to the right one, then implementing it with the correct generator and DSL. Getting the routing wrong means writing the wrong kind of code, so start with the routing table below every time.
14
+
15
+ **Docs** (fetch on demand with WebFetch — the `.md` variants are agent-friendly):
16
+
17
+ - Overview / decision: https://docs.avohq.io/4.0/filters.md
18
+ - Basic filters: https://docs.avohq.io/4.0/basic-filters.md · API: https://docs.avohq.io/4.0/basic-filters-api.md
19
+ - Dynamic filters: https://docs.avohq.io/4.0/dynamic-filters.md · API: https://docs.avohq.io/4.0/dynamic-filters-api.md
20
+ - Scopes: https://docs.avohq.io/4.0/scopes.md · API: https://docs.avohq.io/4.0/scopes-api.md
21
+ - Full docs map: https://docs.avohq.io/4.0/docs-map.md
22
+
23
+ Fetch the relevant page when you need an option you don't see here (custom conditions, `fetch_values_from`, `picker_options`, per-scope `fields`, humanized pills, …). This file covers the common cases end to end.
24
+
25
+ ## When this applies
26
+
27
+ The user wants to change **which records appear** on a resource's index (or an association's `has_many` listing) — filter them, let end users filter them, segment them into tabs, or pick a default view. All three systems apply to the `Index` view only and encode their state in the URL, so a filtered/segmented view can be bookmarked and shared.
28
+
29
+ Avo files live under `app/avo/`. Before editing, locate the resource (`app/avo/resources/<name>.rb`) and its model (`app/models/<name>.rb`). Use Glob (`app/avo/resources/**/*.rb`) if you're unsure of the exact path.
30
+
31
+ ## Choose the right tool
32
+
33
+ Read the request against this table **first**. The three systems are independent — a resource can use any combination — but each request usually maps to exactly one.
34
+
35
+ | What the user is asking for | Use | License |
36
+ | --- | --- | --- |
37
+ | A specific, developer-defined filter with exact query logic the developer controls — "filter by status", "published/unpublished", "featured only", "by author", "by a date range". One filter = one value or checkbox set. | **Basic filter** | Community |
38
+ | End users composing their **own** filters across many attributes — pick an attribute, a condition (`Contains`, `Is`, `>=`, `Is null`), and a value, stacking several at once. "let users filter however they want", "ad-hoc filtering", "filter by an association". | **Dynamic filters** | Paid add-on |
39
+ | One-click **segment tabs** above the table — "add a tab for admins", "segment orders into paid/unpaid tabs", "Active / Archived tabs", "show a count next to each tab". | **Scopes** | Paid add-on |
40
+ | A **default view** — "only show active records by default", "hide soft-deleted by default", "default to my team's records". | **Scope** marked `default: true` (optionally with `remove_scope_all`) | Paid add-on |
41
+
42
+ Quick disambiguation:
43
+
44
+ - **"Filter" is ambiguous.** If the developer decides the exact query and offers a fixed set of choices → basic filter. If the *end user* builds the query from a palette of attributes/conditions → dynamic filters.
45
+ - **Tabs vs. filters.** A tab bar the user clicks between (mutually-exclusive segments, one active at a time) → scopes. A panel where the user sets values and applies → basic or dynamic filters.
46
+ - **"By default" / "only show X" is almost always a default scope**, not a filter — filters start empty; scopes can be pre-applied.
47
+
48
+ If the user's own words don't settle it, ask one clarifying question rather than guessing (e.g. "Do you want a fixed 'Status' filter you control, or should users build their own filters?").
49
+
50
+ **License gate:** basic filters ship in Avo's **Community** edition. **Dynamic filters and scopes are paid add-ons** (`avo-dynamic_filters` and `avo-scopes` gems). If the routing lands on dynamic filters or scopes, mention the add-on requirement in your report so the user isn't surprised when the DSL is present but nothing renders on an unlicensed install.
51
+
52
+ ---
53
+
54
+ ## Basic filters
55
+
56
+ One Ruby class per filter under `app/avo/filters/`. You choose the input type, define its `options`, and write the exact Active Record query in `apply`. Then register it on each resource that should show it.
57
+
58
+ ### Generate
59
+
60
+ ```bash
61
+ bin/rails generate avo:filter published --type select
62
+ ```
63
+
64
+ `--type` accepts `boolean` (default), `select`, `multiple_select`, `text`, and `date_time`. Each maps to a base class and a value shape in `apply`:
65
+
66
+ | Type | Base class | `apply` receives |
67
+ | --- | --- | --- |
68
+ | `boolean` | `Avo::Filters::BooleanFilter` | `Hash` of `"option_id" => true/false` |
69
+ | `select` | `Avo::Filters::SelectFilter` | `String` (selected option id) |
70
+ | `multiple_select` | `Avo::Filters::MultipleSelectFilter` | `Array` of `String`s |
71
+ | `text` | `Avo::Filters::TextFilter` | `String` |
72
+ | `date_time` | `Avo::Filters::DateTimeFilter` | `String` (or `"<start> to <end>"` in range mode) |
73
+
74
+ ### Write the filter
75
+
76
+ A select filter — the everyday "filter by status" case:
77
+
78
+ ```ruby
79
+ # app/avo/filters/published.rb
80
+ class Avo::Filters::Published < Avo::Filters::SelectFilter
81
+ self.name = "Published status"
82
+
83
+ # value is a String, e.g. "published"
84
+ def apply(request, query, value)
85
+ case value
86
+ when "published" then query.where.not(published_at: nil)
87
+ when "unpublished" then query.where(published_at: nil)
88
+ else query
89
+ end
90
+ end
91
+
92
+ def options
93
+ {published: "Published", unpublished: "Unpublished"}
94
+ end
95
+ end
96
+ ```
97
+
98
+ A boolean filter receives a hash keyed by option id — **read the keys as strings** (see Gotchas):
99
+
100
+ ```ruby
101
+ # app/avo/filters/featured.rb
102
+ class Avo::Filters::Featured < Avo::Filters::BooleanFilter
103
+ self.name = "Featured"
104
+
105
+ # values = { "is_featured" => true, "is_unfeatured" => false }
106
+ def apply(request, query, values)
107
+ return query if values["is_featured"] && values["is_unfeatured"]
108
+
109
+ if values["is_featured"]
110
+ query.where(featured: true)
111
+ elsif values["is_unfeatured"]
112
+ query.where(featured: false)
113
+ else
114
+ query
115
+ end
116
+ end
117
+
118
+ def options
119
+ {is_featured: "Featured", is_unfeatured: "Unfeatured"}
120
+ end
121
+ end
122
+ ```
123
+
124
+ A date-time filter — for "filter by a date range". Default `self.mode` is `:range`:
125
+
126
+ ```ruby
127
+ # app/avo/filters/created_at.rb
128
+ class Avo::Filters::CreatedAt < Avo::Filters::DateTimeFilter
129
+ self.name = "Created at"
130
+ self.type = :date # :date_time (default), :date, or :time
131
+ self.mode = :range # :range (default) or :single
132
+
133
+ def apply(request, query, value)
134
+ from, to = value.split(" to ") # range arrives as "2024-08-13 to 2024-08-16"
135
+ query.where(created_at: Date.parse(from)..Date.parse(to))
136
+ end
137
+ end
138
+ ```
139
+
140
+ Text filters need no `options` (`value` is the raw string). Multiple-select filters get an `Array` of strings and `options` like select.
141
+
142
+ ### Register on the resource
143
+
144
+ Filters only render once registered inside the resource's `filters` method:
145
+
146
+ ```ruby
147
+ # app/avo/resources/post.rb
148
+ class Avo::Resources::Post < Avo::BaseResource
149
+ def filters
150
+ filter Avo::Filters::Published
151
+ filter Avo::Filters::Featured
152
+ end
153
+ end
154
+ ```
155
+
156
+ The same filter class can be registered on many resources. To vary behavior per resource, pass `arguments: {...}` — available in `apply`, `options`, and the `self.name`/`self.visible` blocks.
157
+
158
+ ### Common extras (in the guide)
159
+
160
+ - **Default state:** define `default` returning the same shape `apply` expects (`:published`, `{is_featured: true}`, `["a", "b"]`).
161
+ - **Dynamic options:** `options` is plain Ruby — query the DB or an API.
162
+ - **Conditional visibility:** `self.visible = -> { current_user.admin? }`.
163
+ - **Filters that depend on each other:** read `applied_filters` in `options`, or override `react` to change a filter's own value when another changes.
164
+ - **Link to a pre-filtered view:** `Avo::Filters::BaseFilter.encode_filters({"Avo::Filters::Name" => "Apple"})` → pass as `encoded_filters:`.
165
+
166
+ ---
167
+ ## Dynamic filters and scopes ship in their own gems
168
+
169
+ Both are paid add-ons, and their instructions ship inside the gem that implements them — so they describe the version this app has locked, not a guess.
170
+
171
+ | Subject | Gem | Skill to read |
172
+ | --- | --- | --- |
173
+ | End users composing ad-hoc filters — `filterable` fields, `dynamic_filter`, Ransack, the filters bar | `avo-dynamic_filters` | `avo-dynamic-filters` |
174
+ | Segment tabs, default views, per-tab counts and columns | `avo-scopes` | `avo-scopes` |
175
+
176
+ Re-run the Avo skills loader and read the skill from the path it prints for that gem. If the loader says the gem is not in `Gemfile.lock`, the app does not have it — tell the user which add-on provides the feature instead of writing DSL that will not render.
177
+
178
+ Route to them from the table above; do not reimplement their DSL from memory here.
179
+
180
+ ---
181
+
182
+ ## Gotchas
183
+
184
+ - **Basic filter values are always strings.** State is serialized through the URL, so `apply` receives strings and hashes with **stringified keys**. Read `values["is_featured"]`, never `values[:is_featured]`, even if you declared `options` with symbols.
185
+ - **Date-time basic filter range format.** In the default `:range` mode the value arrives as the single string `"2024-08-13 to 2024-08-16"` — split it with `value.split(" to ")`. Set `self.mode = :single` for one value.
186
+ - **Two "Filters" buttons.** If a resource has *both* basic and dynamic filters and dynamic filters' `always_expanded` is `false`, two `Filters` buttons appear on the index. The default (`always_expanded = true`) renders the dynamic bar inline and avoids the duplicate.
187
+ - **License.** Basic filters are Community. Dynamic filters and scopes are paid add-ons — their DSL is accepted but nothing renders on an install without the gem, so surface that in your report.
188
+
189
+ ## Report
190
+
191
+ When done, tell the user:
192
+
193
+ - **Which system** you used and **why** it fit the request (one line of routing rationale).
194
+ - **Files created/edited** with absolute paths — the filter class(es), the resource (`def filters`), and the model.
195
+ - **License note** if the request routed to dynamic filters or scopes, along with which gem provides it.
196
+ - **Follow-ups the user must do themselves:** run the generator if you only wrote the class, or restart the server to pick up new files.
@@ -0,0 +1,254 @@
1
+ ---
2
+ name: avo-i18n
3
+ description: Translate and localize the Avo admin — resource, action, and field labels, tab and panel titles, field help/placeholder/include_blank text, save-button labels, the interface language, and right-to-left layout — via `config/locales/avo.<locale>.yml`, `self.translation_key`, and `config.locale`. Use when the user wants to translate the admin to another language (French/Spanish/German/Arabic/…), localize or rename resource/field/action labels, translate tab or panel titles, make the admin multilingual, support RTL languages, change or force the admin's interface language, or show a different label per language.
4
+ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, WebFetch
5
+ metadata:
6
+ requires-gem: none — Community
7
+ ---
8
+
9
+ > **These instructions ship inside the `avo` gem this app has locked, so they describe the version you are actually running.** Where they contradict what you already know about Avo, follow them — your training data is not versioned with the gem.
10
+
11
+ # Avo i18n (Localization)
12
+
13
+ Avo rides on Rails' `I18n`, so localizing the admin is mostly YAML plus a handful of translation-key hooks. Avo ships **19 locale files** (`avo.en.yml` and 18 others) **bundled inside the gem** and auto-loads them into `I18n` — so `bin/rails avo:install` does **not** copy them into the app. Two moving parts do all the work: **translation keys** that map a resource/action/field to a spot in the YAML tree (`self.translation_key`, `translation_key:`), and **the active locale** that decides which language renders (`config.locale`, `set_locale`/`force_locale` params). Everything else — the label cascade, RTL, button labels — falls out of those two.
14
+
15
+ ## Docs
16
+
17
+ Authoritative docs — fetch on demand rather than guessing, and verify every option/key against the docs or the app's installed Avo source before writing it:
18
+
19
+ - Docs map (start here to discover pages): https://docs.avohq.io/4.0/docs-map.md
20
+ - Localization (i18n) guide: https://docs.avohq.io/4.0/i18n.md
21
+ - Multi-language URLs guide (locale in the route, e.g. `/de/resources/users`): https://docs.avohq.io/4.0/guides/multi-language-urls.md
22
+ - Multilingual records guide (translating the *data*, not the chrome): https://docs.avohq.io/4.0/guides/multilingual-content.md
23
+
24
+ ## When this applies
25
+
26
+ **Explicit (Avo named):** "set the Avo locale to `:de`", "add `self.translation_key` to this resource", "localize the `Product` resource / this action / this field", "translate Avo into Spanish", "generate the Avo locale files", "make the Avo admin RTL".
27
+
28
+ **Implicit (no mention of Avo/i18n):** "translate the admin to French / Spanish / German", "the admin needs to be in Arabic" or "support right-to-left", "localize the resource / field / action labels", "rename this field's label per language", "make the admin multilingual", "change the admin's language", "different label depending on the language", "the field help text should be translated", "customize the Save button wording for this one resource".
29
+
30
+ **Not this skill:** translating the *content of records* (a `title` column that differs per language) is the **multilingual records** guide, not i18n. Putting the locale in the URL (`/en/...`, `/de/...`) is the **multi-language URLs** guide. Point the user there and stop.
31
+
32
+ ## Workflow
33
+
34
+ ### 1. Get editable locale files
35
+
36
+ Avo's locale files live inside the gem and are not copied on install. To edit any Avo-provided string (or add a brand-new language), generate local copies first:
37
+
38
+ ```bash
39
+ bin/rails generate avo:locales
40
+ ```
41
+
42
+ That copies all bundled `avo.<locale>.yml` files into `config/locales/`. Edit the ones you need; add `config/locales/avo.<new_locale>.yml` for a language Avo doesn't ship, using a shipped file as the template. Rails auto-loads everything under `config/locales`, so no registration step.
43
+
44
+ For **custom** resource/action/field labels (the common case), you don't need the generator at all — just add your own keys under the `avo:` namespace in any locale file (e.g. `config/locales/avo.en.yml`), following the shapes below.
45
+
46
+ ### 2. Localize a resource
47
+
48
+ Set `self.translation_key` on the resource, then supply the pluralized name under that key. This changes the resource's label **everywhere** in Avo (sidebar, breadcrumbs, headings):
49
+
50
+ ```ruby
51
+ # app/avo/resources/user.rb
52
+ class Avo::Resources::User < Avo::BaseResource
53
+ self.title = :name
54
+ self.translation_key = "avo.resource_translations.user"
55
+ end
56
+ ```
57
+
58
+ ```yaml
59
+ # config/locales/avo.es.yml
60
+ es:
61
+ avo:
62
+ resource_translations:
63
+ user:
64
+ zero: "usuarios"
65
+ one: "usuario"
66
+ other: "usuarios"
67
+ ```
68
+
69
+ Omit `self.translation_key` and Avo derives it from the class name, **namespace included** — `Avo::Resources::Galaxy::Planet` defaults to `avo.resource_translations.galaxy/planet`. So for a plain resource you often only need the YAML, no Ruby change.
70
+
71
+ ### 3. Localize an action
72
+
73
+ Same convention. Avo looks under `avo.action_translations.<class_path>` for the `name`, `message`, `confirm_button_label`, `cancel_button_label`, and `description` sub-keys before falling back to the class attributes (`self.name`, `self.message`, …):
74
+
75
+ ```ruby
76
+ # app/avo/actions/toggle_inactive.rb
77
+ class Avo::Actions::ToggleInactive < Avo::BaseAction
78
+ # Optional — defaults to avo.action_translations.toggle_inactive
79
+ # self.translation_key = "avo.action_translations.toggle_inactive"
80
+ end
81
+ ```
82
+
83
+ ```yaml
84
+ # config/locales/avo.sv.yml
85
+ sv:
86
+ avo:
87
+ action_translations:
88
+ toggle_inactive:
89
+ name: "Växla inaktiv"
90
+ message: "Är du säker på att du vill växla inaktiv status?"
91
+ confirm_button_label: "Växla"
92
+ cancel_button_label: "Avbryt"
93
+ description: "Växlar den inaktiva statusen för användaren"
94
+ city/update: # namespaced: Avo::Actions::City::Update
95
+ name: "Uppdatera stad"
96
+ ```
97
+
98
+ `self.name`/`self.message`/button labels (strings **or** lambdas) remain the fallback when no key is translated — keep them for dynamic labels that depend on `arguments`/`record`.
99
+
100
+ ### 4. Localize fields
101
+
102
+ When a field has no explicit `translation_key:`, Avo resolves its label through a **cascade**, using the first key that has a translation:
103
+
104
+ 1. `avo.resource_translations.<resource>.fields.<field_id>` — resource-scoped; overrides a label for **one** resource.
105
+ 2. `avo.field_translations.<field_id>` — shared across every resource using that field id.
106
+ 3. Humanized field id — the fallback.
107
+
108
+ Point a field at the shared key explicitly with `translation_key:` (this **bypasses the cascade** entirely):
109
+
110
+ ```ruby
111
+ # app/avo/resources/project.rb
112
+ class Avo::Resources::Project < Avo::BaseResource
113
+ def fields
114
+ field :id, as: :id
115
+ field :files, as: :files, translation_key: "avo.field_translations.file"
116
+ end
117
+ end
118
+ ```
119
+
120
+ ```yaml
121
+ # config/locales/avo.es.yml
122
+ es:
123
+ avo:
124
+ field_translations:
125
+ file:
126
+ one: "archivo"
127
+ other: "archivos"
128
+ resource_translations:
129
+ product:
130
+ fields:
131
+ title: # wins over field_translations.title, only on Product
132
+ one: "Título del producto"
133
+ other: "Títulos del producto"
134
+ ```
135
+
136
+ **`help`, `placeholder`, and `include_blank`** resolve from **sibling keys under the same translation key** — no need to pass them in Ruby. Set them once in YAML and drop the Ruby options:
137
+
138
+ ```yaml
139
+ # config/locales/avo.sv.yml
140
+ sv:
141
+ avo:
142
+ field_translations:
143
+ dates:
144
+ one: "Datumintervall"
145
+ other: "Datumintervall"
146
+ help: "Valfritt. Standardperiod: 1 vecka tillbaka till idag."
147
+ placeholder: "Välj datum"
148
+ include_blank: "Ingen"
149
+ ```
150
+
151
+ ```ruby
152
+ field :dates, as: :date_time # help/placeholder/include_blank come from the locale
153
+ ```
154
+
155
+ Explicit `help:`/`placeholder:`/`include_blank:` (strings or lambdas) still win over the locale file.
156
+
157
+ ### 5. Localize tabs and panels
158
+
159
+ Tab and panel titles resolve through the **resource's** translation key too. Avo parameterizes the configured `title:` (downcased, non-alphanumerics turned into `_`) and looks it up under a `tabs` or `panels` scope:
160
+
161
+ - `avo.resource_translations.<resource>.tabs.<title>` for a `tab`
162
+ - `avo.resource_translations.<resource>.panels.<title>` for a `panel`
163
+
164
+ So `tab title: "Activity"` resolves to `avo.resource_translations.user.tabs.activity`, and `panel title: "Contact information"` to `avo.resource_translations.user.panels.contact_information`:
165
+
166
+ ```yaml
167
+ # config/locales/avo.es.yml
168
+ es:
169
+ avo:
170
+ resource_translations:
171
+ user:
172
+ tabs:
173
+ activity: "Actividad"
174
+ panels:
175
+ contact_information: "Información de contacto"
176
+ ```
177
+
178
+ The configured `title:` stays the fallback when the key has no translation. To pin a different key, pass `translation_key:` to the `tab` or `panel` and Avo uses it verbatim:
179
+
180
+ ```ruby
181
+ tab title: "Activity", translation_key: "avo.resource_translations.user.tabs.recent_activity" do
182
+ # ...
183
+ end
184
+ ```
185
+
186
+ ### 6. Localize button labels
187
+
188
+ `avo.save` covers every Save button globally. To override it for **one** resource, add a `save` key under that resource's translation key:
189
+
190
+ ```yaml
191
+ # config/locales/avo.en.yml
192
+ en:
193
+ avo:
194
+ resource_translations:
195
+ product:
196
+ save: "Save the product!"
197
+ ```
198
+
199
+ ### 7. Set or switch the interface language
200
+
201
+ `config.locale` sets Avo's locale for **Avo requests only** — the rest of the app keeps `config.i18n.default_locale`. Default is `nil` (falls back to the app default):
202
+
203
+ ```ruby
204
+ # config/initializers/avo.rb
205
+ Avo.configure do |config|
206
+ config.locale = :de # default: nil
207
+ end
208
+ ```
209
+
210
+ Two request params switch language on the fly:
211
+
212
+ - **`?set_locale=pt-BR`** — sets Avo's default locale process-wide **until the server restarts** (it mutates `Avo.configuration.locale`). Affects every user; use it as a switch, not per-user preference.
213
+ - **`?force_locale=pt-BR`** — sets the locale for that request only, but Avo **keeps the param in every link** while you navigate. Remove it to return to the configured locale. Good for previewing a translation without changing config.
214
+
215
+ ### 8. Right-to-left languages
216
+
217
+ RTL is **automatic** — Avo detects it from the active locale and flips the layout (`dir="rtl"`, mirrored chrome) with no configuration. Built-in RTL locales: `ar`, `he`, `fa`, `ur`, `yi`, `ps`, `sd`, `ku`, `ckb`, `ug`, `dv`. Matching is on the **language segment**, so regional variants like `ar-EG` count as RTL too. To ship an Arabic admin, translate into `avo.ar.yml` and set the locale — the layout follows.
218
+
219
+ ## Key options
220
+
221
+ | Hook | Where | Does |
222
+ | --- | --- | --- |
223
+ | `self.translation_key` | Resource class | Maps the resource to `avo.resource_translations.<...>`; changes its label everywhere. Defaults from class name (namespace included). |
224
+ | `self.translation_key` | Action class | Maps the action to `avo.action_translations.<...>` (`name`/`message`/`confirm_button_label`/`cancel_button_label`/`description` sub-keys). |
225
+ | `translation_key:` | Field declaration | Pins the field to one key and **skips the cascade**. Omit to use resource-scoped → shared → humanized. |
226
+ | `avo.resource_translations.<r>.fields.<f>` | YAML | Resource-scoped field label — overrides the shared one for a single resource. |
227
+ | `avo.field_translations.<f>` | YAML | Shared field label + `help`/`placeholder`/`include_blank` siblings across all resources. |
228
+ | `avo.resource_translations.<r>.tabs.<t>` | YAML | Tab title, keyed on the parameterized `title:`. Pin a different key with `translation_key:` on the `tab`. |
229
+ | `avo.resource_translations.<r>.panels.<p>` | YAML | Panel title, same convention as tabs; `translation_key:` on the `panel` overrides it. |
230
+ | `avo.resource_translations.<r>.save` | YAML | Per-resource Save-button text (else global `avo.save`). |
231
+ | `config.locale` | Initializer | Avo's interface language for Avo requests only. Default `nil` → app default. |
232
+ | `?set_locale=` param | URL | Switches Avo's default locale process-wide until restart. |
233
+ | `?force_locale=` param | URL | Switches locale for the current navigation only; sticks in links until removed. |
234
+ | `bin/rails g avo:locales` | Shell | Copies the 19 bundled locale files into `config/locales` for editing. |
235
+
236
+ ## Gotchas
237
+
238
+ - **Locale files ship inside the gem — not copied on install.** To edit Avo's own strings or add a language, run `bin/rails g avo:locales` first. For custom labels you only need your own keys under `avo:`; the generator is optional.
239
+ - **Explicit `translation_key:` on a field bypasses the cascade.** You lose the resource-scoped → shared → humanized fallback and the automatic `help`/`placeholder`/`include_blank` sibling lookup. Only pin a key when you want exactly that key.
240
+ - **Default keys include the namespace.** `Avo::Resources::Galaxy::Planet` → `avo.resource_translations.galaxy/planet`; `Avo::Actions::City::Update` → `avo.action_translations.city/update` (underscored, slash-joined). Match that path in YAML or the lookup misses and you fall back to the humanized name.
241
+ - **Pluralization keys are required.** Resource/field name lookups run `I18n.t(key, count:, default:)`, so provide `one`/`other` (and `zero` where relevant). A bare string can raise `I18n::InvalidPluralizationData` or silently fall back to the computed name.
242
+ - **`set_locale` is global and sticky.** It mutates `Avo.configuration.locale` and persists until the server restarts, affecting all users — it is not a per-user preference. Use `force_locale` for a scoped, reversible switch (it rides along in every link until removed).
243
+ - **RTL is automatic and matches on the language segment.** `ar`, `he`, `fa`, `ur`, `yi`, `ps`, `sd`, `ku`, `ckb`, `ug`, `dv` — and regional variants (`ar-EG`) — flip the layout with no config. Don't hand-roll a direction toggle.
244
+ - **Don't confuse chrome with data.** This skill localizes labels/buttons/help (the admin UI). Translating record *content* is the multilingual-records guide; putting the locale in the URL is the multi-language-URLs guide.
245
+ - **Verify before writing.** Key names and the cascade drift between versions — check the docs URL above or the app's installed Avo source rather than trusting memory.
246
+
247
+ ## Report
248
+
249
+ When done, tell the user:
250
+
251
+ - Which locale file(s) you created or edited (full paths) and any resource/action/field file(s) where you set `translation_key`, plus any generator command run (`bin/rails g avo:locales`).
252
+ - The translation keys you added and the resource/action/field each targets — call out where you relied on the field cascade vs. an explicit `translation_key:`.
253
+ - The active-locale change, if any (`config.locale`, or that they can preview with `?force_locale=`), and whether RTL will engage for the chosen language.
254
+ - Anything still needed: add the remaining `one`/`other` plural forms, translate the other bundled strings, restart the server after a `config.locale` change, or move to the multilingual-records / multi-language-URLs guide if the real goal was record content or locale-in-URL.