testimonials 0.7.7 → 0.7.9

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +28 -0
  4. data/app/controllers/testimonials/nps_responses_controller.rb +8 -0
  5. data/app/models/testimonials/nps_response.rb +17 -0
  6. data/app/views/testimonials/nps_responses/index.html.erb +81 -6
  7. data/config/locales/testimonials.ar.yml +12 -0
  8. data/config/locales/testimonials.bg.yml +12 -0
  9. data/config/locales/testimonials.bn.yml +12 -0
  10. data/config/locales/testimonials.de.yml +12 -0
  11. data/config/locales/testimonials.el.yml +12 -0
  12. data/config/locales/testimonials.en.yml +12 -0
  13. data/config/locales/testimonials.es.yml +12 -0
  14. data/config/locales/testimonials.fr.yml +12 -0
  15. data/config/locales/testimonials.hi.yml +12 -0
  16. data/config/locales/testimonials.hr.yml +12 -0
  17. data/config/locales/testimonials.id.yml +12 -0
  18. data/config/locales/testimonials.it.yml +12 -0
  19. data/config/locales/testimonials.ja.yml +12 -0
  20. data/config/locales/testimonials.ko.yml +12 -0
  21. data/config/locales/testimonials.lb.yml +12 -0
  22. data/config/locales/testimonials.nl.yml +12 -0
  23. data/config/locales/testimonials.pl.yml +12 -0
  24. data/config/locales/testimonials.pt.yml +12 -0
  25. data/config/locales/testimonials.ro.yml +12 -0
  26. data/config/locales/testimonials.ru.yml +12 -0
  27. data/config/locales/testimonials.th.yml +12 -0
  28. data/config/locales/testimonials.tr.yml +12 -0
  29. data/config/locales/testimonials.uk.yml +12 -0
  30. data/config/locales/testimonials.ur.yml +12 -0
  31. data/config/locales/testimonials.vi.yml +12 -0
  32. data/config/locales/testimonials.zh-CN.yml +12 -0
  33. data/lib/generators/testimonials/install/install_generator.rb +13 -2
  34. data/lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt +2 -0
  35. data/lib/generators/testimonials/install/templates/{initializer.rb → initializer.rb.tt} +8 -0
  36. data/lib/generators/testimonials/nps/nps_generator.rb +37 -0
  37. data/lib/generators/testimonials/nps/templates/create_testimonials_nps_responses.rb.tt +20 -0
  38. data/lib/generators/testimonials/tenant/templates/add_tenant_to_testimonials.rb.tt +6 -2
  39. data/lib/testimonials/dashboard.css +51 -0
  40. data/lib/testimonials/dashboard.js +16 -0
  41. data/lib/testimonials/seeds.rb +2 -1
  42. data/lib/testimonials/version.rb +1 -1
  43. metadata +4 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c9b9d504e0560e2e38bec8dfe4ec41c3db57b1abb6c4df66f8781502494bb44
4
- data.tar.gz: de8db179e088abaa205551cadd2610a539a3444e42bebc7e2265499db45a23e3
3
+ metadata.gz: 746c0b7bc1421a8aadfbbbd7e9d41cb4aabc81ff0c6afa60669e4d9c06e963b0
4
+ data.tar.gz: 3914ba53cef25fcb6f541e6b4176e1d6135205cb37cdcc05b289125d456f1bbb
5
5
  SHA512:
6
- metadata.gz: cae8ec4cf6bcc3681b281ceea1fe29fd132f631a2473bceecc7ae4f0f1141bfb931612730cc509f10299bcecef0fd91f9eafe719f805b42e9a084304986a28d2
7
- data.tar.gz: 3503ec49afd1137817eb4fd1d2b6b184cfee69de4e936a2e05807606230b4c615180dd8b903de21d146c6a229dd2d8d71a8aef1c1792266ad75baeaf877be545
6
+ metadata.gz: 55f8694212e3f1395481121efd0f67c8474e9422173ba63e9b05cf66f40320b340301a1be39537cfbeb8ce17292b289d4f73247b2a05742e52f79b1bad6d47ae
7
+ data.tar.gz: 1a57a1aa934adc3d3b92199d922effc5ec36d66dc9138f819b1b153f754431c5e5bd7bea4f21e65fe51bb725d732e648acf7f389f8168ac277f174e220130b19
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.7.9
4
+
5
+ - NPS is now optional at install time. `bin/rails generate testimonials:install
6
+ --skip-nps` leaves out the `testimonials_nps_responses` table and writes
7
+ `config.nps = false`, so an app that only wants testimonials carries neither
8
+ the table nor the NPS tab. `bin/rails generate testimonials:nps` adds the
9
+ table later, mirroring the `testimonials:tenant` upgrade path.
10
+ - The guard is `config.nps`, not table introspection — nothing checks the
11
+ schema at runtime and nothing touches the database at boot. Consequently
12
+ `/testimonials/nps_responses` now 404s when `config.nps` is false instead of
13
+ still serving history; flip the flag back to read old responses. The
14
+ dashboard nav already hid the tab, so only a typed URL reached it.
15
+ - `testimonials:seed_demo` skips NPS rows when the flow is off, and the
16
+ `testimonials:tenant` migration skips the NPS table when it isn't there.
17
+
18
+ ## 0.7.8
19
+
20
+ - The NPS dashboard now says what the score means. The card carries the band
21
+ the number falls into (needs work / good / great / excellent), a scale
22
+ showing where it sits between −100 and +100, and a note when fewer than 30
23
+ responses make the number too thin to read. A collapsed "How is this
24
+ calculated, and what is a good score?" panel spells out the formula, the four
25
+ bands, and the caveats — industry, trend, comments. Translated into all 26
26
+ locales.
27
+ - The marker's position rides on a `data-nps-marker` attribute that
28
+ `dashboard.js` reads, so the page still ships no inline styles for a strict
29
+ `style-src` to refuse.
30
+
3
31
  ## 0.7.7
4
32
 
5
33
  - Shrank the gem from 1.0 MB back to ~90 KB by dropping the bundled 1.1 MB demo
data/README.md CHANGED
@@ -33,6 +33,15 @@ bin/rails db:migrate
33
33
  The generator writes the initializer, the migration, and mounts the engine at
34
34
  `/testimonials`. Nothing renders until the widget is opened.
35
35
 
36
+ Don't want NPS? Install without it — no table, no dashboard tab, no prompt:
37
+
38
+ ```bash
39
+ bin/rails generate testimonials:install --skip-nps
40
+ ```
41
+
42
+ Changed your mind later: `bin/rails generate testimonials:nps`, migrate, and
43
+ set `config.nps = true`.
44
+
36
45
  Optional demo data:
37
46
 
38
47
  ```bash
@@ -242,10 +251,29 @@ throttle would allow it. Detractors (0–6) trigger `on_detractor`, which pairs
242
251
  well with [ideasbugs](https://github.com/yshmarov/ideasbugs). Your score and
243
252
  every response live at `/testimonials/nps_responses`.
244
253
 
254
+ | 1. One question, eleven taps | 2. The reason, optional |
255
+ | --- | --- |
256
+ | ![The NPS prompt: how likely are you to recommend us, scored 0 to 10](https://raw.githubusercontent.com/yshmarov/testimonials/main/docs/screenshots/nps-01-score.jpg) | ![The follow-up asking for the main reason behind the score](https://raw.githubusercontent.com/yshmarov/testimonials/main/docs/screenshots/nps-02-comment.png) |
257
+ | Ten seconds of work, and "Not now" is always there. | The comment is where the actionable half lives. Promoters go straight on to the testimonial form. |
258
+
259
+ The score is `% promoters − % detractors`, from −100 to +100, and the dashboard
260
+ reads it against the usual benchmarks so nobody has to go and google them:
261
+ below 0 needs work, 0–29 is good, 30–69 is great, 70 and up is world-class. A
262
+ scale under the number shows where you sit, and under 30 responses the card
263
+ says the sample is too thin to read much into.
264
+
265
+ <img src="https://raw.githubusercontent.com/yshmarov/testimonials/main/docs/screenshots/nps-dashboard.jpg" alt="The NPS dashboard: a score of 41 badged Great, a scale marking where it sits between −100 and +100, promoter/passive/detractor counts, and the collapsed panel explaining the formula and the benchmarks" width="820">
266
+
245
267
  `/testimonials/nps/new` is the same question on its own page — the link for an
246
268
  email campaign, where there is no app session to prompt inside of. A promoter
247
269
  who scores 9–10 gets the testimonial form right there on the page.
248
270
 
271
+ NPS is optional. `config.nps = false` — what `--skip-nps` writes at install —
272
+ turns off the prompt, the public page, the `/api/stats` score and the dashboard
273
+ tab, and `/testimonials/nps_responses` then 404s. Turning it off on an install
274
+ that has the table hides existing responses along with everything else; flip it
275
+ back to read them.
276
+
249
277
  ## The public pages
250
278
 
251
279
  Two standalone, self-styled pages for people outside the app — drop either link
@@ -8,11 +8,15 @@ module Testimonials
8
8
  layout :testimonials_admin_layout
9
9
 
10
10
  before_action :require_admin
11
+ # An install run with --skip-nps has no table behind these pages, and the
12
+ # nav already hides the tab whenever config.nps is off.
13
+ before_action :require_nps
11
14
  before_action :set_response, only: :show
12
15
 
13
16
  def index
14
17
  scope = NpsResponse.for_tenant(current_tenant)
15
18
  @score = NpsResponse.score(scope)
19
+ @band = NpsResponse.band(@score)
16
20
  @total = scope.count
17
21
  @promoters = scope.where(score: 9..10).count
18
22
  @passives = scope.where(score: 7..8).count
@@ -30,6 +34,10 @@ module Testimonials
30
34
 
31
35
  private
32
36
 
37
+ def require_nps
38
+ head :not_found unless Testimonials.config.nps
39
+ end
40
+
33
41
  def set_response
34
42
  @response = NpsResponse.for_tenant(current_tenant).find(params[:id])
35
43
  end
@@ -21,5 +21,22 @@ module Testimonials
21
21
  detractors = scope.where(score: 0..6).count
22
22
  ((promoters - detractors) * 100.0 / total).round
23
23
  end
24
+
25
+ # The industry-standard reading of an NPS number, so the dashboard can say
26
+ # whether a score is good without anyone having to go and google it.
27
+ def self.band(nps)
28
+ return nil if nps.nil?
29
+
30
+ case nps
31
+ when ...0 then :needs_work
32
+ when 0...30 then :good
33
+ when 30...70 then :great
34
+ else :excellent
35
+ end
36
+ end
37
+
38
+ # Below this many responses a single answer swings the score by several
39
+ # points, so the dashboard says so instead of implying the number is solid.
40
+ THIN_SAMPLE = 30
24
41
  end
25
42
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  <div class="page-head">
4
4
  <h1><%= t('testimonials.dashboard.nps', default: 'NPS') %></h1>
5
- <%# The shareable NPS page, same idea as the testimonial one. This dashboard
6
- still reads history when config.nps is off, but that page 404s — so the
7
- link needs both switches. %>
8
- <% if Testimonials.config.public_collection && Testimonials.config.nps %>
5
+ <%# The shareable NPS page, same idea as the testimonial one. config.nps is
6
+ already true here the controller 404s otherwise — so only the public
7
+ switch is left to check. %>
8
+ <% if Testimonials.config.public_collection %>
9
9
  <a class="button share-link" href="<%= nps_collection_url %>" target="_blank" rel="noopener"
10
10
  title="<%= nps_collection_url %>">
11
11
  🔗 <%= t('testimonials.dashboard.nps_page', default: 'Ask for NPS scores') %>
@@ -13,10 +13,59 @@
13
13
  <% end %>
14
14
  </div>
15
15
 
16
+ <%# How the industry reads a score, so nobody has to go and google it. Range
17
+ labels are digits on purpose — no translation needed for "< 0". %>
18
+ <% bands = [
19
+ { key: 'needs-work', range: '< 0',
20
+ name: t('testimonials.dashboard.nps_band_needs_work', default: 'Needs work'),
21
+ note: t('testimonials.dashboard.nps_guide_needs_work', default: 'More detractors than promoters.') },
22
+ { key: 'good', range: '0 – 29',
23
+ name: t('testimonials.dashboard.nps_band_good', default: 'Good'),
24
+ note: t('testimonials.dashboard.nps_guide_good', default: 'Where most companies land.') },
25
+ { key: 'great', range: '30 – 69',
26
+ name: t('testimonials.dashboard.nps_band_great', default: 'Great'),
27
+ note: t('testimonials.dashboard.nps_guide_great', default: 'Strong loyalty.') },
28
+ { key: 'excellent', range: '70 +',
29
+ name: t('testimonials.dashboard.nps_band_excellent', default: 'Excellent'),
30
+ note: t('testimonials.dashboard.nps_guide_excellent', default: 'World-class, and rare.') }
31
+ ] %>
32
+ <% current = bands.find { |b| b[:key] == @band.to_s.tr('_', '-') } if @band %>
33
+ <% if current
34
+ band_class = "badge nps-band-#{current[:key]}"
35
+ meter_label = "#{@score} — #{current[:name]} #{current[:note]}"
36
+ end %>
37
+
16
38
  <div class="stat-row">
17
- <div class="stat">
18
- <b><%= @score.nil? ? '—' : @score %></b>
39
+ <div class="stat stat-nps">
40
+ <div class="stat-headline">
41
+ <b><%= @score.nil? ? '—' : @score %></b>
42
+ <% if current %>
43
+ <span class="<%= band_class %>"><%= current[:name] %></span>
44
+ <% end %>
45
+ </div>
19
46
  <span><%= t('testimonials.dashboard.nps_score', default: 'NPS score') %></span>
47
+ <%# Where this score sits on the −100..100 scale, so the number reads as
48
+ good or bad at a glance. Kept LTR: it is a number line, not prose. %>
49
+ <% if current %>
50
+ <div class="nps-meter" role="img" aria-label="<%= meter_label %>">
51
+ <div class="nps-meter-track">
52
+ <i class="nps-seg nps-band-needs-work"></i><i class="nps-seg nps-band-good"></i>
53
+ <i class="nps-seg nps-band-great"></i><i class="nps-seg nps-band-excellent"></i>
54
+ <%# Position is data, not markup: dashboard.js places it, so a strict
55
+ style-src has no inline style to refuse. %>
56
+ <i class="nps-marker" data-nps-marker="<%= @score %>"></i>
57
+ </div>
58
+ <div class="nps-meter-ticks" aria-hidden="true">
59
+ <span class="tick-min">−100</span><span class="tick-zero">0</span>
60
+ <span class="tick-good">30</span><span class="tick-great">70</span>
61
+ <span class="tick-max">100</span>
62
+ </div>
63
+ </div>
64
+ <% end %>
65
+ <% if @total.positive? && @total < Testimonials::NpsResponse::THIN_SAMPLE %>
66
+ <p class="muted nps-sample"><%= t('testimonials.dashboard.nps_sample_note', count: @total,
67
+ default: 'Based on %{count} responses — too few to read much into.') %></p>
68
+ <% end %>
20
69
  </div>
21
70
  <div class="stat">
22
71
  <b><%= @promoters %></b>
@@ -32,6 +81,32 @@
32
81
  </div>
33
82
  </div>
34
83
 
84
+ <%# The formula and the benchmarks, closed by default — there when someone
85
+ wonders whether their number is any good, out of the way the rest of the time. %>
86
+ <details class="nps-guide">
87
+ <summary><%= t('testimonials.dashboard.nps_guide', default: 'How is this calculated, and what is a good score?') %></summary>
88
+ <div class="nps-guide-body">
89
+ <p><%= t('testimonials.dashboard.nps_guide_formula',
90
+ default: 'NPS = the % of promoters minus the % of detractors. Passives count in the total but on ' \
91
+ 'neither side, so they pull the score toward zero. The result runs from −100 to +100 — ' \
92
+ 'a score, not a percentage.') %></p>
93
+ <ul class="nps-bands">
94
+ <% bands.each do |band| %>
95
+ <% badge_class = "badge nps-band-#{band[:key]}" %>
96
+ <li>
97
+ <span class="<%= badge_class %>"><%= band[:name] %></span>
98
+ <b><%= band[:range] %></b>
99
+ <span class="muted"><%= band[:note] %></span>
100
+ </li>
101
+ <% end %>
102
+ </ul>
103
+ <p class="muted"><%= t('testimonials.dashboard.nps_guide_caveat',
104
+ default: 'Benchmarks move with the industry — software often sits near 40, airlines ' \
105
+ 'and internet providers near 0. The trend over time tells you more than any ' \
106
+ 'single number, and the comments tell you more than the score.') %></p>
107
+ </div>
108
+ </details>
109
+
35
110
  <div class="dashboard-shell <%= 'has-selected' if @selected_response %>">
36
111
  <aside class="dashboard-sidebar" aria-label="<%= t('testimonials.dashboard.nps_responses', default: 'NPS responses') %>">
37
112
  <div class="record-list">
@@ -102,5 +102,17 @@ ar:
102
102
  promoters: "المروّجون (9–10)"
103
103
  passives: "المحايدون (7–8)"
104
104
  detractors: "المنتقدون (0–6)"
105
+ nps_band_needs_work: "يحتاج إلى تحسين"
106
+ nps_band_good: "جيد"
107
+ nps_band_great: "جيد جدًا"
108
+ nps_band_excellent: "ممتاز"
109
+ nps_sample_note: "بناءً على %{count} ردًا — عدد قليل جدًا لبناء استنتاج عليه."
110
+ nps_guide: "كيف يُحتسب، وما هي النتيجة الجيدة؟"
111
+ nps_guide_formula: "NPS = نسبة المروّجين ناقص نسبة المنتقدين. المحايدون يُحتسبون ضمن الإجمالي لكن لا يُضافون إلى أي من الطرفين، لذلك يسحبون النتيجة نحو الصفر. تتراوح النتيجة بين −100 و+100، وهي مؤشر وليست نسبة مئوية."
112
+ nps_guide_needs_work: "عدد المنتقدين أكبر من عدد المروّجين."
113
+ nps_guide_good: "النطاق الذي تقع فيه معظم الشركات."
114
+ nps_guide_great: "ولاء قوي."
115
+ nps_guide_excellent: "مستوى عالمي، ونادر."
116
+ nps_guide_caveat: "تختلف المعايير حسب القطاع — البرمجيات غالبًا قرب 40، وشركات الطيران ومزوّدو الإنترنت قرب 0. الاتجاه عبر الوقت يقول أكثر من رقم واحد، والتعليقات تقول أكثر من النتيجة."
105
117
  score: "الدرجة"
106
118
  comment: "التعليق"
@@ -102,5 +102,17 @@ bg:
102
102
  promoters: "Промоутъри (9–10)"
103
103
  passives: "Неутрални (7–8)"
104
104
  detractors: "Критици (0–6)"
105
+ nps_band_needs_work: "Нужно е подобрение"
106
+ nps_band_good: "Добро"
107
+ nps_band_great: "Много добро"
108
+ nps_band_excellent: "Отлично"
109
+ nps_sample_note: "Въз основа на %{count} отговора — твърде малко за изводи."
110
+ nps_guide: "Как се изчислява и кой резултат е добър?"
111
+ nps_guide_formula: "NPS = % промотори минус % критици. Неутралните влизат в общия брой, но в нито една от двете страни, затова дърпат резултата към нулата. Стойността е от −100 до +100 — това е индекс, а не процент."
112
+ nps_guide_needs_work: "Критиците са повече от промоторите."
113
+ nps_guide_good: "Там, където са повечето компании."
114
+ nps_guide_great: "Силна лоялност."
115
+ nps_guide_excellent: "Световно ниво, среща се рядко."
116
+ nps_guide_caveat: "Ориентирите зависят от бранша — при софтуера често са около 40, при авиокомпаниите и интернет доставчиците — близо до 0. Тенденцията във времето казва повече от едно число, а коментарите — повече от резултата."
105
117
  score: "Резултат"
106
118
  comment: "Коментар"
@@ -102,5 +102,17 @@ bn:
102
102
  promoters: "প্রোমোটার (৯–১০)"
103
103
  passives: "নিরপেক্ষ (৭–৮)"
104
104
  detractors: "সমালোচক (০–৬)"
105
+ nps_band_needs_work: "উন্নতি দরকার"
106
+ nps_band_good: "ভালো"
107
+ nps_band_great: "খুব ভালো"
108
+ nps_band_excellent: "চমৎকার"
109
+ nps_sample_note: "%{count}টি উত্তরের ভিত্তিতে — সিদ্ধান্তে আসার জন্য যথেষ্ট নয়।"
110
+ nps_guide: "এটি কীভাবে হিসাব হয়, আর কত হলে ভালো?"
111
+ nps_guide_formula: "NPS = প্রোমোটারের শতাংশ বিয়োগ ডিট্র্যাক্টরের শতাংশ। নিরপেক্ষরা মোট সংখ্যায় ধরা হয় কিন্তু কোনো পক্ষেই যায় না, তাই স্কোরকে শূন্যের দিকে টানে। ফল −১০০ থেকে +১০০ পর্যন্ত — এটি স্কোর, শতাংশ নয়।"
112
+ nps_guide_needs_work: "প্রোমোটারের চেয়ে ডিট্র্যাক্টর বেশি।"
113
+ nps_guide_good: "বেশির ভাগ কোম্পানি এখানেই থাকে।"
114
+ nps_guide_great: "শক্ত আনুগত্য।"
115
+ nps_guide_excellent: "বিশ্বমানের, বিরল।"
116
+ nps_guide_caveat: "মানদণ্ড শিল্প অনুযায়ী বদলায় — সফটওয়্যারে প্রায়ই ৪০-এর কাছাকাছি, বিমান সংস্থা ও ইন্টারনেট সরবরাহকারীদের ০-এর কাছাকাছি। একক সংখ্যার চেয়ে সময়ের ধারা বেশি বলে, আর স্কোরের চেয়ে মন্তব্য বেশি বলে।"
105
117
  score: "স্কোর"
106
118
  comment: "মন্তব্য"
@@ -102,5 +102,17 @@ de:
102
102
  promoters: "Promotoren (9–10)"
103
103
  passives: "Passive (7–8)"
104
104
  detractors: "Detraktoren (0–6)"
105
+ nps_band_needs_work: "Verbesserungsbedarf"
106
+ nps_band_good: "Gut"
107
+ nps_band_great: "Sehr gut"
108
+ nps_band_excellent: "Hervorragend"
109
+ nps_sample_note: "Basiert auf %{count} Antworten — zu wenige für belastbare Schlüsse."
110
+ nps_guide: "Wie wird das berechnet, und was ist ein guter Wert?"
111
+ nps_guide_formula: "NPS = % der Promotoren minus % der Kritiker. Indifferente zählen zur Gesamtzahl, aber zu keiner Seite, und ziehen den Wert damit Richtung null. Das Ergebnis reicht von −100 bis +100 — ein Indexwert, kein Prozentsatz."
112
+ nps_guide_needs_work: "Mehr Kritiker als Promotoren."
113
+ nps_guide_good: "Der Bereich, in dem die meisten Unternehmen liegen."
114
+ nps_guide_great: "Starke Loyalität."
115
+ nps_guide_excellent: "Weltklasse und selten."
116
+ nps_guide_caveat: "Die Richtwerte hängen von der Branche ab — Software liegt oft bei rund 40, Fluggesellschaften und Internetanbieter nahe 0. Der Verlauf über die Zeit sagt mehr als eine einzelne Zahl, und die Kommentare sagen mehr als der Wert."
105
117
  score: "Wert"
106
118
  comment: "Kommentar"
@@ -102,5 +102,17 @@ el:
102
102
  promoters: "Υποστηρικτές (9–10)"
103
103
  passives: "Ουδέτεροι (7–8)"
104
104
  detractors: "Επικριτές (0–6)"
105
+ nps_band_needs_work: "Χρειάζεται βελτίωση"
106
+ nps_band_good: "Καλό"
107
+ nps_band_great: "Πολύ καλό"
108
+ nps_band_excellent: "Εξαιρετικό"
109
+ nps_sample_note: "Με βάση %{count} απαντήσεις — πολύ λίγες για ασφαλή συμπεράσματα."
110
+ nps_guide: "Πώς υπολογίζεται και ποια βαθμολογία θεωρείται καλή;"
111
+ nps_guide_formula: "NPS = ποσοστό υποστηρικτών μείον ποσοστό επικριτών. Οι ουδέτεροι μετρούν στο σύνολο αλλά σε καμία από τις δύο πλευρές, οπότε τραβούν τη βαθμολογία προς το μηδέν. Το αποτέλεσμα κυμαίνεται από −100 έως +100 — είναι δείκτης, όχι ποσοστό."
112
+ nps_guide_needs_work: "Περισσότεροι επικριτές από υποστηρικτές."
113
+ nps_guide_good: "Εκεί βρίσκονται οι περισσότερες εταιρείες."
114
+ nps_guide_great: "Ισχυρή πιστότητα."
115
+ nps_guide_excellent: "Παγκόσμιου επιπέδου, και σπάνιο."
116
+ nps_guide_caveat: "Τα σημεία αναφοράς αλλάζουν ανά κλάδο — το λογισμικό συχνά κοντά στο 40, οι αεροπορικές και οι πάροχοι ίντερνετ κοντά στο 0. Η τάση στον χρόνο λέει περισσότερα από έναν αριθμό, και τα σχόλια περισσότερα από τη βαθμολογία."
105
117
  score: "Βαθμός"
106
118
  comment: "Σχόλιο"
@@ -102,5 +102,17 @@ en:
102
102
  promoters: "Promoters (9–10)"
103
103
  passives: "Passives (7–8)"
104
104
  detractors: "Detractors (0–6)"
105
+ nps_band_needs_work: "Needs work"
106
+ nps_band_good: "Good"
107
+ nps_band_great: "Great"
108
+ nps_band_excellent: "Excellent"
109
+ nps_sample_note: "Based on %{count} responses — too few to read much into."
110
+ nps_guide: "How is this calculated, and what is a good score?"
111
+ nps_guide_formula: "NPS = the % of promoters minus the % of detractors. Passives count in the total but on neither side, so they pull the score toward zero. The result runs from −100 to +100 — a score, not a percentage."
112
+ nps_guide_needs_work: "More detractors than promoters."
113
+ nps_guide_good: "Where most companies land."
114
+ nps_guide_great: "Strong loyalty."
115
+ nps_guide_excellent: "World-class, and rare."
116
+ nps_guide_caveat: "Benchmarks move with the industry — software often sits near 40, airlines and internet providers near 0. The trend over time tells you more than any single number, and the comments tell you more than the score."
105
117
  score: "Score"
106
118
  comment: "Comment"
@@ -102,5 +102,17 @@ es:
102
102
  promoters: "Promotores (9–10)"
103
103
  passives: "Pasivos (7–8)"
104
104
  detractors: "Detractores (0–6)"
105
+ nps_band_needs_work: "Necesita mejorar"
106
+ nps_band_good: "Bueno"
107
+ nps_band_great: "Muy bueno"
108
+ nps_band_excellent: "Excelente"
109
+ nps_sample_note: "Basado en %{count} respuestas: demasiado pocas para sacar conclusiones."
110
+ nps_guide: "¿Cómo se calcula y qué puntuación es buena?"
111
+ nps_guide_formula: "NPS = % de promotores menos % de detractores. Los pasivos cuentan en el total pero en ninguno de los dos lados, así que empujan la puntuación hacia cero. El resultado va de −100 a +100: es un índice, no un porcentaje."
112
+ nps_guide_needs_work: "Más detractores que promotores."
113
+ nps_guide_good: "Donde se sitúa la mayoría de las empresas."
114
+ nps_guide_great: "Lealtad sólida."
115
+ nps_guide_excellent: "De primer nivel, y poco común."
116
+ nps_guide_caveat: "Las referencias cambian según el sector: el software suele rondar 40, y las aerolíneas y los proveedores de internet, cerca de 0. La tendencia en el tiempo dice más que una sola cifra, y los comentarios dicen más que la puntuación."
105
117
  score: "Puntuación"
106
118
  comment: "Comentario"
@@ -102,5 +102,17 @@ fr:
102
102
  promoters: "Promoteurs (9–10)"
103
103
  passives: "Passifs (7–8)"
104
104
  detractors: "Détracteurs (0–6)"
105
+ nps_band_needs_work: "À améliorer"
106
+ nps_band_good: "Bon"
107
+ nps_band_great: "Très bon"
108
+ nps_band_excellent: "Excellent"
109
+ nps_sample_note: "Basé sur %{count} réponses — trop peu pour en tirer des conclusions."
110
+ nps_guide: "Comment est-il calculé, et qu'est-ce qu'un bon score ?"
111
+ nps_guide_formula: "NPS = % de promoteurs moins % de détracteurs. Les passifs comptent dans le total mais dans aucun des deux camps, ce qui tire le score vers zéro. Le résultat va de −100 à +100 — un score, pas un pourcentage."
112
+ nps_guide_needs_work: "Plus de détracteurs que de promoteurs."
113
+ nps_guide_good: "Là où se situe la plupart des entreprises."
114
+ nps_guide_great: "Forte fidélité."
115
+ nps_guide_excellent: "Niveau mondial, et rare."
116
+ nps_guide_caveat: "Les repères varient selon le secteur — le logiciel tourne souvent autour de 40, les compagnies aériennes et les fournisseurs d'accès près de 0. La tendance dans le temps en dit plus qu'un chiffre isolé, et les commentaires en disent plus que le score."
105
117
  score: "Score"
106
118
  comment: "Commentaire"
@@ -102,5 +102,17 @@ hi:
102
102
  promoters: "प्रमोटर (9–10)"
103
103
  passives: "तटस्थ (7–8)"
104
104
  detractors: "आलोचक (0–6)"
105
+ nps_band_needs_work: "सुधार चाहिए"
106
+ nps_band_good: "अच्छा"
107
+ nps_band_great: "बहुत अच्छा"
108
+ nps_band_excellent: "उत्कृष्ट"
109
+ nps_sample_note: "%{count} जवाबों पर आधारित — निष्कर्ष निकालने के लिए बहुत कम।"
110
+ nps_guide: "यह कैसे निकलता है, और अच्छा स्कोर क्या है?"
111
+ nps_guide_formula: "NPS = प्रमोटर का प्रतिशत घटा डिट्रैक्टर का प्रतिशत। पैसिव कुल में गिने जाते हैं पर किसी पक्ष में नहीं, इसलिए वे स्कोर को शून्य की ओर खींचते हैं। नतीजा −100 से +100 तक होता है — यह स्कोर है, प्रतिशत नहीं।"
112
+ nps_guide_needs_work: "प्रमोटर से ज़्यादा डिट्रैक्टर हैं।"
113
+ nps_guide_good: "अधिकतर कंपनियाँ यहीं होती हैं।"
114
+ nps_guide_great: "मज़बूत वफ़ादारी।"
115
+ nps_guide_excellent: "विश्वस्तरीय, और दुर्लभ।"
116
+ nps_guide_caveat: "मानक उद्योग के हिसाब से बदलते हैं — सॉफ़्टवेयर अक्सर 40 के आसपास, एयरलाइंस और इंटरनेट प्रदाता 0 के करीब। किसी एक आँकड़े से ज़्यादा समय के साथ रुझान बताता है, और स्कोर से ज़्यादा टिप्पणियाँ।"
105
117
  score: "स्कोर"
106
118
  comment: "टिप्पणी"
@@ -102,5 +102,17 @@ hr:
102
102
  promoters: "Promotori (9–10)"
103
103
  passives: "Pasivni (7–8)"
104
104
  detractors: "Kritičari (0–6)"
105
+ nps_band_needs_work: "Treba poboljšati"
106
+ nps_band_good: "Dobro"
107
+ nps_band_great: "Vrlo dobro"
108
+ nps_band_excellent: "Izvrsno"
109
+ nps_sample_note: "Na temelju %{count} odgovora — premalo za zaključke."
110
+ nps_guide: "Kako se ovo računa i koji je rezultat dobar?"
111
+ nps_guide_formula: "NPS = postotak promotora minus postotak kritičara. Neutralni se broje u ukupan zbroj, ali ni na jednu stranu, pa vuku rezultat prema nuli. Rezultat je od −100 do +100 — to je indeks, a ne postotak."
112
+ nps_guide_needs_work: "Više kritičara nego promotora."
113
+ nps_guide_good: "Ondje gdje je većina tvrtki."
114
+ nps_guide_great: "Snažna lojalnost."
115
+ nps_guide_excellent: "Svjetska razina, i rijetkost."
116
+ nps_guide_caveat: "Mjerila se razlikuju po industriji — softver je često oko 40, zrakoplovne tvrtke i pružatelji interneta blizu 0. Kretanje kroz vrijeme govori više od jednog broja, a komentari više od rezultata."
105
117
  score: "Rezultat"
106
118
  comment: "Komentar"
@@ -102,5 +102,17 @@ id:
102
102
  promoters: "Promotor (9–10)"
103
103
  passives: "Pasif (7–8)"
104
104
  detractors: "Detraktor (0–6)"
105
+ nps_band_needs_work: "Perlu perbaikan"
106
+ nps_band_good: "Baik"
107
+ nps_band_great: "Sangat baik"
108
+ nps_band_excellent: "Luar biasa"
109
+ nps_sample_note: "Berdasarkan %{count} tanggapan — terlalu sedikit untuk disimpulkan."
110
+ nps_guide: "Bagaimana ini dihitung, dan berapa yang tergolong bagus?"
111
+ nps_guide_formula: "NPS = persentase promotor dikurangi persentase detraktor. Kelompok pasif dihitung dalam total tetapi tidak masuk ke salah satu sisi, sehingga menarik skor mendekati nol. Hasilnya berkisar dari −100 hingga +100 — sebuah skor, bukan persentase."
112
+ nps_guide_needs_work: "Detraktor lebih banyak daripada promotor."
113
+ nps_guide_good: "Kisaran tempat sebagian besar perusahaan berada."
114
+ nps_guide_great: "Loyalitas yang kuat."
115
+ nps_guide_excellent: "Kelas dunia, dan jarang."
116
+ nps_guide_caveat: "Tolok ukur berbeda di tiap industri — perangkat lunak sering di sekitar 40, maskapai dan penyedia internet mendekati 0. Tren dari waktu ke waktu lebih berarti daripada satu angka, dan komentar lebih berarti daripada skor."
105
117
  score: "Skor"
106
118
  comment: "Komentar"
@@ -102,5 +102,17 @@ it:
102
102
  promoters: "Promotori (9–10)"
103
103
  passives: "Passivi (7–8)"
104
104
  detractors: "Detrattori (0–6)"
105
+ nps_band_needs_work: "Da migliorare"
106
+ nps_band_good: "Buono"
107
+ nps_band_great: "Ottimo"
108
+ nps_band_excellent: "Eccellente"
109
+ nps_sample_note: "Basato su %{count} risposte: troppo poche per trarne conclusioni."
110
+ nps_guide: "Come si calcola e qual è un buon punteggio?"
111
+ nps_guide_formula: "NPS = % di promotori meno % di detrattori. I passivi contano nel totale ma in nessuna delle due parti, quindi spingono il punteggio verso lo zero. Il risultato va da −100 a +100: è un indice, non una percentuale."
112
+ nps_guide_needs_work: "Più detrattori che promotori."
113
+ nps_guide_good: "Dove si colloca la maggior parte delle aziende."
114
+ nps_guide_great: "Forte fedeltà."
115
+ nps_guide_excellent: "Livello mondiale, e raro."
116
+ nps_guide_caveat: "I valori di riferimento cambiano con il settore: il software si attesta spesso intorno a 40, le compagnie aeree e i provider internet vicino a 0. L'andamento nel tempo dice più di un singolo numero, e i commenti dicono più del punteggio."
105
117
  score: "Punteggio"
106
118
  comment: "Commento"
@@ -102,5 +102,17 @@ ja:
102
102
  promoters: "推奨者(9–10)"
103
103
  passives: "中立者(7–8)"
104
104
  detractors: "批判者(0–6)"
105
+ nps_band_needs_work: "要改善"
106
+ nps_band_good: "良好"
107
+ nps_band_great: "優秀"
108
+ nps_band_excellent: "卓越"
109
+ nps_sample_note: "%{count}件の回答に基づく結果です。判断するにはまだ少なすぎます。"
110
+ nps_guide: "どう計算され、どのくらいなら良いのか"
111
+ nps_guide_formula: "NPS = 推奨者の割合 − 批判者の割合。中立者は母数には入りますがどちらにも数えないため、スコアをゼロに近づけます。結果は −100 から +100 までで、パーセントではなくスコアです。"
112
+ nps_guide_needs_work: "推奨者より批判者が多い状態です。"
113
+ nps_guide_good: "多くの企業がこの範囲に入ります。"
114
+ nps_guide_great: "強いロイヤルティがあります。"
115
+ nps_guide_excellent: "世界水準で、めったにない値です。"
116
+ nps_guide_caveat: "目安は業界によって変わります。ソフトウェアは 40 前後、航空会社やインターネット回線事業者は 0 付近になりがちです。単独の数値より時間の推移が、スコアよりコメントのほうが多くを語ります。"
105
117
  score: "スコア"
106
118
  comment: "コメント"
@@ -102,5 +102,17 @@ ko:
102
102
  promoters: "추천 고객 (9–10)"
103
103
  passives: "중립 고객 (7–8)"
104
104
  detractors: "비추천 고객 (0–6)"
105
+ nps_band_needs_work: "개선 필요"
106
+ nps_band_good: "좋음"
107
+ nps_band_great: "매우 좋음"
108
+ nps_band_excellent: "탁월함"
109
+ nps_sample_note: "응답 %{count}건 기준 — 판단하기에는 너무 적습니다."
110
+ nps_guide: "어떻게 계산되며, 어느 정도면 좋은 점수인가요?"
111
+ nps_guide_formula: "NPS = 추천 고객 비율 − 비추천 고객 비율. 중립 고객은 전체에는 포함되지만 어느 쪽에도 들어가지 않아 점수를 0에 가깝게 만듭니다. 결과는 −100에서 +100 사이이며, 백분율이 아니라 점수입니다."
112
+ nps_guide_needs_work: "추천 고객보다 비추천 고객이 많습니다."
113
+ nps_guide_good: "대부분의 기업이 속하는 구간입니다."
114
+ nps_guide_great: "충성도가 높습니다."
115
+ nps_guide_excellent: "세계적인 수준으로, 보기 드뭅니다."
116
+ nps_guide_caveat: "기준은 업종에 따라 달라집니다. 소프트웨어는 보통 40 안팎, 항공사와 인터넷 사업자는 0 부근입니다. 하나의 숫자보다 시간에 따른 추이가, 점수보다 코멘트가 더 많은 것을 알려 줍니다."
105
117
  score: "점수"
106
118
  comment: "의견"
@@ -102,5 +102,17 @@ lb:
102
102
  promoters: "Promoteuren (9–10)"
103
103
  passives: "Passiv (7–8)"
104
104
  detractors: "Kritiker (0–6)"
105
+ nps_band_needs_work: "Muss besser ginn"
106
+ nps_band_good: "Gutt"
107
+ nps_band_great: "Ganz gutt"
108
+ nps_band_excellent: "Excellent"
109
+ nps_sample_note: "Baséiert op %{count} Äntwerten — ze wéineg fir doraus eppes ofzeleeden."
110
+ nps_guide: "Wéi gëtt dat gerechent, a wat ass e gudde Score?"
111
+ nps_guide_formula: "NPS = % vun de Promoteuren ofzunn % vun de Kritiker. D'Passiv zielen an der Gesamtzuel mat, awer op kenger vun de Säiten, sou datt se de Score Richtung null zéien. D'Resultat geet vu −100 bis +100 — e Score, kee Prozentsaz."
112
+ nps_guide_needs_work: "Méi Kritiker wéi Promoteuren."
113
+ nps_guide_good: "Do, wou déi meescht Betriber leien."
114
+ nps_guide_great: "Staark Trei."
115
+ nps_guide_excellent: "Weltklass, a seelen."
116
+ nps_guide_caveat: "D'Richtwäerter hänke vum Secteur of — Software läit dacks ronn 40, Fluchgesellschaften an Internetfournisseuren no bei 0. D'Entwécklung iwwer d'Zäit seet méi wéi eng eenzel Zuel, an d'Kommentaren soe méi wéi de Score."
105
117
  score: "Score"
106
118
  comment: "Kommentar"
@@ -102,5 +102,17 @@ nl:
102
102
  promoters: "Promotors (9–10)"
103
103
  passives: "Passieven (7–8)"
104
104
  detractors: "Criticasters (0–6)"
105
+ nps_band_needs_work: "Moet beter"
106
+ nps_band_good: "Goed"
107
+ nps_band_great: "Zeer goed"
108
+ nps_band_excellent: "Uitstekend"
109
+ nps_sample_note: "Gebaseerd op %{count} reacties — te weinig om conclusies aan te verbinden."
110
+ nps_guide: "Hoe wordt dit berekend en wat is een goede score?"
111
+ nps_guide_formula: "NPS = % promotors min % criticasters. Passieven tellen mee in het totaal maar aan geen van beide kanten, waardoor ze de score richting nul trekken. De uitkomst loopt van −100 tot +100 — een score, geen percentage."
112
+ nps_guide_needs_work: "Meer criticasters dan promotors."
113
+ nps_guide_good: "Waar de meeste bedrijven zitten."
114
+ nps_guide_great: "Sterke loyaliteit."
115
+ nps_guide_excellent: "Wereldklasse, en zeldzaam."
116
+ nps_guide_caveat: "Richtwaarden verschillen per branche — software zit vaak rond de 40, luchtvaartmaatschappijen en internetproviders dicht bij 0. De trend over tijd zegt meer dan één getal, en de opmerkingen zeggen meer dan de score."
105
117
  score: "Score"
106
118
  comment: "Opmerking"
@@ -102,5 +102,17 @@ pl:
102
102
  promoters: "Promotorzy (9–10)"
103
103
  passives: "Neutralni (7–8)"
104
104
  detractors: "Krytycy (0–6)"
105
+ nps_band_needs_work: "Wymaga pracy"
106
+ nps_band_good: "Dobrze"
107
+ nps_band_great: "Bardzo dobrze"
108
+ nps_band_excellent: "Znakomicie"
109
+ nps_sample_note: "Na podstawie %{count} odpowiedzi — zbyt mało, by wyciągać wnioski."
110
+ nps_guide: "Jak to jest liczone i jaki wynik jest dobry?"
111
+ nps_guide_formula: "NPS = % promotorów minus % krytyków. Neutralni liczą się do sumy, ale do żadnej ze stron, więc ciągną wynik w stronę zera. Wartość mieści się w przedziale od −100 do +100 — to wskaźnik, nie procent."
112
+ nps_guide_needs_work: "Więcej krytyków niż promotorów."
113
+ nps_guide_good: "Poziom większości firm."
114
+ nps_guide_great: "Silna lojalność."
115
+ nps_guide_excellent: "Poziom światowy, spotykany rzadko."
116
+ nps_guide_caveat: "Punkty odniesienia zależą od branży — oprogramowanie często ma około 40, linie lotnicze i dostawcy internetu blisko 0. Trend w czasie mówi więcej niż pojedyncza liczba, a komentarze więcej niż sam wynik."
105
117
  score: "Wynik"
106
118
  comment: "Komentarz"
@@ -102,5 +102,17 @@ pt:
102
102
  promoters: "Promotores (9–10)"
103
103
  passives: "Neutros (7–8)"
104
104
  detractors: "Detratores (0–6)"
105
+ nps_band_needs_work: "Precisa melhorar"
106
+ nps_band_good: "Bom"
107
+ nps_band_great: "Muito bom"
108
+ nps_band_excellent: "Excelente"
109
+ nps_sample_note: "Com base em %{count} respostas — poucas demais para tirar conclusões."
110
+ nps_guide: "Como isto é calculado e o que é uma boa pontuação?"
111
+ nps_guide_formula: "NPS = % de promotores menos % de detratores. Os neutros contam no total, mas não em nenhum dos lados, por isso puxam a pontuação para zero. O resultado vai de −100 a +100 — é um índice, não uma percentagem."
112
+ nps_guide_needs_work: "Mais detratores do que promotores."
113
+ nps_guide_good: "Onde fica a maioria das empresas."
114
+ nps_guide_great: "Lealdade forte."
115
+ nps_guide_excellent: "Nível mundial, e raro."
116
+ nps_guide_caveat: "As referências variam por setor — software costuma ficar perto de 40, companhias aéreas e provedores de internet perto de 0. A tendência ao longo do tempo diz mais do que um número isolado, e os comentários dizem mais do que a pontuação."
105
117
  score: "Nota"
106
118
  comment: "Comentário"
@@ -102,5 +102,17 @@ ro:
102
102
  promoters: "Promotori (9–10)"
103
103
  passives: "Pasivi (7–8)"
104
104
  detractors: "Detractori (0–6)"
105
+ nps_band_needs_work: "Necesită îmbunătățiri"
106
+ nps_band_good: "Bun"
107
+ nps_band_great: "Foarte bun"
108
+ nps_band_excellent: "Excelent"
109
+ nps_sample_note: "Pe baza a %{count} răspunsuri — prea puține pentru concluzii."
110
+ nps_guide: "Cum se calculează și ce înseamnă un scor bun?"
111
+ nps_guide_formula: "NPS = procentul de promotori minus procentul de detractori. Pasivii intră în total, dar în niciuna dintre părți, așa că trag scorul spre zero. Rezultatul merge de la −100 la +100 — este un scor, nu un procent."
112
+ nps_guide_needs_work: "Mai mulți detractori decât promotori."
113
+ nps_guide_good: "Acolo unde se află majoritatea companiilor."
114
+ nps_guide_great: "Loialitate puternică."
115
+ nps_guide_excellent: "Nivel mondial, și rar."
116
+ nps_guide_caveat: "Reperele diferă de la o industrie la alta — software-ul stă adesea în jur de 40, companiile aeriene și furnizorii de internet aproape de 0. Tendința în timp spune mai mult decât un singur număr, iar comentariile spun mai mult decât scorul."
105
117
  score: "Scor"
106
118
  comment: "Comentariu"
@@ -102,5 +102,17 @@ ru:
102
102
  promoters: "Промоутеры (9–10)"
103
103
  passives: "Нейтралы (7–8)"
104
104
  detractors: "Критики (0–6)"
105
+ nps_band_needs_work: "Требует работы"
106
+ nps_band_good: "Хорошо"
107
+ nps_band_great: "Отлично"
108
+ nps_band_excellent: "Превосходно"
109
+ nps_sample_note: "Основано на %{count} ответах — слишком мало, чтобы делать выводы."
110
+ nps_guide: "Как это считается и какой результат хороший?"
111
+ nps_guide_formula: "NPS = % промоутеров минус % критиков. Нейтралы входят в общее число, но не идут ни в одну из сторон, поэтому тянут результат к нулю. Значение лежит в диапазоне от −100 до +100 — это индекс, а не проценты."
112
+ nps_guide_needs_work: "Критиков больше, чем промоутеров."
113
+ nps_guide_good: "Уровень большинства компаний."
114
+ nps_guide_great: "Высокая лояльность."
115
+ nps_guide_excellent: "Мировой уровень, встречается редко."
116
+ nps_guide_caveat: "Ориентиры зависят от отрасли: у софта это часто около 40, у авиакомпаний и провайдеров — около 0. Динамика во времени говорит больше, чем одно число, а комментарии — больше, чем сам индекс."
105
117
  score: "Оценка"
106
118
  comment: "Комментарий"
@@ -102,5 +102,17 @@ th:
102
102
  promoters: "ผู้สนับสนุน (9–10)"
103
103
  passives: "เป็นกลาง (7–8)"
104
104
  detractors: "ผู้ไม่พอใจ (0–6)"
105
+ nps_band_needs_work: "ต้องปรับปรุง"
106
+ nps_band_good: "ดี"
107
+ nps_band_great: "ดีมาก"
108
+ nps_band_excellent: "ยอดเยี่ยม"
109
+ nps_sample_note: "จากคำตอบ %{count} รายการ — น้อยเกินกว่าจะสรุปอะไรได้"
110
+ nps_guide: "คำนวณอย่างไร และเท่าไรถึงเรียกว่าดี"
111
+ nps_guide_formula: "NPS = สัดส่วนผู้สนับสนุน ลบ สัดส่วนผู้ไม่พอใจ กลุ่มเฉย ๆ นับรวมในยอดทั้งหมดแต่ไม่นับเข้าฝั่งใด จึงดึงคะแนนเข้าใกล้ศูนย์ ผลลัพธ์อยู่ระหว่าง −100 ถึง +100 เป็นคะแนน ไม่ใช่เปอร์เซ็นต์"
112
+ nps_guide_needs_work: "มีผู้ไม่พอใจมากกว่าผู้สนับสนุน"
113
+ nps_guide_good: "ช่วงที่บริษัทส่วนใหญ่อยู่"
114
+ nps_guide_great: "ความภักดีสูง"
115
+ nps_guide_excellent: "ระดับโลก และพบได้ยาก"
116
+ nps_guide_caveat: "เกณฑ์เปรียบเทียบต่างกันไปตามอุตสาหกรรม ซอฟต์แวร์มักอยู่ราว 40 ส่วนสายการบินและผู้ให้บริการอินเทอร์เน็ตใกล้ 0 แนวโน้มตามเวลาบอกอะไรได้มากกว่าตัวเลขเดียว และความคิดเห็นบอกได้มากกว่าคะแนน"
105
117
  score: "คะแนน"
106
118
  comment: "ความคิดเห็น"
@@ -102,5 +102,17 @@ tr:
102
102
  promoters: "Destekleyenler (9–10)"
103
103
  passives: "Pasifler (7–8)"
104
104
  detractors: "Eleştirenler (0–6)"
105
+ nps_band_needs_work: "Geliştirilmeli"
106
+ nps_band_good: "İyi"
107
+ nps_band_great: "Çok iyi"
108
+ nps_band_excellent: "Mükemmel"
109
+ nps_sample_note: "%{count} yanıta dayanıyor — sonuç çıkarmak için fazla az."
110
+ nps_guide: "Bu nasıl hesaplanır ve iyi bir puan nedir?"
111
+ nps_guide_formula: "NPS = tavsiye edenlerin yüzdesi eksi eleştirenlerin yüzdesi. Kararsızlar toplama dahildir ama iki taraftan birine yazılmaz, bu yüzden puanı sıfıra doğru çeker. Sonuç −100 ile +100 arasındadır — yüzde değil, bir puandır."
112
+ nps_guide_needs_work: "Eleştirenler tavsiye edenlerden fazla."
113
+ nps_guide_good: "Çoğu şirketin bulunduğu aralık."
114
+ nps_guide_great: "Güçlü bağlılık."
115
+ nps_guide_excellent: "Dünya standartlarında ve nadir."
116
+ nps_guide_caveat: "Ölçütler sektöre göre değişir — yazılımda genelde 40 civarı, havayollarında ve internet sağlayıcılarında 0'a yakındır. Zaman içindeki eğilim tek bir sayıdan, yorumlar ise puandan daha çok şey anlatır."
105
117
  score: "Puan"
106
118
  comment: "Yorum"
@@ -102,5 +102,17 @@ uk:
102
102
  promoters: "Промоутери (9–10)"
103
103
  passives: "Нейтральні (7–8)"
104
104
  detractors: "Критики (0–6)"
105
+ nps_band_needs_work: "Потребує роботи"
106
+ nps_band_good: "Добре"
107
+ nps_band_great: "Чудово"
108
+ nps_band_excellent: "Відмінно"
109
+ nps_sample_note: "На основі %{count} відповідей — замало, щоб робити висновки."
110
+ nps_guide: "Як це обчислюється і який результат вважається добрим?"
111
+ nps_guide_formula: "NPS = % промоутерів мінус % критиків. Нейтральні входять до загальної кількості, але не до жодної зі сторін, тому тягнуть результат до нуля. Значення — від −100 до +100 — це індекс, а не відсотки."
112
+ nps_guide_needs_work: "Критиків більше, ніж промоутерів."
113
+ nps_guide_good: "Рівень більшості компаній."
114
+ nps_guide_great: "Висока лояльність."
115
+ nps_guide_excellent: "Світовий рівень, трапляється рідко."
116
+ nps_guide_caveat: "Орієнтири залежать від галузі: у програмного забезпечення це часто близько 40, у авіакомпаній та провайдерів — близько 0. Динаміка в часі говорить більше, ніж одне число, а коментарі — більше, ніж сам індекс."
105
117
  score: "Оцінка"
106
118
  comment: "Коментар"
@@ -102,5 +102,17 @@ ur:
102
102
  promoters: "حامی (9–10)"
103
103
  passives: "غیر جانبدار (7–8)"
104
104
  detractors: "ناقدین (0–6)"
105
+ nps_band_needs_work: "بہتری درکار"
106
+ nps_band_good: "اچھا"
107
+ nps_band_great: "بہت اچھا"
108
+ nps_band_excellent: "شاندار"
109
+ nps_sample_note: "%{count} جوابات کی بنیاد پر — نتیجہ نکالنے کے لیے بہت کم۔"
110
+ nps_guide: "یہ کیسے نکلتا ہے، اور اچھا اسکور کیا ہے؟"
111
+ nps_guide_formula: "NPS = پروموٹرز کا فیصد منفی ناقدین کا فیصد۔ غیر جانبدار کل تعداد میں شمار ہوتے ہیں مگر کسی طرف نہیں، اس لیے وہ اسکور کو صفر کی طرف کھینچتے ہیں۔ نتیجہ −100 سے +100 تک ہوتا ہے — یہ اسکور ہے، فیصد نہیں۔"
112
+ nps_guide_needs_work: "پروموٹرز سے زیادہ ناقدین ہیں۔"
113
+ nps_guide_good: "زیادہ تر کمپنیاں یہیں ہوتی ہیں۔"
114
+ nps_guide_great: "مضبوط وفاداری۔"
115
+ nps_guide_excellent: "عالمی معیار، اور کم یاب۔"
116
+ nps_guide_caveat: "معیار صنعت کے ساتھ بدلتے ہیں — سافٹ ویئر اکثر 40 کے آس پاس، ایئر لائنز اور انٹرنیٹ فراہم کنندگان 0 کے قریب۔ کسی ایک عدد سے زیادہ وقت کے ساتھ رجحان بتاتا ہے، اور اسکور سے زیادہ تبصرے۔"
105
117
  score: "اسکور"
106
118
  comment: "تبصرہ"
@@ -102,5 +102,17 @@ vi:
102
102
  promoters: "Người ủng hộ (9–10)"
103
103
  passives: "Trung lập (7–8)"
104
104
  detractors: "Người phản đối (0–6)"
105
+ nps_band_needs_work: "Cần cải thiện"
106
+ nps_band_good: "Tốt"
107
+ nps_band_great: "Rất tốt"
108
+ nps_band_excellent: "Xuất sắc"
109
+ nps_sample_note: "Dựa trên %{count} phản hồi — quá ít để rút ra kết luận."
110
+ nps_guide: "Chỉ số này tính thế nào, và bao nhiêu là tốt?"
111
+ nps_guide_formula: "NPS = % người ủng hộ trừ % người phản đối. Nhóm trung lập được tính vào tổng nhưng không thuộc bên nào, nên kéo điểm về gần 0. Kết quả nằm trong khoảng −100 đến +100 — là điểm số, không phải phần trăm."
112
+ nps_guide_needs_work: "Người phản đối nhiều hơn người ủng hộ."
113
+ nps_guide_good: "Mức mà phần lớn công ty đạt được."
114
+ nps_guide_great: "Mức độ trung thành cao."
115
+ nps_guide_excellent: "Đẳng cấp thế giới, và hiếm gặp."
116
+ nps_guide_caveat: "Mốc tham chiếu thay đổi theo ngành — phần mềm thường quanh 40, hàng không và nhà mạng gần 0. Xu hướng theo thời gian nói lên nhiều hơn một con số, và các bình luận nói lên nhiều hơn điểm số."
105
117
  score: "Điểm"
106
118
  comment: "Bình luận"
@@ -102,5 +102,17 @@ zh-CN:
102
102
  promoters: "推荐者(9–10)"
103
103
  passives: "中立者(7–8)"
104
104
  detractors: "贬损者(0–6)"
105
+ nps_band_needs_work: "有待改进"
106
+ nps_band_good: "良好"
107
+ nps_band_great: "优秀"
108
+ nps_band_excellent: "卓越"
109
+ nps_sample_note: "基于 %{count} 条回复,样本太少,参考价值有限。"
110
+ nps_guide: "这个分数怎么算,多少算好?"
111
+ nps_guide_formula: "NPS = 推荐者占比 − 贬损者占比。被动者计入总数,但不计入任何一方,因此会把分数拉向零。结果范围是 −100 到 +100,是分数而不是百分比。"
112
+ nps_guide_needs_work: "贬损者多于推荐者。"
113
+ nps_guide_good: "多数公司所处的区间。"
114
+ nps_guide_great: "客户忠诚度很高。"
115
+ nps_guide_excellent: "世界一流水平,相当少见。"
116
+ nps_guide_caveat: "基准随行业而变:软件行业常在 40 左右,航空公司和网络运营商接近 0。趋势比单个数字更有意义,评论又比分数更有意义。"
105
117
  score: "分数"
106
118
  comment: "评论"
@@ -12,8 +12,15 @@ module Testimonials
12
12
 
13
13
  desc 'Installs testimonials: config initializer, migration, and engine mount.'
14
14
 
15
+ # NPS is the one part of the gem an app can genuinely not want. Skipping
16
+ # it leaves out the table and writes `config.nps = false`, so nothing
17
+ # ever reaches a table that isn't there — no runtime introspection, no
18
+ # boot-time database call. `testimonials:nps` adds it later.
19
+ class_option :skip_nps, type: :boolean, default: false,
20
+ desc: 'Leave out the NPS table and turn the NPS flow off'
21
+
15
22
  def create_initializer
16
- copy_file 'initializer.rb', 'config/initializers/testimonials.rb'
23
+ template 'initializer.rb.tt', 'config/initializers/testimonials.rb'
17
24
  end
18
25
 
19
26
  def create_migration_file
@@ -29,8 +36,12 @@ module Testimonials
29
36
  say "\ntestimonials installed. Run `rails db:migrate`, then add", :green
30
37
  say '`<%= testimonials_tag %>` before </body> in your layout.'
31
38
  say 'Triage testimonials at /testimonials (development only until you set config.authorize_admin).'
32
- say 'Optional: run `bin/rails testimonials:seed_demo` for sample testimonials and NPS.'
39
+ samples = options[:skip_nps] ? 'testimonials' : 'testimonials and NPS'
40
+ say "Optional: run `bin/rails testimonials:seed_demo` for sample #{samples}."
33
41
  say "Collect from outside the app via /testimonials/new.\n"
42
+ return unless options[:skip_nps]
43
+
44
+ say 'Installed without NPS. Add it later with `bin/rails generate testimonials:nps`.', :yellow
34
45
  end
35
46
 
36
47
  private
@@ -27,6 +27,7 @@ class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
27
27
  add_index :testimonials_testimonials, %i[tenant status]
28
28
  add_index :testimonials_testimonials, %i[tenant status consent_given]
29
29
 
30
+ <% unless options[:skip_nps] -%>
30
31
  create_table :testimonials_nps_responses do |t|
31
32
  t.integer :score, null: false
32
33
  t.text :comment
@@ -42,6 +43,7 @@ class CreateTestimonialsTables < ActiveRecord::Migration<%= migration_version %>
42
43
  end
43
44
  add_index :testimonials_nps_responses, %i[tenant score]
44
45
 
46
+ <% end -%>
45
47
  create_table :testimonials_prompt_events do |t|
46
48
  t.string :kind, null: false
47
49
  t.string :action, null: false
@@ -84,7 +84,15 @@ Testimonials.configure do |config|
84
84
  # config.public_api = false
85
85
 
86
86
  # NPS surveys. Promoters (9–10) are offered the testimonial form right away.
87
+ <% if options[:skip_nps] -%>
88
+ # Installed with --skip-nps, so there is no testimonials_nps_responses table
89
+ # and the NPS flow, dashboard tab and public page stay off. To add it later:
90
+ # `bin/rails generate testimonials:nps && bin/rails db:migrate`, then flip
91
+ # this to true.
92
+ config.nps = false
93
+ <% else -%>
87
94
  # config.nps = true
95
+ <% end -%>
88
96
  # config.nps_reprompt_after = 90.days
89
97
 
90
98
  # Called with each saved Testimonial or NpsResponse — notify Slack, email…
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails/generators'
4
+ require 'rails/generators/active_record'
5
+
6
+ module Testimonials
7
+ module Generators
8
+ # For apps that installed with --skip-nps and later want the 0–10 survey:
9
+ # creates the testimonials_nps_responses table. A full install already has
10
+ # it. Additive and safe — nothing reads the table until config.nps is true.
11
+ #
12
+ # bin/rails generate testimonials:nps && bin/rails db:migrate
13
+ class NpsGenerator < Rails::Generators::Base
14
+ include ActiveRecord::Generators::Migration
15
+
16
+ source_root File.expand_path('templates', __dir__)
17
+
18
+ desc 'Adds the NPS responses table for an install that skipped it.'
19
+
20
+ def create_migration_file
21
+ migration_template 'create_testimonials_nps_responses.rb.tt',
22
+ 'db/migrate/create_testimonials_nps_responses.rb'
23
+ end
24
+
25
+ def post_install
26
+ say "\nNPS table queued. Run `rails db:migrate`, then set", :green
27
+ say 'config.nps = true in config/initializers/testimonials.rb to turn the flow on.'
28
+ end
29
+
30
+ private
31
+
32
+ def migration_version
33
+ "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateTestimonialsNpsResponses < ActiveRecord::Migration<%= migration_version %>
4
+ def change
5
+ create_table :testimonials_nps_responses do |t|
6
+ t.integer :score, null: false
7
+ t.text :comment
8
+ t.string :author_id
9
+ t.string :name
10
+ t.string :email
11
+ t.string :page_url
12
+ t.string :user_agent
13
+ t.string :locale
14
+ t.string :tenant # opaque per-tenant key; nil = single global collection
15
+
16
+ t.timestamps
17
+ end
18
+ add_index :testimonials_nps_responses, %i[tenant score]
19
+ end
20
+ end
@@ -3,13 +3,17 @@
3
3
  class AddTenantToTestimonials < ActiveRecord::Migration<%= migration_version %>
4
4
  def change
5
5
  add_column :testimonials_testimonials, :tenant, :string
6
- add_column :testimonials_nps_responses, :tenant, :string
7
6
  add_column :testimonials_prompt_events, :tenant, :string
8
7
 
9
8
  add_index :testimonials_testimonials, %i[tenant status]
10
9
  add_index :testimonials_testimonials, %i[tenant status consent_given]
11
- add_index :testimonials_nps_responses, %i[tenant score]
12
10
  add_index :testimonials_prompt_events, %i[tenant author_id kind]
13
11
  add_index :testimonials_prompt_events, %i[tenant visitor_token kind]
12
+
13
+ # An install run with --skip-nps has no NPS table to add the column to.
14
+ return unless table_exists?(:testimonials_nps_responses)
15
+
16
+ add_column :testimonials_nps_responses, :tenant, :string
17
+ add_index :testimonials_nps_responses, %i[tenant score]
14
18
  end
15
19
  end
@@ -5,6 +5,8 @@
5
5
  --pending: #d97706; --approved: #16a34a; --archived: #6b7280;
6
6
  --star: #f59e0b; --danger: #dc2626;
7
7
  --promoter: #16a34a; --passive: #d97706; --detractor: #dc2626;
8
+ /* The four bands an NPS number falls into: bad, ordinary, strong, rare. */
9
+ --nps-needs-work: #dc2626; --nps-good: #d97706; --nps-great: #16a34a; --nps-excellent: #0f766e;
8
10
  }
9
11
  @media (prefers-color-scheme: dark) {
10
12
  :root {
@@ -86,6 +88,51 @@ img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
86
88
  border-radius: 12px; padding: 14px 16px; }
87
89
  .stat b { display: block; font-size: 26px; }
88
90
  .stat span { color: var(--muted); font-size: 13px; }
91
+
92
+ /* NPS score card: the number, the band it falls in, and where it sits on the
93
+ −100..100 scale — so nobody has to google whether 50 is any good. */
94
+ .stat-nps { flex: 2.4; min-width: 240px; }
95
+ .stat-headline { display: flex; align-items: center; gap: 10px; }
96
+ .stat-headline b { display: inline-block; }
97
+ /* `.stat span` would otherwise mute the badge's own white text. */
98
+ .stat .badge { color: #fff; font-size: 12px; }
99
+ .badge.nps-band-needs-work { background: var(--nps-needs-work); }
100
+ .badge.nps-band-good { background: var(--nps-good); }
101
+ .badge.nps-band-great { background: var(--nps-great); }
102
+ .badge.nps-band-excellent { background: var(--nps-excellent); }
103
+ .nps-meter { direction: ltr; margin-top: 10px; }
104
+ .nps-meter-track { position: relative; display: flex; height: 6px; border-radius: 999px; overflow: hidden; }
105
+ .nps-seg { display: block; height: 100%; opacity: .45; }
106
+ .nps-seg.nps-band-needs-work { flex: 100; background: var(--nps-needs-work); }
107
+ .nps-seg.nps-band-good { flex: 30; background: var(--nps-good); }
108
+ .nps-seg.nps-band-great { flex: 40; background: var(--nps-great); }
109
+ .nps-seg.nps-band-excellent { flex: 30; background: var(--nps-excellent); }
110
+ /* Hidden until dashboard.js reads data-nps-marker and positions it, so a
111
+ score of 50 never renders as if it were −100. */
112
+ .nps-marker { display: none; position: absolute; top: -3px; width: 4px; height: 12px; margin-left: -2px;
113
+ border-radius: 2px; background: var(--text); box-shadow: 0 0 0 2px var(--surface); }
114
+ .nps-marker.placed { display: block; }
115
+ .nps-meter-ticks { position: relative; height: 14px; margin-top: 3px; color: var(--muted); font-size: 11px; }
116
+ .nps-meter-ticks span { position: absolute; transform: translateX(-50%); }
117
+ .nps-meter-ticks .tick-min { left: 0; transform: none; }
118
+ .nps-meter-ticks .tick-zero { left: 50%; }
119
+ .nps-meter-ticks .tick-good { left: 65%; }
120
+ .nps-meter-ticks .tick-great { left: 85%; }
121
+ .nps-meter-ticks .tick-max { left: 100%; transform: translateX(-100%); }
122
+ .nps-sample { margin: 8px 0 0; font-size: 12px; }
123
+
124
+ /* The formula and the benchmarks, tucked behind a disclosure. */
125
+ .nps-guide { flex: 0 0 auto; margin-bottom: 14px; background: var(--surface);
126
+ border: 1px solid var(--border); border-radius: 12px; }
127
+ .nps-guide summary { padding: 10px 16px; color: var(--muted); font-size: 13px; cursor: pointer; }
128
+ .nps-guide summary:hover { color: var(--text); }
129
+ .nps-guide[open] summary { border-bottom: 1px solid var(--border); }
130
+ .nps-guide-body { padding: 12px 16px 14px; font-size: 13px; }
131
+ .nps-guide-body p { margin: 0; max-width: 78ch; }
132
+ .nps-bands { display: grid; grid-template-columns: max-content max-content 1fr; gap: 8px 14px;
133
+ margin: 12px 0; padding: 0; list-style: none; align-items: baseline; }
134
+ .nps-bands li { display: contents; }
135
+ .nps-bands b { font-variant-numeric: tabular-nums; white-space: nowrap; }
89
136
  .best-line-form textarea { width: 100%; padding: 8px; border: 1px solid var(--border); border-radius: 8px;
90
137
  background: var(--surface); color: var(--text); font: inherit; resize: vertical; }
91
138
  .best-line-hint { margin: 2px 0 6px; }
@@ -183,5 +230,9 @@ img.avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; }
183
230
  .stat-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
184
231
  .stat { min-width: 0; padding: 10px 12px; }
185
232
  .stat b { font-size: 22px; }
233
+ .stat-nps { grid-column: 1 / -1; }
234
+ .nps-bands { grid-template-columns: max-content 1fr; }
235
+ .nps-bands b { grid-column: 2; }
236
+ .nps-bands li > .muted { grid-column: 1 / -1; }
186
237
  .share-link { flex: 1 0 100%; margin-left: 0; text-align: center; }
187
238
  }
@@ -21,6 +21,22 @@
21
21
  }
22
22
  }, true);
23
23
 
24
+ // Places the NPS score marker on the −100..100 scale. The position is a data
25
+ // attribute rather than an inline style, which a strict style-src refuses.
26
+ function placeNpsMarkers() {
27
+ var markers = document.querySelectorAll("[data-nps-marker]");
28
+ for (var i = 0; i < markers.length; i++) {
29
+ var score = parseFloat(markers[i].getAttribute("data-nps-marker"));
30
+ if (isNaN(score)) continue;
31
+ markers[i].style.left = ((score + 100) / 2) + "%";
32
+ markers[i].classList.add("placed");
33
+ }
34
+ }
35
+
36
+ document.addEventListener("DOMContentLoaded", placeNpsMarkers);
37
+ document.addEventListener("turbo:load", placeNpsMarkers);
38
+ placeNpsMarkers();
39
+
24
40
  document.addEventListener("change", function (event) {
25
41
  var control = event.target;
26
42
  if (!control || !control.dataset || !control.dataset.autosubmit || !control.form) return;
@@ -90,7 +90,8 @@ module Testimonials
90
90
  def self.load!(tenant: nil)
91
91
  {
92
92
  testimonials: load_testimonials!(tenant: tenant),
93
- nps_responses: load_nps_responses!(tenant: tenant),
93
+ # An install run with --skip-nps has no table to seed into.
94
+ nps_responses: (Testimonials.config.nps ? load_nps_responses!(tenant: tenant) : []),
94
95
  prompt_events: load_prompt_events!(tenant: tenant)
95
96
  }
96
97
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Testimonials
4
- VERSION = '0.7.7'
4
+ VERSION = '0.7.9'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: testimonials
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.7
4
+ version: 0.7.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Shmarov
@@ -97,7 +97,9 @@ files:
97
97
  - config/routes.rb
98
98
  - lib/generators/testimonials/install/install_generator.rb
99
99
  - lib/generators/testimonials/install/templates/create_testimonials_tables.rb.tt
100
- - lib/generators/testimonials/install/templates/initializer.rb
100
+ - lib/generators/testimonials/install/templates/initializer.rb.tt
101
+ - lib/generators/testimonials/nps/nps_generator.rb
102
+ - lib/generators/testimonials/nps/templates/create_testimonials_nps_responses.rb.tt
101
103
  - lib/generators/testimonials/tenant/templates/add_tenant_to_testimonials.rb.tt
102
104
  - lib/generators/testimonials/tenant/tenant_generator.rb
103
105
  - lib/tasks/testimonials_tasks.rake