senren-ui 0.3.0 → 0.4.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +127 -0
- data/CONTRIBUTING.md +10 -0
- data/README.md +36 -0
- data/docs/i18n.md +194 -0
- data/lib/generators/senren/install/install_generator.rb +36 -0
- data/lib/generators/senren/install/templates/base_component.rb.tt +39 -0
- data/lib/generators/senren/install/templates/conventions.md.tt +16 -0
- data/lib/senren/rails/agent_rules_writer.rb +4 -0
- data/lib/senren/rails/base_component_patch.rb +24 -0
- data/lib/senren/rails/component_copier.rb +21 -0
- data/lib/senren/rails/host_paths.rb +5 -0
- data/lib/senren/rails/locale_installer.rb +90 -0
- data/lib/senren/rails/version.rb +1 -1
- data/lib/senren/rails.rb +1 -0
- data/lib/tasks/senren.rake +9 -0
- data/templates/components/accordion/accordion_component.html.erb +22 -4
- data/templates/components/accordion/accordion_component.rb +2 -1
- data/templates/components/alert_dialog/alert_dialog_component.html.erb +1 -1
- data/templates/components/api_key_field/api_key_field_component.html.erb +1 -1
- data/templates/components/api_key_field/api_key_field_component.rb +8 -1
- data/templates/components/app_shell/app_shell_component.rb +2 -1
- data/templates/components/breadcrumb/breadcrumb_component.rb +2 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.html.erb +1 -1
- data/templates/components/bulk_action_bar/bulk_action_bar_component.rb +4 -2
- data/templates/components/calendar/calendar_component.rb +37 -2
- data/templates/components/carousel/carousel_component.html.erb +5 -5
- data/templates/components/carousel/carousel_component.rb +12 -1
- data/templates/components/cart/cart_component.html.erb +6 -6
- data/templates/components/cart/cart_component.rb +11 -2
- data/templates/components/clipboard/clipboard_component.html.erb +1 -1
- data/templates/components/clipboard/clipboard_component.rb +6 -1
- data/templates/components/collapsible/collapsible_component.rb +1 -1
- data/templates/components/combobox/combobox_component.rb +2 -1
- data/templates/components/command/command_component.rb +3 -1
- data/templates/components/data_table/data_table_component.html.erb +3 -3
- data/templates/components/data_table/data_table_component.rb +16 -1
- data/templates/components/date_picker/date_picker_component.html.erb +2 -2
- data/templates/components/date_picker/date_picker_component.rb +2 -1
- data/templates/components/dialog/dialog_component.html.erb +2 -2
- data/templates/components/filter_bar/filter_bar_component.rb +1 -1
- data/templates/components/form/form_component.html.erb +1 -1
- data/templates/components/form/form_component.rb +21 -0
- data/templates/components/invite_member_dialog/invite_member_dialog_component.html.erb +9 -8
- data/templates/components/invite_member_dialog/invite_member_dialog_component.rb +11 -3
- data/templates/components/native_select/native_select_component.rb +12 -3
- data/templates/components/pagination/pagination_component.html.erb +2 -2
- data/templates/components/pagination/pagination_component.rb +2 -1
- data/templates/components/product_card/product_card_component.html.erb +1 -1
- data/templates/components/product_card/product_card_component.rb +2 -2
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.html.erb +2 -16
- data/templates/components/rich_text_editor_lite/rich_text_editor_lite_component.rb +32 -1
- data/templates/components/search_input/search_input_component.rb +2 -1
- data/templates/components/sheet/sheet_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.html.erb +2 -2
- data/templates/components/sidebar/sidebar_component.rb +4 -1
- data/templates/components/table/table_component.html.erb +1 -1
- data/templates/components/tabs/tabs_component.rb +4 -2
- data/templates/components/theme_toggle/theme_toggle_component.html.erb +17 -3
- data/templates/components/theme_toggle/theme_toggle_component.rb +6 -1
- data/templates/components/top_nav/top_nav_component.html.erb +1 -1
- data/templates/components/top_nav/top_nav_component.rb +3 -2
- data/templates/controllers/accordion_controller.js +68 -3
- data/templates/controllers/api_key_field_controller.js +3 -2
- data/templates/controllers/carousel_controller.js +6 -1
- data/templates/controllers/clipboard_controller.js +3 -3
- data/templates/controllers/rich_text_editor_lite_controller.js +2 -2
- data/templates/controllers/theme_toggle_controller.js +13 -3
- data/templates/locales/senren.en.yml +151 -0
- data/templates/locales/senren.vi.yml +151 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 99b7430e710e44023cbc886c862b4d237d06bb489a4a00f5e30685b59768f6dc
|
|
4
|
+
data.tar.gz: 6ead04eac8e4e518cbf7129c65dfb63ad683db3dfb44b13a794366eee8be37dd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7cf42a8f5992b7de9bc4135c7d6bea7c44987ca3844de06b575876957877efd773711cdfcb9cd54d2ddc1ca878f38842d4df082b0fb7451b5b63613c9bb5f056
|
|
7
|
+
data.tar.gz: e3c329bc7eb8ef0c7b90af18971c92f24dfdea4822e17a9e43faff3b1e94a6815ed4b6c18cff30a2e65932cd8bf96d88b2e64153671d72e2a52c8ffe2d759c76
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,133 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
7
7
|
v0.x is a pre-stable line: minor bumps may break things; patch bumps are
|
|
8
8
|
bug fixes only.
|
|
9
9
|
|
|
10
|
+
## [0.4.0] — 2026-10-08
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Every word a component shows or announces can be translated from the host
|
|
15
|
+
app, without editing a component.** Visible text, `aria-label` and `sr-only`
|
|
16
|
+
text, the defaults of `label:`, `placeholder:` and `empty_text:`, the
|
|
17
|
+
calendar's month and weekday names, and the sentences a Stimulus controller
|
|
18
|
+
says after load now go through one helper, `senren_t`, with the English as its
|
|
19
|
+
default: `senren_t('pagination.next', default: 'Next')` is
|
|
20
|
+
`I18n.t("senren.pagination.next", default: "Next")`. 111 keys across 27
|
|
21
|
+
components, named `senren.<component>.<key>`, with `senren.shared.*` for the
|
|
22
|
+
two words that are the same word in several components.
|
|
23
|
+
|
|
24
|
+
An app with no locale file renders exactly what it did before, and a missing
|
|
25
|
+
translation never raises, including under
|
|
26
|
+
`config.i18n.raise_on_missing_translations`. Text you pass in yourself is used
|
|
27
|
+
as given and never looked up; `label: nil` still means "none", and leaving the
|
|
28
|
+
argument out is what gets the translated default.
|
|
29
|
+
|
|
30
|
+
The gem ships `senren.en.yml` and `senren.vi.yml`, and copies either only
|
|
31
|
+
when asked for: `bin/rails generate senren:install --locales vi`, or later
|
|
32
|
+
`bin/rails senren:locales vi`. Not even English is copied by default: a copied
|
|
33
|
+
`senren.en.yml` loads after the app's `en.yml`, overriding any `senren.*` key
|
|
34
|
+
set there, and pins today's wording against later releases. An existing file
|
|
35
|
+
is never overwritten unless that is asked for by name
|
|
36
|
+
(`senren:locales vi --force`, or `senren:install --force-locales`); the
|
|
37
|
+
`--force` that refreshes Senren's own files leaves translations alone. To add
|
|
38
|
+
another language, `bin/rails senren:locales en`, copy the file, change `en:`,
|
|
39
|
+
and translate the values. A translation with a misspelt `%{variable}` falls
|
|
40
|
+
back to the English instead of raising. See [docs/i18n.md](docs/i18n.md).
|
|
41
|
+
|
|
42
|
+
`bin/i18n-sync` regenerates `templates/locales/senren.en.yml` from the code,
|
|
43
|
+
and a test fails if it, `senren.vi.yml`, and the components disagree, or if a
|
|
44
|
+
template shows a word that did not go through `senren_t`.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- **Five Stimulus controllers no longer carry an English sentence.** `clipboard`
|
|
49
|
+
("Copied", "Copied to clipboard"), `api_key_field` ("Copy complete"),
|
|
50
|
+
`carousel` ("Slide 2 of 3"), `theme_toggle` ("Light theme", "Dark theme") and
|
|
51
|
+
`rich_text_editor_lite` (the link prompt) read the sentence from a Stimulus
|
|
52
|
+
value the component renders, so they speak the host's language. The markup
|
|
53
|
+
gains six `data-senren--<controller>-…-value` attributes and is otherwise
|
|
54
|
+
byte-for-byte what it was.
|
|
55
|
+
|
|
56
|
+
**Migrate:** none for an unmodified install. The controller and its component
|
|
57
|
+
go together: if you copied a component and edited its controller, or the other
|
|
58
|
+
way round, copy both again (`bin/rails senren:add <name> --force`) so the value
|
|
59
|
+
the controller reads is the one the component writes.
|
|
60
|
+
- `bin/rails senren:add` appends `senren_t` to an existing
|
|
61
|
+
`app/components/senren/base_component.rb` that lacks it, so a component added
|
|
62
|
+
after upgrading the gem does not raise `NoMethodError`. Components you copied
|
|
63
|
+
earlier keep the English written into them until you copy them again.
|
|
64
|
+
- The performance budgets for component source (140000 -> 150000 bytes) and for
|
|
65
|
+
the Stimulus payload (56000 -> 56500) are raised, with the reasons recorded in
|
|
66
|
+
`config/performance_budgets.yml`. The per-file caps and the gzip total are
|
|
67
|
+
unchanged.
|
|
68
|
+
- **`NativeSelectComponent` defaults to its own chevron instead of the browser's
|
|
69
|
+
arrow** (`native_arrow: false`). A select was the one control in the library
|
|
70
|
+
with no visible state: the OS arrow cannot be styled or animated, so nothing
|
|
71
|
+
distinguished an open select from a closed one. The custom chevron already
|
|
72
|
+
existed, rotated on focus, and had to be asked for by name — which meant
|
|
73
|
+
nobody got it. Pass `native_arrow: true` to opt back in; worth doing on
|
|
74
|
+
mobile, where the OS renders its own picker anyway.
|
|
75
|
+
|
|
76
|
+
The component marker moves with it. In custom-arrow mode
|
|
77
|
+
`data-senren-component="native_select"` sits on the wrapper that positions the
|
|
78
|
+
chevron, and the `<select>` underneath still carries the name, the controller
|
|
79
|
+
and the selection. Anything selecting on `select[data-senren-component]` needs
|
|
80
|
+
updating.
|
|
81
|
+
- `InviteMemberDialogComponent` renders its role field through
|
|
82
|
+
`NativeSelectComponent` rather than a hand-written `<select>` whose styling
|
|
83
|
+
had been copied out of it — which is why it silently missed everything that
|
|
84
|
+
component gained afterwards, the chevron included.
|
|
85
|
+
|
|
86
|
+
### Fixed
|
|
87
|
+
|
|
88
|
+
- `FormComponent` yields the Rails form builder, as `docs/components.md` always
|
|
89
|
+
said it did. The block received the component instead, and because a
|
|
90
|
+
component is an `ActionView::Base` the documented `f.text_field :title,
|
|
91
|
+
class: "x"` did not raise: it reached the template-level helper and rendered
|
|
92
|
+
`name="title[{class: "x"}]"`, so the field posted under a name no controller
|
|
93
|
+
reads. Blocks that never touch `f`, and `with_content`, render as before.
|
|
94
|
+
|
|
95
|
+
Code that treated `|f|` as the component breaks: `f.tag`, `f.link_to`,
|
|
96
|
+
`f.render`, `f.model` and `f.url` now raise `NoMethodError` on the builder.
|
|
97
|
+
Call those on the view instead. A `FormComponent` rendered with neither a
|
|
98
|
+
block nor content now renders an empty `<form>` rather than raising.
|
|
99
|
+
- `TopNavComponent`'s default variant had no background. Its class,
|
|
100
|
+
`bg-[hsl(var(--senren-background))/0.88]`, put the alpha outside `hsl()`,
|
|
101
|
+
inside the brackets, so Tailwind emitted `hsl(...)/0.88` verbatim and the
|
|
102
|
+
browser dropped it as an invalid colour: the sticky header was transparent
|
|
103
|
+
over scrolled content. It is now `bg-[hsl(var(--senren-background)/0.88)]`,
|
|
104
|
+
the form every other translucent class in the library uses, and a test bans
|
|
105
|
+
the broken shape across `templates/` and `lib/`.
|
|
106
|
+
- `TableComponent` and `DataTableComponent` are now `relative`, so their
|
|
107
|
+
`sr-only` caption is positioned inside the component. `sr-only` is
|
|
108
|
+
`position: absolute`, and with nothing positioned the caption escaped the
|
|
109
|
+
components' own overflow clipping and was placed against the page. In a
|
|
110
|
+
fixed-height shell whose `<main>` scrolls, a captioned table below the fold
|
|
111
|
+
made the document taller, and the window scrolled into blank space under a
|
|
112
|
+
shell that should never scroll.
|
|
113
|
+
- `DataTableComponent` no longer breaks a `<td>` whose cell holds markup. The
|
|
114
|
+
cell value was written straight into `data-sort-value="..."`, and a badge,
|
|
115
|
+
link or anything else built with `tag` or `render` is a SafeBuffer, which ERB
|
|
116
|
+
does not escape: the markup's first `"` closed the attribute, the rest became
|
|
117
|
+
junk attributes, and every row sorted on the same `<span class=` fragment.
|
|
118
|
+
The sort value is now the cell's decoded text, escaped once, so a cell
|
|
119
|
+
showing `R&D` sorts as `R&D`. Plain-string and numeric cells are unchanged.
|
|
120
|
+
- `ThemeToggleComponent` rendered the letters `O`, `D` and `L` where an icon
|
|
121
|
+
belongs. It now ships sun and moon SVGs and swaps which is hidden, so the
|
|
122
|
+
correct icon is in the HTML the server sends rather than written in by
|
|
123
|
+
JavaScript after first paint. The icon names the theme the click switches to,
|
|
124
|
+
matching the label.
|
|
125
|
+
- `AccordionComponent` showed a static `+` and snapped open. It now has a
|
|
126
|
+
chevron that turns over, driven by `aria-expanded` so the icon cannot drift
|
|
127
|
+
out of step with the state assistive technology is told, and the panel
|
|
128
|
+
animates via `grid-template-rows: 0fr → 1fr` — which reaches the content's own
|
|
129
|
+
height without measuring it in JavaScript.
|
|
130
|
+
|
|
131
|
+
The `hidden` attribute is kept through the animation. Content that is visually
|
|
132
|
+
collapsed but present is still focusable and still read aloud, so the
|
|
133
|
+
controller drops `hidden` before opening and restores it once the closing
|
|
134
|
+
transition ends, with a timer for the cases where `transitionend` never
|
|
135
|
+
arrives: reduced motion, an off-screen element, an already-collapsed panel.
|
|
136
|
+
|
|
10
137
|
## [0.3.0] — 2026-09-01
|
|
11
138
|
|
|
12
139
|
### Breaking
|
data/CONTRIBUTING.md
CHANGED
|
@@ -36,6 +36,7 @@ bundle exec rubocop
|
|
|
36
36
|
bundle exec bundle-audit check --update
|
|
37
37
|
bun run controllers:check
|
|
38
38
|
bin/lint-fix # auto-fix RuboCop, ERB lint, controller JS
|
|
39
|
+
bin/i18n-sync --check # templates/locales/senren.en.yml matches the components
|
|
39
40
|
```
|
|
40
41
|
|
|
41
42
|
`bin/system` needs no setup: it uses a system Chrome and
|
|
@@ -87,6 +88,13 @@ real documentation/reference app is maintained separately in
|
|
|
87
88
|
- ViewComponent Ruby class under `Senren::` namespace.
|
|
88
89
|
- ERB template with `data-senren-component="<name>"` on root.
|
|
89
90
|
- Tailwind classes use semantic tokens only.
|
|
91
|
+
- Every word the component shows or announces (visible text, `aria-label`,
|
|
92
|
+
`sr-only`, a default `label:` or `placeholder:`, a sentence a controller will
|
|
93
|
+
say) goes through `senren_t('<component>.<key>', default: 'English')` with a
|
|
94
|
+
literal key and a literal default, and a controller receives it as a Stimulus
|
|
95
|
+
value, never as text in JavaScript. Run `bin/i18n-sync` to regenerate
|
|
96
|
+
`templates/locales/senren.en.yml` and add the same key to `senren.vi.yml`.
|
|
97
|
+
See `docs/i18n.md`.
|
|
90
98
|
- Variants/sizes declared as class-level constants.
|
|
91
99
|
- Stimulus controller iff client behavior is needed
|
|
92
100
|
(`app/javascript/controllers/senren/<name>_controller.js`).
|
|
@@ -115,6 +123,8 @@ real documentation/reference app is maintained separately in
|
|
|
115
123
|
templates, Stimulus controllers, or Importmap loading guidance.
|
|
116
124
|
- Run `bun run controllers:check` before pushing if you touched
|
|
117
125
|
`templates/controllers/*.js`.
|
|
126
|
+
- Run `bin/i18n-sync --check` before pushing if you touched text in a
|
|
127
|
+
component; `bin/i18n-sync` fixes what it reports.
|
|
118
128
|
|
|
119
129
|
## Pull request workflow
|
|
120
130
|
|
data/README.md
CHANGED
|
@@ -80,6 +80,40 @@ change, no build step, no configuration. Switch at runtime with
|
|
|
80
80
|
|
|
81
81
|
Details and the token table: [docs/visual_style.md](docs/visual_style.md).
|
|
82
82
|
|
|
83
|
+
## Translating components
|
|
84
|
+
|
|
85
|
+
Every word a component shows or announces (labels, `aria-label` and
|
|
86
|
+
screen-reader text, default placeholders, the calendar's month and weekday names,
|
|
87
|
+
and what a Stimulus controller says after load) goes through one helper with its
|
|
88
|
+
English as the default:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
senren_t('pagination.next', default: 'Next') # I18n.t("senren.pagination.next", default: "Next")
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
An app with no locale file renders exactly the English it always did, and the
|
|
95
|
+
installer copies none unless asked. To change a word, set its `senren.*` key in
|
|
96
|
+
your own locale file. To add a language, copy a shipped file and translate it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
bin/rails generate senren:install --locales vi # writes config/locales/senren.vi.yml
|
|
100
|
+
bin/rails senren:locales en # every key and its English, to translate from
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```yaml
|
|
104
|
+
# config/locales/senren.fr.yml - copy senren.en.yml, change `en:` to `fr:`, translate the values
|
|
105
|
+
fr:
|
|
106
|
+
senren:
|
|
107
|
+
pagination:
|
|
108
|
+
previous: "Précédent"
|
|
109
|
+
next: "Suivant"
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The gem ships `en` and `vi`. A key you leave out stays English, and a missing
|
|
113
|
+
key never raises. [docs/i18n.md](docs/i18n.md) has the key naming, the values a
|
|
114
|
+
controller reads, what is not translated, and how to move components you copied
|
|
115
|
+
before this existed.
|
|
116
|
+
|
|
83
117
|
## Daily commands
|
|
84
118
|
|
|
85
119
|
```bash
|
|
@@ -88,6 +122,7 @@ bin/rails generate senren:component picker --client # custom component with Sti
|
|
|
88
122
|
bin/rails generate senren:component picker --no-client # without Stimulus
|
|
89
123
|
bin/rails senren:add dialog --client # install interactive official component
|
|
90
124
|
bin/rails senren:add button # install static official component
|
|
125
|
+
bin/rails senren:locales vi # copy a shipped translation to config/locales
|
|
91
126
|
bundle exec rails senren:add form input # equivalent alternate entry point
|
|
92
127
|
bin/rails senren:skill:sync # rebuild .senren/skill.md
|
|
93
128
|
bin/rails senren:agents:sync # rebuild .senren/agent-rules + adapters
|
|
@@ -263,6 +298,7 @@ bun run controllers:check # lint + syntax check for templates/controllers
|
|
|
263
298
|
bun run controllers:lint:fix # auto-fix lint issues for controllers
|
|
264
299
|
bundle exec rake test:system # Stimulus/system tests
|
|
265
300
|
bin/lint-fix # auto-fix RuboCop, ERB lint, and controller JS
|
|
301
|
+
bin/i18n-sync # rewrite templates/locales/senren.en.yml from the senren_t calls
|
|
266
302
|
```
|
|
267
303
|
|
|
268
304
|
## Contributing
|
data/docs/i18n.md
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
# Translating Senren components
|
|
2
|
+
|
|
3
|
+
Every word a component shows or announces can be translated from your app's
|
|
4
|
+
locale files, without editing a component. That covers the visible labels
|
|
5
|
+
("Previous", "Next", "Close", "Today"), the `aria-label` and screen-reader text
|
|
6
|
+
("Toggle sidebar", "Go to slide 2"), the defaults for arguments such as
|
|
7
|
+
`label:` and `placeholder:`, the calendar's month and weekday names, and the
|
|
8
|
+
sentences a Stimulus controller writes into the page after it loads ("Copied",
|
|
9
|
+
"Slide 2 of 3").
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
Each string goes through one helper, `senren_t`, with its English as the
|
|
14
|
+
default:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
senren_t('pagination.next', default: 'Next')
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That is `I18n.t("senren.pagination.next", default: "Next")`. If your app defines
|
|
21
|
+
`senren.pagination.next` for the current locale it wins; if not, the English in
|
|
22
|
+
the component is used. Two things follow:
|
|
23
|
+
|
|
24
|
+
- **Nothing is required for English.** Install Senren, render a component, and it
|
|
25
|
+
says what it always said. There is no locale file to forget.
|
|
26
|
+
- **A missing translation never raises**, not even with
|
|
27
|
+
`config.i18n.raise_on_missing_translations = true`, because I18n only reports
|
|
28
|
+
a key that has no default.
|
|
29
|
+
|
|
30
|
+
Keys are `senren.<component>.<key>`. `senren.shared.*` holds the few words that
|
|
31
|
+
are the same word in more than one component (`open`, `close`).
|
|
32
|
+
|
|
33
|
+
## Install the locale files
|
|
34
|
+
|
|
35
|
+
`senren:install` copies no locale file: English needs none. Every file is
|
|
36
|
+
copied only when you ask for it by name:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bin/rails generate senren:install --locales vi # at install time
|
|
40
|
+
bin/rails senren:locales vi # any time later
|
|
41
|
+
bin/rails senren:locales en # every key and its English, to translate from
|
|
42
|
+
bin/rails senren:locales vi --force # replace your copy with the shipped one
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Today the gem ships `en` and `vi`. The files are yours once they are copied:
|
|
46
|
+
the installer never overwrites one that exists unless you ask for that file to
|
|
47
|
+
be replaced (`senren:locales <name> --force`, or `--force-locales` on
|
|
48
|
+
`senren:install`). The `--force` that refreshes Senren's own files leaves them
|
|
49
|
+
alone, so a re-run after a gem upgrade does not undo your wording.
|
|
50
|
+
|
|
51
|
+
A copied `senren.en.yml` has two side effects, which is why it is not copied by
|
|
52
|
+
default. Rails loads `config/locales` in name order, so it loads after your
|
|
53
|
+
`en.yml` and overrides any `senren.*` key you set there. And it pins today's
|
|
54
|
+
English: when a later Senren release improves a default, an app holding the copy
|
|
55
|
+
keeps the old wording. Copy it to translate from, and delete it once your own
|
|
56
|
+
language file exists.
|
|
57
|
+
|
|
58
|
+
A translation is only loaded when its file is in `config/locales`, and a locale
|
|
59
|
+
becomes known to I18n when a file for it is loaded. If your app builds a
|
|
60
|
+
language switcher from `I18n.available_locales`, copying `senren.vi.yml` is what
|
|
61
|
+
makes Vietnamese appear in it, which is why it is opt-in.
|
|
62
|
+
|
|
63
|
+
## Change one word
|
|
64
|
+
|
|
65
|
+
Define just that key in any locale file you already have. You do not need to
|
|
66
|
+
copy the whole file:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
# config/locales/en.yml
|
|
70
|
+
en:
|
|
71
|
+
senren:
|
|
72
|
+
pagination:
|
|
73
|
+
next: "Newer"
|
|
74
|
+
previous: "Older"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Everything you do not mention keeps its English. If you copied
|
|
78
|
+
`config/locales/senren.en.yml`, edit the value there instead: that file loads
|
|
79
|
+
after `en.yml` and wins.
|
|
80
|
+
|
|
81
|
+
## Add a language
|
|
82
|
+
|
|
83
|
+
1. Run `bin/rails senren:locales en`, then copy `config/locales/senren.en.yml` to
|
|
84
|
+
`config/locales/senren.fr.yml`.
|
|
85
|
+
2. Change the top-level key from `en:` to `fr:`.
|
|
86
|
+
3. Translate the values.
|
|
87
|
+
|
|
88
|
+
```yaml
|
|
89
|
+
fr:
|
|
90
|
+
senren:
|
|
91
|
+
pagination:
|
|
92
|
+
label: "Pagination"
|
|
93
|
+
previous: "Précédent"
|
|
94
|
+
next: "Suivant"
|
|
95
|
+
cart:
|
|
96
|
+
remove_item: "Retirer %{name}"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Keep every `%{name}` spelled exactly as it is. They are filled in when the
|
|
100
|
+
component renders. A translation that names a variable the component does not
|
|
101
|
+
pass (a typo such as `%{nmae}`) cannot be filled in, so that string falls back to
|
|
102
|
+
its English rather than raising. You can
|
|
103
|
+
move a variable anywhere in the sentence, and leave one out if the sentence does
|
|
104
|
+
not need it. A key you delete falls back to English, so a partial translation is
|
|
105
|
+
a working translation.
|
|
106
|
+
|
|
107
|
+
Then select the locale the way your app already does, for example
|
|
108
|
+
`I18n.locale = :fr` in an `around_action`. A word you did not translate shows in
|
|
109
|
+
English, because the English is the default.
|
|
110
|
+
|
|
111
|
+
If you would like the language included in the gem, send the file in a pull
|
|
112
|
+
request: `templates/locales/senren.<locale>.yml`, with the same keys as
|
|
113
|
+
`senren.en.yml`. `test/i18n_catalog_test.rb` checks that the keys and the
|
|
114
|
+
`%{}` variables match.
|
|
115
|
+
|
|
116
|
+
## Text you pass in yourself
|
|
117
|
+
|
|
118
|
+
An argument you pass is used exactly as given and is never looked up:
|
|
119
|
+
|
|
120
|
+
```erb
|
|
121
|
+
<%= render Senren::PaginationComponent.new(label: "Pages", current_page: 2, total_pages: 5) %>
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Only the argument you leave out gets the translated default. Two details:
|
|
125
|
+
|
|
126
|
+
- `label: nil` still means "no label". For an attribute such as `aria-label` that
|
|
127
|
+
omits it, as it always has. Pass `""` for an empty string and leave the
|
|
128
|
+
argument out for the default.
|
|
129
|
+
- Words that arrive as *data* are yours to supply: a column header built from the
|
|
130
|
+
key `:created_at` ("Created at"), the items of a menu, the text in a slot. Give
|
|
131
|
+
`TableComponent` and `DataTableComponent` a `label:` per column to control it.
|
|
132
|
+
|
|
133
|
+
Values you interpolate into a translation are text. A name containing `<b>`
|
|
134
|
+
shows as the characters `<b>`, not as an element.
|
|
135
|
+
|
|
136
|
+
## What a controller says
|
|
137
|
+
|
|
138
|
+
Five controllers write a sentence into the page after it loads. They no longer
|
|
139
|
+
carry the English in JavaScript: the component renders each one into a Stimulus
|
|
140
|
+
value, and the controller reads it.
|
|
141
|
+
|
|
142
|
+
| Controller | Value | Key |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `clipboard` | `copied-status` | `senren.clipboard.copied_status` |
|
|
145
|
+
| `api_key_field` | `copied-status` | `senren.api_key_field.copied_status` |
|
|
146
|
+
| `carousel` | `status-template` | `senren.carousel.status` |
|
|
147
|
+
| `theme_toggle` | `light-label`, `dark-label` | `senren.theme_toggle.light`, `.dark` |
|
|
148
|
+
| `rich_text_editor_lite` | `link-prompt` | `senren.rich_text_editor_lite.link_prompt` |
|
|
149
|
+
|
|
150
|
+
The carousel's value is a template. Its `%{current}` and `%{total}` are left in
|
|
151
|
+
for the controller to fill in on every slide change, so a translation has to keep
|
|
152
|
+
them.
|
|
153
|
+
|
|
154
|
+
## Components you copied before this existed
|
|
155
|
+
|
|
156
|
+
`bin/rails senren:add <anything>` adds the `senren_t` helper to your
|
|
157
|
+
`app/components/senren/base_component.rb` if it is missing, so components you add
|
|
158
|
+
from now on work.
|
|
159
|
+
|
|
160
|
+
Components you copied earlier still have their English written into them. To
|
|
161
|
+
translate one, copy it again with `bin/rails senren:add pagination --force`
|
|
162
|
+
(which replaces any edits you made to that component), or change its strings by
|
|
163
|
+
hand. It is one call per string: swap `"Next"` for
|
|
164
|
+
`senren_t('pagination.next', default: 'Next')`. `senren.en.yml` lists the key for
|
|
165
|
+
every string.
|
|
166
|
+
|
|
167
|
+
## Writing a component or an edit that has text
|
|
168
|
+
|
|
169
|
+
Anything a person reads or a screen reader announces goes through `senren_t`
|
|
170
|
+
with a literal key and a literal English default:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
def initialize(label: senren_t('tabs.label', default: 'Tabs'), **html)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```erb
|
|
177
|
+
<button><%= senren_t('date_picker.today', default: 'Today') %></button>
|
|
178
|
+
<button aria-label="<%= senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name]) %>">
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- Use the component's own namespace. Use `shared` only for a word that more than
|
|
182
|
+
one component says.
|
|
183
|
+
- Put the English where the call is. Do not write it in a locale file first.
|
|
184
|
+
- Give an argument its default in the signature, as above. Ruby evaluates it
|
|
185
|
+
when the component is built, in the locale that is current then.
|
|
186
|
+
- A string a controller needs goes to it as a Stimulus value, never as text in the
|
|
187
|
+
JavaScript.
|
|
188
|
+
- Symbols that are not words (`×`, `+`, `*`) stay as they are.
|
|
189
|
+
|
|
190
|
+
If you are working on the gem itself, `bin/i18n-sync` regenerates
|
|
191
|
+
`templates/locales/senren.en.yml` from the calls, and `bin/i18n-sync --check`
|
|
192
|
+
reports whether it is current. `senren.vi.yml` is written by hand. The tests fail
|
|
193
|
+
if the two files disagree with the code or with each other, if a call is not
|
|
194
|
+
literal, and if a template shows a word that did not go through `senren_t`.
|
|
@@ -11,6 +11,24 @@ module Senren
|
|
|
11
11
|
class_option :force, type: :boolean, default: false,
|
|
12
12
|
desc: 'Overwrite existing Senren-managed files.'
|
|
13
13
|
|
|
14
|
+
# Nothing by default: English renders with no file. Every locale, English
|
|
15
|
+
# included, is asked for by name. A shipped translation adds its locale to
|
|
16
|
+
# I18n.available_locales, which a language switcher built from that list
|
|
17
|
+
# will then offer; a copied English file overrides the app's own en.yml.
|
|
18
|
+
#
|
|
19
|
+
# `enum:` makes Thor refuse an unknown locale while it parses the options,
|
|
20
|
+
# before any step has run. Refusing in the middle would leave an app with the
|
|
21
|
+
# components copied and no instruction files.
|
|
22
|
+
class_option :locales, type: :array, default: [],
|
|
23
|
+
enum: Senren::Rails::LocaleInstaller.available,
|
|
24
|
+
desc: 'Translations to copy into config/locales.'
|
|
25
|
+
|
|
26
|
+
# --force refreshes the files Senren owns. A locale file stops being one
|
|
27
|
+
# the moment it is copied: it is the app's translation. So replacing it is
|
|
28
|
+
# its own flag, and a routine --force re-run cannot undo someone's wording.
|
|
29
|
+
class_option :force_locales, type: :boolean, default: false,
|
|
30
|
+
desc: 'Replace locale files that already exist with the shipped ones.'
|
|
31
|
+
|
|
14
32
|
def create_senren_dir
|
|
15
33
|
empty_directory '.senren'
|
|
16
34
|
end
|
|
@@ -41,6 +59,23 @@ module Senren
|
|
|
41
59
|
copy_file Senren::Rails.registry_path, '.senren/registry.yml'
|
|
42
60
|
end
|
|
43
61
|
|
|
62
|
+
# Components work without these: the English is in the code. Nothing is
|
|
63
|
+
# copied unless asked for, English included -- a copied senren.en.yml would
|
|
64
|
+
# override the app's own en.yml and pin today's wording (see
|
|
65
|
+
# LocaleInstaller). An existing file is a translation someone wrote, so it
|
|
66
|
+
# is kept unless --force-locales is given.
|
|
67
|
+
def copy_locale_files
|
|
68
|
+
locales = options[:locales]
|
|
69
|
+
if locales.empty?
|
|
70
|
+
say_status :senren, 'no locale files copied: English needs none (bin/rails senren:locales vi to add one)'
|
|
71
|
+
return
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
say_status :senren, "copying locale files (#{locales.join(', ')}) to config/locales"
|
|
75
|
+
installer = Senren::Rails::LocaleInstaller.new(paths: host_paths)
|
|
76
|
+
installer.install(locales: locales, force: options[:force_locales])
|
|
77
|
+
end
|
|
78
|
+
|
|
44
79
|
# Switches Stimulus to on-demand loading instead of documenting it.
|
|
45
80
|
#
|
|
46
81
|
# Rails' default is `eagerLoadControllersFrom("controllers", application)`,
|
|
@@ -75,6 +110,7 @@ module Senren
|
|
|
75
110
|
def print_next_steps
|
|
76
111
|
say "\nSenren installed."
|
|
77
112
|
say 'Next: bin/rails senren:add button card badge alert dialog'
|
|
113
|
+
say 'Translate the components: bin/rails senren:locales vi (see docs/i18n.md)'
|
|
78
114
|
say 'Then: bin/rails senren:agents:sync'
|
|
79
115
|
end
|
|
80
116
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
require 'uri'
|
|
2
|
+
require 'active_support/i18n'
|
|
2
3
|
require 'view_component'
|
|
3
4
|
|
|
4
5
|
module Senren
|
|
@@ -105,6 +106,44 @@ module Senren
|
|
|
105
106
|
"#{base}-#{seed.parameterize}"
|
|
106
107
|
end
|
|
107
108
|
|
|
109
|
+
# Translates a string the component shows or announces: a visible label, an
|
|
110
|
+
# aria-label, sr-only text, a default placeholder, an announcement a Stimulus
|
|
111
|
+
# controller will read out.
|
|
112
|
+
#
|
|
113
|
+
# senren_t('pagination.next', default: 'Next')
|
|
114
|
+
# senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name])
|
|
115
|
+
#
|
|
116
|
+
# `default:` is the English text and it is required. It is what renders when
|
|
117
|
+
# the host app has no `senren.<key>` translation, which is why installing
|
|
118
|
+
# Senren needs no locale file, why a key nobody translated never raises (not
|
|
119
|
+
# even under `config.i18n.raise_on_missing_translations`, which only fires
|
|
120
|
+
# for a key with no default), and why the output is the same as it was before
|
|
121
|
+
# any string was translatable.
|
|
122
|
+
#
|
|
123
|
+
# This is I18n.t on purpose, not ViewComponent's `t`. `t` needs a view
|
|
124
|
+
# context, so it cannot run in a unit test that renders a template without
|
|
125
|
+
# Rails; it resolves a leading-dot key against the component's own sidecar
|
|
126
|
+
# scope, which is the lookup this namespace must not have; and it marks keys
|
|
127
|
+
# ending in `_html` as safe, which no Senren string is.
|
|
128
|
+
#
|
|
129
|
+
# Keys are `senren.<component>.<key>`; `senren.shared.<key>` is for a word
|
|
130
|
+
# that is the same word in more than one component. Interpolate with `%{}`.
|
|
131
|
+
# With no values passed the raw template comes back untouched, which is how a
|
|
132
|
+
# Stimulus controller is handed a string it fills in itself.
|
|
133
|
+
#
|
|
134
|
+
# A translation that names a variable the call does not pass (`%{nmae}`
|
|
135
|
+
# for `%{name}`) would raise I18n::MissingInterpolationArgument and turn a
|
|
136
|
+
# typo in a locale file into a 500. It falls back to the English instead,
|
|
137
|
+
# interpolated with the values the call did pass.
|
|
138
|
+
#
|
|
139
|
+
# Public, because the unit tests that render a template without a view
|
|
140
|
+
# delegate every public component method to it.
|
|
141
|
+
def senren_t(key, default:, **options)
|
|
142
|
+
I18n.t("senren.#{key}", default: default, **options)
|
|
143
|
+
rescue I18n::MissingInterpolationArgument
|
|
144
|
+
I18n.interpolate(default, options)
|
|
145
|
+
end
|
|
146
|
+
|
|
108
147
|
private
|
|
109
148
|
|
|
110
149
|
# Callers write `data: { controller: "x" }` and `data: { "controller" => "x" }`
|
|
@@ -83,6 +83,22 @@ and obey it strictly.
|
|
|
83
83
|
```
|
|
84
84
|
Classes from families the component does not use need no modifier.
|
|
85
85
|
|
|
86
|
+
12. **Every word a component shows or announces goes through `senren_t`.**
|
|
87
|
+
Visible text, `aria-label`, `sr-only` text, a default `label:` or
|
|
88
|
+
`placeholder:`, and any sentence a Stimulus controller will say. The English
|
|
89
|
+
is the `default:`, the key is `<component>.<key>`, and both are literals:
|
|
90
|
+
```ruby
|
|
91
|
+
senren_t('pagination.next', default: 'Next')
|
|
92
|
+
senren_t('cart.remove_item', default: 'Remove %{name}', name: item[:name])
|
|
93
|
+
def initialize(label: senren_t('tabs.label', default: 'Tabs'), **html)
|
|
94
|
+
```
|
|
95
|
+
Never hard-code a sentence in a template, and never write one in a Stimulus
|
|
96
|
+
controller: hand the controller a translated string as a value. An app with
|
|
97
|
+
no locale file renders the English unchanged; to change a word, set its
|
|
98
|
+
`senren.*` key in a locale file, and to add a language, start from
|
|
99
|
+
`bin/rails senren:locales en` (`bin/rails senren:locales vi` installs a
|
|
100
|
+
shipped translation).
|
|
101
|
+
|
|
86
102
|
## File ownership
|
|
87
103
|
|
|
88
104
|
| Path | Owned by |
|
|
@@ -109,6 +109,10 @@ module Senren
|
|
|
109
109
|
- A button inside a form needs no `type:`. ButtonComponent omits the
|
|
110
110
|
attribute so the browser submits, as plain HTML does. Pass
|
|
111
111
|
`type: :button` only for a trigger that must not submit.
|
|
112
|
+
- Text a component shows or announces goes through
|
|
113
|
+
`senren_t('<component>.<key>', default: 'English')`, with a literal key
|
|
114
|
+
and default. Never hard-code a sentence in a template or in a Stimulus
|
|
115
|
+
controller; hand the controller a translated string as a value.
|
|
112
116
|
|
|
113
117
|
## Important Files
|
|
114
118
|
|
|
@@ -59,6 +59,30 @@ module Senren
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
RUBY
|
|
62
|
+
|
|
63
|
+
# Ruby appended to a host app's existing BaseComponent when it predates
|
|
64
|
+
# senren_t. A component copied by `senren:add` calls it, so without this a
|
|
65
|
+
# host that upgraded the gem and added one component would get a
|
|
66
|
+
# NoMethodError the first time that component rendered.
|
|
67
|
+
#
|
|
68
|
+
# Same arrangement as URL_HELPERS: apps installed earlier never receive the
|
|
69
|
+
# template again, so the helper is duplicated here, and
|
|
70
|
+
# test/i18n_catalog_test.rb pins the two bodies together.
|
|
71
|
+
TRANSLATION_HELPER = <<~'RUBY'
|
|
72
|
+
|
|
73
|
+
# Added by senren:add so components that translate their text can render.
|
|
74
|
+
require 'active_support/i18n'
|
|
75
|
+
|
|
76
|
+
module Senren
|
|
77
|
+
class BaseComponent
|
|
78
|
+
def senren_t(key, default:, **options)
|
|
79
|
+
I18n.t("senren.#{key}", default: default, **options)
|
|
80
|
+
rescue I18n::MissingInterpolationArgument
|
|
81
|
+
I18n.interpolate(default, options)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
RUBY
|
|
62
86
|
end
|
|
63
87
|
end
|
|
64
88
|
end
|
|
@@ -19,6 +19,7 @@ module Senren
|
|
|
19
19
|
).freeze
|
|
20
20
|
BASE_COMPONENT_TEMPLATE = File.join(INSTALL_GENERATOR_TEMPLATES, 'base_component.rb.tt').freeze
|
|
21
21
|
BASE_URL_HELPER_PATCH = BaseComponentPatch::URL_HELPERS
|
|
22
|
+
BASE_TRANSLATION_HELPER_PATCH = BaseComponentPatch::TRANSLATION_HELPER
|
|
22
23
|
|
|
23
24
|
attr_reader :registry, :paths, :stdout
|
|
24
25
|
|
|
@@ -35,6 +36,7 @@ module Senren
|
|
|
35
36
|
validate_client_override!(component_names, client_override)
|
|
36
37
|
paths.ensure_dirs!
|
|
37
38
|
ensure_base_component_url_helpers!
|
|
39
|
+
ensure_base_component_translation_helper!
|
|
38
40
|
|
|
39
41
|
requested = Array(component_names).map(&:to_s)
|
|
40
42
|
wanted.each do |name|
|
|
@@ -85,6 +87,25 @@ module Senren
|
|
|
85
87
|
copy_file(BASE_COMPONENT_TEMPLATE, dest, force: false, label: 'base_component.rb')
|
|
86
88
|
end
|
|
87
89
|
|
|
90
|
+
# A component that translates its text calls senren_t, so a host whose
|
|
91
|
+
# BaseComponent predates it needs the helper before the component renders.
|
|
92
|
+
# Runs after the URL helper step, which copies the whole current template
|
|
93
|
+
# (helper included) when there is no base component at all, so in that
|
|
94
|
+
# case there is nothing left to add.
|
|
95
|
+
#
|
|
96
|
+
# A destination that escapes the app root was already reported by the URL
|
|
97
|
+
# step, so this one declines without saying it a second time.
|
|
98
|
+
def ensure_base_component_translation_helper!
|
|
99
|
+
dest = paths.base_component_path
|
|
100
|
+
|
|
101
|
+
return unless SafeWrite.inside?(dest, paths.root)
|
|
102
|
+
return unless dest.exist?
|
|
103
|
+
return if dest.read.include?('def senren_t')
|
|
104
|
+
|
|
105
|
+
SafeWrite.write!(dest, dest.read + BASE_TRANSLATION_HELPER_PATCH, paths.root, 'base_component.rb')
|
|
106
|
+
stdout.puts " update #{dest} (senren_t)"
|
|
107
|
+
end
|
|
108
|
+
|
|
88
109
|
# Delegates to SafeWrite so an intermediate symlinked directory is caught,
|
|
89
110
|
# not just a symlinked leaf. `app/components/senren -> /elsewhere` used to
|
|
90
111
|
# pass every check here.
|
|
@@ -24,6 +24,11 @@ module Senren
|
|
|
24
24
|
|
|
25
25
|
def stylesheet_path = root.join('app', 'assets', 'stylesheets', 'senren.css')
|
|
26
26
|
|
|
27
|
+
# Where the host's translations of component text live. One file per
|
|
28
|
+
# locale, so a team adds a language by adding a file.
|
|
29
|
+
def locales_dir = root.join('config', 'locales')
|
|
30
|
+
def locale_file(locale) = locales_dir.join("senren.#{locale}.yml")
|
|
31
|
+
|
|
27
32
|
def stimulus_dir = root.join('app', 'javascript', 'controllers', 'senren')
|
|
28
33
|
|
|
29
34
|
def github_dir = root.join('.github')
|