rubocop-dev_doc 0.5.0.beta1 → 0.5.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.
@@ -1,116 +0,0 @@
1
- module RuboCop
2
- module Cop
3
- module DevDoc
4
- module I18n
5
- # Flag hardcoded user-facing strings in glib JSON-UI component props.
6
- #
7
- # ## Rationale
8
- # Glib components render text from props like `text:`, `label:`, and
9
- # `title:`. Passing a string literal ships untranslatable copy — it can
10
- # never be localized and bypasses the I18n catalog. Pass `t('...')` (or
11
- # any non-literal) so the text resolves through the locale files.
12
- #
13
- # Only the hash form is checked (`view.p text: '...'`). Empty/whitespace
14
- # strings are ignored — they carry no user-facing text. An interpolated
15
- # string (`"Hi #{name}"`) is flagged because its literal portions are
16
- # still hardcoded copy — unless it interpolates a `t(...)` (or
17
- # `translate`/`I18n.t`) call, which is treated as positive localization
18
- # and left alone.
19
- #
20
- # The watched method names and localizable keys are configurable via
21
- # `WatchedMethods:` and `LocalizableKeys:`.
22
- #
23
- # ❌ Hardcoded — can't be translated
24
- # view.p text: 'Welcome'
25
- # view.fields_text label: 'Email'
26
- #
27
- # ✔️ Resolved through I18n
28
- # view.p text: t('home.welcome')
29
- # view.fields_text label: t('user.email')
30
- # view.p text: "#{t('home.welcome')}, #{user.name}"
31
- #
32
- # @example
33
- # # bad
34
- # view.p text: 'Welcome'
35
- #
36
- # # bad
37
- # view.fields_text label: 'Email'
38
- #
39
- # # bad (interpolation, but no translation call)
40
- # view.p text: "Hi #{name}"
41
- #
42
- # # good
43
- # view.p text: t('home.welcome')
44
- #
45
- # # good (interpolates a translation call)
46
- # view.p text: "#{t('home.greeting')} #{user.name}"
47
- #
48
- # # good (non-literal — not flagged)
49
- # view.p text: user.name
50
- class RequireTranslation < Base
51
- MSG = 'Localize this string: pass `t(...)` instead of a hardcoded ' \
52
- 'string for `%<key>s:`.'.freeze
53
-
54
- DEFAULT_WATCHED_METHODS = %w[
55
- h1 h2 h3 h4 h5 p label markdown
56
- fields_text fields_number fields_select fields_password
57
- fields_textarea fields_check fields_checkGroup fields_chipGroup
58
- fields_timeZone fields_radioGroup fields_date fields_datetime
59
- ].freeze
60
-
61
- DEFAULT_LOCALIZABLE_KEYS = %w[
62
- title subtitle subsubtitle label placeholder text
63
- ].freeze
64
-
65
- TRANSLATION_METHODS = %i[t translate].freeze
66
-
67
- def on_send(node)
68
- return unless watched_methods.include?(node.method_name.to_s)
69
-
70
- node.arguments.each do |arg|
71
- next unless arg.hash_type?
72
-
73
- arg.pairs.each { |pair| check_pair(pair) }
74
- end
75
- end
76
- alias on_csend on_send
77
-
78
- private
79
-
80
- def check_pair(pair)
81
- key = pair.key
82
- return unless key.sym_type?
83
- return unless localizable_keys.include?(key.value.to_s)
84
- return unless hardcoded_string?(pair.value)
85
-
86
- add_offense(pair.value, message: format(MSG, key: key.value))
87
- end
88
-
89
- # A plain string literal is hardcoded copy unless it's blank. A `dstr`
90
- # (interpolated string) is hardcoded copy too — unless it interpolates
91
- # a translation call, which counts as positive localization.
92
- def hardcoded_string?(node)
93
- return !localized_interpolation?(node) if node.dstr_type?
94
- return false unless node.str_type?
95
-
96
- !node.value.strip.empty?
97
- end
98
-
99
- def localized_interpolation?(node)
100
- node.each_descendant(:send, :csend).any? do |call|
101
- TRANSLATION_METHODS.include?(call.method_name)
102
- end
103
- end
104
-
105
- def watched_methods
106
- cop_config.fetch('WatchedMethods', DEFAULT_WATCHED_METHODS)
107
- end
108
-
109
- def localizable_keys
110
- cop_config.fetch('LocalizableKeys', DEFAULT_LOCALIZABLE_KEYS)
111
- end
112
- end
113
- end
114
- end
115
- end
116
- end
@@ -1,89 +0,0 @@
1
- module RuboCop
2
- module Cop
3
- module DevDoc
4
- module I18n
5
- # Require translation keys to start with an allowed namespace prefix.
6
- #
7
- # ## Rationale
8
- # A flat translation catalog drifts into collisions and dead keys.
9
- # Requiring every `t(...)` key to start with an agreed namespace (e.g.
10
- # `hotel.`, `general.`) keeps the locale files organized and makes it
11
- # obvious which feature owns a key.
12
- #
13
- # The allowed prefixes are project-specific, so `AllowedPrefixes:`
14
- # defaults to empty and the cop does nothing until a project configures
15
- # it. Matches `t`, `translate`, and `I18n.t` (the receiver is ignored).
16
- #
17
- # Only statically-literal keys are checked. A key built from
18
- # interpolation (`t("#{prefix}.foo")`) or a variable (`t(key)`) can't be
19
- # verified and is skipped. A key that begins with a literal segment
20
- # (`t("hotel.#{id}")`) is checked against that leading segment.
21
- #
22
- # ❌ No recognized prefix
23
- # t('welcome.title')
24
- #
25
- # ✔️ Namespaced
26
- # t('general.welcome.title')
27
- # t('hotel.rooms.heading')
28
- #
29
- # @example AllowedPrefixes: ['hotel.', 'general.']
30
- # # bad
31
- # t('welcome.title')
32
- #
33
- # # bad (lazy key has no namespace)
34
- # t('.title')
35
- #
36
- # # good
37
- # t('hotel.rooms.heading')
38
- #
39
- # # good (variable suffix, literal prefix is checked)
40
- # t("general.#{key}")
41
- #
42
- # # not checked (fully dynamic key)
43
- # t("#{prefix}.title")
44
- class TranslationKeyPrefix < Base
45
- MSG = 'Translation key `%<key>s` must start with an allowed ' \
46
- 'prefix: %<prefixes>s.'.freeze
47
-
48
- RESTRICT_ON_SEND = %i[t translate].freeze
49
-
50
- def on_send(node)
51
- prefixes = allowed_prefixes
52
- return if prefixes.empty?
53
-
54
- key_node = node.first_argument
55
- return unless key_node
56
-
57
- leading = leading_literal(key_node)
58
- return if leading.nil?
59
- return if prefixes.any? { |prefix| leading.start_with?(prefix) }
60
-
61
- add_offense(key_node, message: format(MSG, key: key_node.source, prefixes: prefixes_display(prefixes)))
62
- end
63
-
64
- private
65
-
66
- # The statically-known leading portion of the key, or nil when the key
67
- # is dynamic (a variable, or a string that begins with interpolation).
68
- def leading_literal(node)
69
- case node.type
70
- when :str then node.value
71
- when :sym then node.value.to_s
72
- when :dstr
73
- first = node.children.first
74
- first&.str_type? ? first.value : nil
75
- end
76
- end
77
-
78
- def allowed_prefixes
79
- Array(cop_config['AllowedPrefixes'])
80
- end
81
-
82
- def prefixes_display(prefixes)
83
- prefixes.map { |prefix| "`#{prefix}`" }.join(', ')
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -1,106 +0,0 @@
1
- module RuboCop
2
- module Cop
3
- module DevDoc
4
- module I18n
5
- # Warn when a glib text prop is set from a non-literal value that isn't
6
- # a `t(...)` call — it may be an unlocalized string.
7
- #
8
- # ## Rationale
9
- # `DevDoc/I18n/RequireTranslation` only catches string literals written
10
- # at the call site. A value passed through a variable or method
11
- # (`label: label_text`) escapes it — the literal could be one hop away.
12
- # This cop flags those values so a human can confirm they resolve
13
- # through I18n.
14
- #
15
- # It can't tell a stashed literal from genuinely dynamic data
16
- # (`user.name`) or a translation reached via a variable, so it's a
17
- # review aid, not a clean lint: it's **disabled by default** and runs at
18
- # `info` severity. Run it during a localization pass, not on every
19
- # commit. Values that are obviously translated (`t(...)`, `translate`,
20
- # `I18n.t`) are excluded; literals are left to `RequireTranslation`.
21
- #
22
- # ⚠️ Can't be verified — confirm it's localized
23
- # view.fields_text label: label_text
24
- # view.p text: user.name
25
- #
26
- # ✔️ Obviously localized — not flagged
27
- # view.p text: t('home.welcome')
28
- #
29
- # @example
30
- # # warning (non-literal — may be unlocalized)
31
- # view.fields_text label: label_text
32
- #
33
- # # warning (attribute — may be unlocalized)
34
- # view.p text: user.name
35
- #
36
- # # good (obviously localized — not flagged)
37
- # view.p text: t('home.welcome')
38
- #
39
- # # ignored (literal — handled by RequireTranslation)
40
- # view.p text: 'Welcome'
41
- class UnverifiedTranslation < Base
42
- MSG = 'Possibly unlocalized: `%<key>s:` is set from a non-literal. ' \
43
- 'Use `t(...)` if this is user-facing text.'.freeze
44
-
45
- DYNAMIC_TYPES = %i[lvar ivar send csend].freeze
46
- TRANSLATION_METHODS = %i[t translate].freeze
47
-
48
- DEFAULT_WATCHED_METHODS = %w[
49
- h1 h2 h3 h4 h5 p label markdown
50
- fields_text fields_number fields_select fields_password
51
- fields_textarea fields_check fields_checkGroup fields_chipGroup
52
- fields_timeZone fields_radioGroup fields_date fields_datetime
53
- ].freeze
54
-
55
- DEFAULT_LOCALIZABLE_KEYS = %w[
56
- title subtitle subsubtitle label placeholder text
57
- ].freeze
58
-
59
- def on_send(node)
60
- return unless watched_methods.include?(node.method_name.to_s)
61
-
62
- node.arguments.each do |arg|
63
- next unless arg.hash_type?
64
-
65
- arg.pairs.each { |pair| check_pair(pair) }
66
- end
67
- end
68
- alias on_csend on_send
69
-
70
- private
71
-
72
- def check_pair(pair)
73
- key = pair.key
74
- return unless key.sym_type?
75
- return unless localizable_keys.include?(key.value.to_s)
76
- return unless unverified?(pair.value)
77
-
78
- add_offense(pair.value, message: format(MSG, key: key.value))
79
- end
80
-
81
- # A value we can neither confirm nor deny is localized: a variable or
82
- # method result (but not an obvious `t(...)`/`translate` call). String
83
- # and other literals are excluded — those are RequireTranslation's job.
84
- def unverified?(node)
85
- return false unless DYNAMIC_TYPES.include?(node.type)
86
-
87
- !translation_call?(node)
88
- end
89
-
90
- def translation_call?(node)
91
- (node.send_type? || node.csend_type?) &&
92
- TRANSLATION_METHODS.include?(node.method_name)
93
- end
94
-
95
- def watched_methods
96
- cop_config.fetch('WatchedMethods', DEFAULT_WATCHED_METHODS)
97
- end
98
-
99
- def localizable_keys
100
- cop_config.fetch('LocalizableKeys', DEFAULT_LOCALIZABLE_KEYS)
101
- end
102
- end
103
- end
104
- end
105
- end
106
- end