├── .github └── workflows │ └── main.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin ├── console ├── release.sh └── setup ├── lib ├── translate_enum.rb └── translate_enum │ ├── active_record.rb │ ├── builder.rb │ └── version.rb ├── spec ├── spec_helper.rb └── translate_enum │ ├── action_view_spec.rb │ ├── active_record_spec.rb │ └── translate_enum_spec.rb └── translate_enum.gemspec /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | rspec: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | ruby-version: [3.0, 2.7, 2.6] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Set up Ruby ${{ matrix.ruby-version }} 17 | uses: ruby/setup-ruby@v1 18 | with: 19 | ruby-version: ${{ matrix.ruby-version }} 20 | 21 | - name: Cache gems 22 | uses: actions/cache@v1 23 | with: 24 | path: vendor/bundle 25 | key: ${{ runner.os }}-${{ matrix.ruby-version }}-bundler-${{ hashFiles('**/Gemfile.lock') }} 26 | restore-keys: | 27 | ${{ runner.os }}-${{ matrix.ruby-version }}-bundler- 28 | 29 | - name: Install gems 30 | run: | 31 | bundle config path vendor/bundle 32 | bundle install --jobs 4 --retry 3 33 | 34 | - name: Run tests 35 | run: bundle exec rspec 36 | 37 | rubocop: 38 | runs-on: ubuntu-latest 39 | 40 | steps: 41 | - uses: actions/checkout@v2 42 | 43 | - name: Set up Ruby 44 | uses: ruby/setup-ruby@v1 45 | with: 46 | ruby-version: 3.0 47 | 48 | - name: Cache gems 49 | uses: actions/cache@v1 50 | with: 51 | path: vendor/bundle 52 | key: ${{ runner.os }}-bundler-${{ hashFiles('**/Gemfile.lock') }} 53 | restore-keys: | 54 | ${{ runner.os }}-bundler- 55 | 56 | - name: Install gems 57 | run: | 58 | bundle config path vendor/bundle 59 | bundle install --jobs 4 --retry 3 60 | 61 | - name: Run Rubocop 62 | run: bundle exec rubocop 63 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | /*.gem 11 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | require: 2 | - rubocop-performance 3 | 4 | AllCops: 5 | AutoCorrect: false 6 | SuggestExtensions: false 7 | Exclude: 8 | - 'click_house.gemspec' 9 | - 'bin/*' 10 | - 'spec/**/*' 11 | - 'vendor/**/*' 12 | TargetRubyVersion: 2.6 13 | Bundler/OrderedGems: 14 | Enabled: false 15 | 16 | # ============================== Gemspec ====================== 17 | Gemspec/DeprecatedAttributeAssignment: 18 | Enabled: true 19 | Gemspec/RequireMFA: # new in 1.23 20 | Enabled: true 21 | 22 | # =============================== Performance ======================= 23 | Performance/AncestorsInclude: 24 | Enabled: true 25 | Performance/BigDecimalWithNumericArgument: 26 | Enabled: true 27 | Performance/RedundantSortBlock: 28 | Enabled: true 29 | Performance/RedundantStringChars: 30 | Enabled: true 31 | Performance/ReverseFirst: 32 | Enabled: true 33 | Performance/SortReverse: 34 | Enabled: true 35 | Performance/Squeeze: 36 | Enabled: true 37 | Performance/StringInclude: 38 | Enabled: true 39 | Performance/Sum: 40 | Enabled: true 41 | Performance/ArraySemiInfiniteRangeSlice: 42 | Enabled: true 43 | Performance/BlockGivenWithExplicitBlock: 44 | Enabled: true 45 | Performance/CollectionLiteralInLoop: 46 | Enabled: true 47 | Performance/ConstantRegexp: 48 | Enabled: true 49 | Performance/MethodObjectAsBlock: 50 | Enabled: false 51 | Performance/RedundantEqualityComparisonBlock: 52 | Enabled: true 53 | Performance/RedundantSplitRegexpArgument: 54 | Enabled: true 55 | Performance/MapCompact: 56 | Enabled: true 57 | Performance/ConcurrentMonotonicTime: # new in 1.12 58 | Enabled: true 59 | Performance/StringIdentifierArgument: # new in 1.13 60 | Enabled: true 61 | 62 | # ============================== Metrics ============================ 63 | Metrics/ClassLength: 64 | Max: 180 65 | Metrics/BlockLength: 66 | Enabled: true 67 | Metrics/MethodLength: 68 | Max: 25 69 | Metrics/AbcSize: 70 | Max: 40 71 | 72 | # ============================== Naming ============================= 73 | Naming/PredicateName: 74 | ForbiddenPrefixes: 75 | - is_ 76 | Naming/FileName: 77 | Enabled: true 78 | Exclude: 79 | - 'Gemfile' 80 | Naming/MethodParameterName: 81 | Enabled: false 82 | Naming/AccessorMethodName: 83 | Enabled: false 84 | Naming/InclusiveLanguage: 85 | Enabled: true 86 | Naming/BlockForwarding: # new in 1.24 87 | Enabled: true 88 | 89 | # ============================== Layout ============================= 90 | Layout/LineLength: 91 | Max: 140 92 | Layout/HashAlignment: 93 | EnforcedHashRocketStyle: key 94 | EnforcedColonStyle: key 95 | Layout/ParameterAlignment: 96 | EnforcedStyle: with_fixed_indentation 97 | Layout/CaseIndentation: 98 | EnforcedStyle: case 99 | IndentOneStep: false 100 | Layout/MultilineMethodCallIndentation: 101 | Enabled: true 102 | EnforcedStyle: indented 103 | Layout/SpaceBeforeBlockBraces: 104 | EnforcedStyle: space 105 | EnforcedStyleForEmptyBraces: space 106 | Layout/EmptyLines: 107 | Enabled: true 108 | Layout/EmptyLineAfterMagicComment: 109 | Enabled: false 110 | Layout/EmptyLinesAroundBlockBody: 111 | Enabled: true 112 | Layout/EndAlignment: 113 | EnforcedStyleAlignWith: variable 114 | Layout/FirstHashElementIndentation: 115 | EnforcedStyle: consistent 116 | Layout/HeredocIndentation: 117 | Enabled: false 118 | Layout/RescueEnsureAlignment: 119 | Enabled: false 120 | Layout/EmptyLinesAroundAttributeAccessor: 121 | Enabled: true 122 | Layout/SpaceAroundMethodCallOperator: 123 | Enabled: true 124 | Layout/SpaceBeforeBrackets: 125 | Enabled: true 126 | Layout/LineEndStringConcatenationIndentation: 127 | Enabled: true 128 | Layout/LineContinuationLeadingSpace: # new in 1.31 129 | Enabled: true 130 | Layout/LineContinuationSpacing: # new in 1.31 131 | Enabled: true 132 | 133 | # ============================== Style ============================== 134 | Style/RescueModifier: 135 | Enabled: true 136 | Style/PercentLiteralDelimiters: 137 | PreferredDelimiters: 138 | default: '[]' 139 | '%i': '[]' 140 | '%w': '[]' 141 | Exclude: 142 | - 'config/routes.rb' 143 | Style/StringLiterals: 144 | Enabled: true 145 | Style/AsciiComments: 146 | Enabled: false 147 | Style/Copyright: 148 | Enabled: false 149 | Style/SafeNavigation: 150 | Enabled: false 151 | Style/Lambda: 152 | Enabled: false 153 | Style/Alias: 154 | Enabled: true 155 | EnforcedStyle: prefer_alias_method 156 | Style/ClassAndModuleChildren: 157 | Enabled: true 158 | EnforcedStyle: nested 159 | Style/TrailingCommaInArrayLiteral: 160 | Enabled: true 161 | EnforcedStyleForMultiline: no_comma 162 | Style/RescueStandardError: 163 | Enabled: true 164 | EnforcedStyle: explicit 165 | Style/InverseMethods: 166 | AutoCorrect: false 167 | Enabled: true 168 | Style/IfUnlessModifier: 169 | Enabled: false 170 | Style/SpecialGlobalVars: 171 | Enabled: false 172 | Style/BlockComments: 173 | Enabled: false 174 | Style/GuardClause: 175 | Enabled: false 176 | Style/TrailingCommaInHashLiteral: 177 | Enabled: false 178 | Style/ExponentialNotation: 179 | Enabled: true 180 | Style/HashEachMethods: 181 | Enabled: true 182 | Style/HashTransformKeys: 183 | Enabled: true 184 | Style/HashTransformValues: 185 | Enabled: true 186 | Style/RedundantFetchBlock: 187 | Enabled: true 188 | Style/RedundantRegexpCharacterClass: 189 | Enabled: true 190 | Style/RedundantRegexpEscape: 191 | Enabled: true 192 | Style/SlicingWithRange: 193 | Enabled: true 194 | Style/AccessorGrouping: 195 | Enabled: false 196 | Style/ArrayCoercion: 197 | Enabled: true 198 | Style/BisectedAttrAccessor: 199 | Enabled: true 200 | Style/CaseLikeIf: 201 | Enabled: true 202 | Style/HashAsLastArrayItem: 203 | Enabled: true 204 | Style/HashLikeCase: 205 | Enabled: true 206 | Style/RedundantAssignment: 207 | Enabled: true 208 | Style/RedundantFileExtensionInRequire: 209 | Enabled: true 210 | Style/ExplicitBlockArgument: 211 | Enabled: true 212 | Style/GlobalStdStream: 213 | Enabled: true 214 | Style/OptionalBooleanParameter: 215 | Enabled: true 216 | Style/SingleArgumentDig: 217 | Enabled: true 218 | Style/StringConcatenation: 219 | Enabled: true 220 | Style/ClassEqualityComparison: 221 | Enabled: true 222 | Style/CombinableLoops: 223 | Enabled: true 224 | Style/KeywordParametersOrder: 225 | Enabled: false 226 | Style/RedundantSelfAssignment: 227 | Enabled: true 228 | Style/SoleNestedConditional: 229 | Enabled: true 230 | Style/ArgumentsForwarding: 231 | Enabled: true 232 | Style/CollectionCompact: 233 | Enabled: true 234 | Style/DocumentDynamicEvalDefinition: 235 | Enabled: false 236 | Style/NegatedIfElseCondition: 237 | Enabled: true 238 | Style/NilLambda: 239 | Enabled: true 240 | Style/SwapValues: 241 | Enabled: true 242 | Style/RedundantArgument: 243 | Enabled: true 244 | Style/HashExcept: 245 | Enabled: true 246 | Style/EndlessMethod: 247 | Enabled: true 248 | Style/IfWithBooleanLiteralBranches: 249 | Enabled: true 250 | Style/HashConversion: 251 | Enabled: true 252 | Style/Documentation: 253 | Enabled: false 254 | Style/InPatternThen: 255 | Enabled: true 256 | Style/MultilineInPatternThen: 257 | Enabled: true 258 | Style/QuotedSymbols: 259 | Enabled: true 260 | Style/StringChars: 261 | Enabled: true 262 | Style/EmptyHeredoc: # new in 1.32 263 | Enabled: true 264 | Style/EnvHome: # new in 1.29 265 | Enabled: true 266 | Style/FetchEnvVar: # new in 1.28 267 | Enabled: true 268 | Style/FileRead: # new in 1.24 269 | Enabled: true 270 | Style/FileWrite: # new in 1.24 271 | Enabled: true 272 | Style/MagicCommentFormat: # new in 1.35 273 | Enabled: true 274 | Style/MapCompactWithConditionalBlock: # new in 1.30 275 | Enabled: true 276 | Style/MapToHash: # new in 1.24 277 | Enabled: true 278 | Style/NestedFileDirname: # new in 1.26 279 | Enabled: true 280 | Style/NumberedParameters: # new in 1.22 281 | Enabled: true 282 | Style/NumberedParametersLimit: # new in 1.22 283 | Enabled: true 284 | Style/ObjectThen: # new in 1.28 285 | Enabled: true 286 | Style/OpenStructUse: # new in 1.23 287 | Enabled: true 288 | Style/OperatorMethodCall: # new in 1.37 289 | Enabled: true 290 | Style/RedundantEach: # new in 1.38 291 | Enabled: true 292 | Style/RedundantInitialize: # new in 1.27 293 | Enabled: true 294 | Style/RedundantSelfAssignmentBranch: # new in 1.19 295 | Enabled: true 296 | Style/RedundantStringEscape: # new in 1.37 297 | Enabled: true 298 | Style/SelectByRegexp: # new in 1.22 299 | Enabled: true 300 | 301 | # ============================== Security ============================== 302 | Security/CompoundHash: # new in 1.28 303 | Enabled: true 304 | Security/IoMethods: # new in 1.22 305 | Enabled: true 306 | 307 | # ============================== Lint ============================== 308 | Lint/DuplicateMethods: 309 | Enabled: false 310 | Lint/AmbiguousOperator: 311 | Enabled: false 312 | Lint/DeprecatedOpenSSLConstant: 313 | Enabled: true 314 | Lint/MixedRegexpCaptureTypes: 315 | Enabled: true 316 | Lint/RaiseException: 317 | Enabled: true 318 | Lint/StructNewOverride: 319 | Enabled: true 320 | Lint/DuplicateElsifCondition: 321 | Enabled: true 322 | Lint/BinaryOperatorWithIdenticalOperands: 323 | Enabled: true 324 | Lint/DuplicateRescueException: 325 | Enabled: true 326 | Lint/EmptyConditionalBody: 327 | Enabled: true 328 | Lint/FloatComparison: 329 | Enabled: true 330 | Lint/MissingSuper: 331 | Enabled: false 332 | Lint/OutOfRangeRegexpRef: 333 | Enabled: true 334 | Lint/SelfAssignment: 335 | Enabled: true 336 | Lint/TopLevelReturnWithArgument: 337 | Enabled: true 338 | Lint/UnreachableLoop: 339 | Enabled: true 340 | Layout/BeginEndAlignment: 341 | Enabled: true 342 | Lint/ConstantDefinitionInBlock: 343 | Enabled: true 344 | Lint/DuplicateRequire: 345 | Enabled: true 346 | Lint/EmptyFile: 347 | Enabled: true 348 | Lint/HashCompareByIdentity: 349 | Enabled: true 350 | Lint/IdentityComparison: 351 | Enabled: true 352 | Lint/RedundantSafeNavigation: 353 | Enabled: true 354 | Lint/TrailingCommaInAttributeDeclaration: 355 | Enabled: true 356 | Lint/UselessMethodDefinition: 357 | Enabled: true 358 | Lint/UselessTimes: 359 | Enabled: true 360 | Lint/DuplicateBranch: 361 | Enabled: true 362 | Lint/DuplicateRegexpCharacterClassElement: 363 | Enabled: true 364 | Lint/EmptyBlock: 365 | Enabled: true 366 | Lint/EmptyClass: 367 | Enabled: true 368 | Lint/NoReturnInBeginEndBlocks: 369 | Enabled: true 370 | Lint/ToEnumArguments: 371 | Enabled: true 372 | Lint/UnmodifiedReduceAccumulator: 373 | Enabled: true 374 | Lint/UnexpectedBlockArity: 375 | Enabled: true 376 | Lint/DeprecatedConstants: 377 | Enabled: true 378 | Lint/LambdaWithoutLiteralBlock: 379 | Enabled: true 380 | Lint/NumberedParameterAssignment: 381 | Enabled: true 382 | Lint/OrAssignmentToConstant: 383 | Enabled: true 384 | Lint/RedundantDirGlobSort: 385 | Enabled: true 386 | Lint/SymbolConversion: 387 | Enabled: true 388 | Lint/TripleQuotes: 389 | Enabled: true 390 | Lint/AmbiguousAssignment: 391 | Enabled: true 392 | Lint/EmptyInPattern: 393 | Enabled: true 394 | Lint/AmbiguousOperatorPrecedence: # new in 1.21 395 | Enabled: true 396 | Lint/AmbiguousRange: # new in 1.19 397 | Enabled: true 398 | Lint/ConstantOverwrittenInRescue: # new in 1.31 399 | Enabled: true 400 | Lint/DuplicateMagicComment: # new in 1.37 401 | Enabled: true 402 | Lint/IncompatibleIoSelectWithFiberScheduler: # new in 1.21 403 | Enabled: true 404 | Lint/NonAtomicFileOperation: # new in 1.31 405 | Enabled: true 406 | Lint/RefinementImportMethods: # new in 1.27 407 | Enabled: true 408 | Lint/RequireRangeParentheses: # new in 1.32 409 | Enabled: true 410 | Lint/RequireRelativeSelfPath: # new in 1.22 411 | Enabled: true 412 | Lint/UselessRuby2Keywords: # new in 1.23 413 | Enabled: true 414 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | * [ISSUE](https://github.com/shlima/translate_enum/issues/9) Added pluralization 3 | 4 | ## 0.1.3 5 | * add licence file 6 | 7 | ## 0.1.2 8 | * add support of default locale lookup (en.attributes) to share locales between models 9 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | # Specify your gem's dependencies in translate_enum.gemspec 6 | gemspec 7 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Aliaksandr Shylau 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![CI](https://github.com/shlima/translate_enum/workflows/CI/badge.svg) 2 | [![Code Climate](https://codeclimate.com/github/shlima/translate_enum/badges/gpa.svg)](https://codeclimate.com/github/shlima/translate_enum) 3 | [![gem version](https://badge.fury.io/rb/health_bit.svg)](https://rubygems.org/gems/translate_enum) 4 | 5 | # TranslateEnum 6 | 7 | Simple, zero-dependant `enum` translation gem for Rails 8 | 9 | ## Installation 10 | 11 | `gem install translate_enum` 12 | 13 | ## Usage 14 | 15 | Here is a regular use case. ActiveRecord model: 16 | 17 | ```ruby 18 | class Post < ActiveRecord::Base 19 | include TranslateEnum 20 | 21 | enum status: { published: 0, archive: 1 } 22 | translate_enum :status 23 | end 24 | ``` 25 | 26 | Localization file 27 | 28 | ```yaml 29 | en: 30 | activerecord: 31 | attributes: 32 | post: 33 | status_list: 34 | published: Was published 35 | archive: Was archived 36 | ``` 37 | 38 | Or if you wish your locales to be available across all models 39 | 40 | ```yaml 41 | en: 42 | attributes: 43 | status_list: 44 | published: Was published 45 | archive: Was archived 46 | ``` 47 | 48 | ```ruby 49 | Post.translated_status(:published) #=> "Was published" 50 | Post.translated_statuses => [["Was published", :published, 0], ["Was archived", :archive, 1]] 51 | 52 | @post = Post.new(status: :published) 53 | @post.translated_status #=> "Was published" 54 | 55 | # Each `translated` method supports 56 | # I18n.translate attributes: 57 | 58 | Post.translated_status(:published, raise: true, throw: true, locale: :en, count: 10) 59 | ``` 60 | 61 | ### Use in a Form 62 | 63 | ```haml 64 | = form_for @post do |f| 65 | = f.select :status, options_for_select(f.object.class.translated_statuses.map { |translation, k, _v| [translation, k] }) 66 | ``` 67 | 68 | ## Extending ActiveRecord 69 | 70 | Be default you should extend each `ActiveRecord` model manually by including `TranslateEnum` module in it. 71 | You can extend `ActiveRecord` by requiring `translate_enum/active_record` in initializer or inside yout `Gemfile`: 72 | 73 | Gemfile: 74 | 75 | ```ruby 76 | gem 'translate_enum', require: 'translate_enum/active_record' 77 | ``` 78 | 79 | Initializer: 80 | 81 | ```ruby 82 | # config/initializers/translate_enum.rb 83 | require 'translate_enum/active_record' 84 | ``` 85 | 86 | ## Advanced options 87 | 88 | ```ruby 89 | class User < ActiveRecord::Base 90 | enum gender: [:male, :female] 91 | 92 | translate_enum :gender do |tr| 93 | tr.i18n_scope = 'activerecord.attributes' 94 | tr.i18n_key = 'gender_list' 95 | tr.enum_klass_method_name = 'genders' 96 | tr.enum_instance_method_name = 'gender' 97 | tr.method_name_singular = 'translated_gender' 98 | tr.method_name_plural = 'translated_genders' 99 | end 100 | 101 | # Or even provide your own logic 102 | def self.translated_gender(key) 103 | I18n.t(key, scope: 'global.gender_list') 104 | end 105 | end 106 | ``` 107 | # How To? 108 | ## How to use pluralization 109 | ```yaml 110 | en: 111 | activerecord: 112 | attributes: 113 | person: 114 | gender_list: 115 | male: 116 | zero: No males 117 | one: One male 118 | other: %{count} males 119 | 120 | ``` 121 | 122 | ```ruby 123 | Person.translated_gender(:make, count: 0) #=> "No males" 124 | Person.translated_genders => [["One male", :male, 0]] # and others 125 | Person.translated_genders(count: 0) => [["No males", :male, 0]] # and others 126 | 127 | @person = Person.new(gender: :male) 128 | @person.translated_gender #=> "One male" 129 | @person.translated_gender(count: 10) #=> "10 Males" 130 | ``` 131 | 132 | ## How use translate enum in serializer 133 | 134 | Example for Grape: 135 | 136 | ```ruby 137 | class Feedback < ApplicationRecord 138 | include TranslateEnum 139 | 140 | enum topic: { 141 | question: 'question', issue: 'issue', complaint: 'complaint', offer: 'offer', 142 | investment: 'investment' 143 | } 144 | 145 | translate_enum :topic 146 | end 147 | ``` 148 | 149 | ```ruby 150 | class FeedbacksApi < Grape::API 151 | resource :feedbacks do 152 | get 'enums' do 153 | present Feedback.method(:translated_topics), with: TranslateEnumSerializer 154 | end 155 | end 156 | end 157 | ``` 158 | 159 | ```ruby 160 | class TranslateEnumSerializer < Grape::Entity 161 | expose :enum, as: ->(method) { method.name[/translated_(.*)/, 1] } do |method| 162 | method.call.map do |translation, key, _value| 163 | { value: key, translation: translation } 164 | end 165 | end 166 | end 167 | ``` 168 | 169 | ```bash 170 | curl http://localhost:3000/feedbacks/enums 171 | ``` 172 | 173 | ```json 174 | { 175 | "topics": [ 176 | { 177 | "value": "question", 178 | "translation": "Vopros" 179 | }, 180 | { 181 | "value": "issue", 182 | "translation": "Problema" 183 | }, 184 | { 185 | "value": "complaint", 186 | "translation": "Zhaloba" 187 | }, 188 | { 189 | "value": "offer", 190 | "translation": "Predlozhenie" 191 | }, 192 | { 193 | "value": "investment", 194 | "translation": "Invisticii" 195 | } 196 | ] 197 | } 198 | ``` 199 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | task default: :spec 5 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "translate_enum" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /bin/release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm ./*.gem 4 | gem build translate_enum.gemspec 5 | gem push translate_enum-* 6 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/translate_enum.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'active_support/concern' 4 | require 'active_support/core_ext/string' 5 | require 'translate_enum/version' 6 | require 'translate_enum/builder' 7 | 8 | module TranslateEnum 9 | extend ActiveSupport::Concern 10 | 11 | # include TranslateEnum in ActiveModel or ActiveRecord 12 | module ClassMethods 13 | # @example 14 | # class User < ActiveRecord::Base 15 | # include TranslateEnum 16 | # enum status: %i(active archived) 17 | # translate_enum :status 18 | # end 19 | # 20 | # User.translated_status(:active) #=> "Active translation" 21 | def translate_enum(attribute, &block) 22 | builder = Builder.new(self, attribute, &block) 23 | 24 | # User.translated_status(:active) 25 | define_singleton_method(builder.method_name_singular) do |key, throw: false, raise: false, locale: nil, **options| 26 | opts = { default: builder.i18n_default_location(key) }.merge(options) 27 | I18n.translate("#{builder.i18n_scope}.#{builder.i18n_location(key)}", throw: throw, raise: raise, locale: locale, **opts) 28 | end 29 | 30 | # @return [Array] 31 | # @example 32 | # f.select_field :gender, f.object.class.translated_genders 33 | define_singleton_method(builder.method_name_plural) do |throw: false, raise: false, locale: nil, **options| 34 | options = { count: 1 }.merge(options) 35 | public_send(builder.enum_klass_method_name).map do |key, value| 36 | translation = public_send(builder.method_name_singular, key, throw: throw, raise: raise, locale: locale, **options) 37 | [translation, key, value] 38 | end 39 | end 40 | 41 | # @return [String] 42 | # @example 43 | # @user.translated_gender 44 | define_method(builder.method_name_singular) do |throw: false, raise: false, locale: nil, **options| 45 | if (key = public_send(builder.enum_instance_method_name)).present? 46 | options = { count: 1 }.merge(options) 47 | self.class.public_send(builder.method_name_singular, key, throw: throw, raise: raise, locale: locale, **options) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /lib/translate_enum/active_record.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | # Require this fle in order to be +TranslateEnum+ included in +ActiveRecord::Base+ 3 | # @example 4 | # require 'translate_enum/active_record' 5 | 6 | require_relative '../translate_enum' 7 | 8 | unless defined?(ActiveRecord::Base) 9 | raise NameError, 'TranslateEnum requires ActiveRecord be defined but it is not' 10 | end 11 | 12 | ActiveRecord::Base.include(TranslateEnum) 13 | -------------------------------------------------------------------------------- /lib/translate_enum/builder.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TranslateEnum 4 | class Builder 5 | attr_accessor :i18n_scope 6 | attr_accessor :i18n_key 7 | 8 | attr_accessor :enum_instance_method_name 9 | attr_accessor :enum_klass_method_name 10 | 11 | attr_accessor :method_name_singular 12 | attr_accessor :method_name_plural 13 | 14 | attr_reader :model, :attribute 15 | 16 | # @param model [ActiveModel::Model, ActiveRecord::Base] 17 | # @param attribute [String] 18 | def initialize(model, attribute) 19 | @model = model 20 | @attribute = attribute 21 | yield(self) if block_given? 22 | end 23 | 24 | # like "activerecord.attributes" or "activemodel.attributes" 25 | def i18n_scope 26 | @i18n_scope ||= "#{model.i18n_scope}.attributes" 27 | end 28 | 29 | def i18n_key 30 | @i18n_key ||= "#{attribute}_list" 31 | end 32 | 33 | def i18n_location(key) 34 | "#{model.model_name.i18n_key}.#{i18n_key}.#{key}" 35 | end 36 | 37 | def i18n_default_location(key) 38 | :"attributes.#{i18n_key}.#{key}" 39 | end 40 | 41 | # @param [String] 42 | # like "translated_genders" 43 | def method_name_plural 44 | @method_name_plural ||= "translated_#{attribute.to_s.pluralize}" 45 | end 46 | 47 | # @param [String] 48 | # like "translated_gender" 49 | def method_name_singular 50 | @method_name_singular ||= "translated_#{attribute.to_s.singularize}" 51 | end 52 | 53 | def enum_klass_method_name 54 | @enum_klass_method_name ||= attribute.to_s.pluralize 55 | end 56 | 57 | def enum_instance_method_name 58 | @enum_instance_method_name ||= attribute 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/translate_enum/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module TranslateEnum 4 | VERSION = '0.2.0' 5 | end 6 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'pry' 2 | require 'active_model' 3 | require 'action_view' 4 | require 'translate_enum' 5 | 6 | RSpec.configure do |config| 7 | config.expect_with :rspec do |expectations| 8 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 9 | end 10 | 11 | config.mock_with :rspec do |mocks| 12 | mocks.verify_partial_doubles = true 13 | end 14 | 15 | config.shared_context_metadata_behavior = :apply_to_host_groups 16 | config.filter_run_when_matching :focus 17 | config.disable_monkey_patching! 18 | config.warnings = true 19 | config.profile_examples = 0 20 | config.order = :random 21 | Kernel.srand config.seed 22 | end 23 | -------------------------------------------------------------------------------- /spec/translate_enum/action_view_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe ActionView do 2 | let(:view) do 3 | Class.new do 4 | include ActionView::Helpers 5 | include ActionView::Context 6 | end.new 7 | end 8 | 9 | let(:model) do 10 | Class.new do 11 | include ActiveModel::Model 12 | include TranslateEnum 13 | 14 | translate_enum :status 15 | 16 | def self.model_name 17 | ActiveModel::Name.new(nil, nil, 'model') 18 | end 19 | 20 | def self.statuses 21 | { published: 1, removed: 0 } 22 | end 23 | end 24 | end 25 | 26 | describe 'select_tag' do 27 | subject do 28 | options_for_select = model.translated_statuses.map { |translation, k, _v| [translation, k] } 29 | view.select_tag :statuses, view.options_for_select(options_for_select, selected: :published) 30 | end 31 | 32 | let(:expectation) do <<-HTML.chomp 33 | 35 | HTML 36 | end 37 | 38 | it 'puts value inside hidden option and translation inside visible tag' do 39 | expect(subject).to eq(expectation) 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/translate_enum/active_record_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe 'ActiveRecord::Base', order: :defined do 2 | subject do 3 | require 'translate_enum/active_record' 4 | end 5 | 6 | context 'ActiveRecord is undefined' do 7 | it 'raises an error' do 8 | expect { subject }.to raise_error(NameError) 9 | end 10 | end 11 | 12 | context 'ActiveRecord defined' do 13 | let(:active_record_class) do 14 | Class.new 15 | end 16 | 17 | before do 18 | stub_const('ActiveRecord::Base', active_record_class) 19 | end 20 | 21 | it 'extends ActiveRecord' do 22 | expect { subject }.to change { ActiveRecord::Base.respond_to?(:translate_enum) }.to true 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/translate_enum/translate_enum_spec.rb: -------------------------------------------------------------------------------- 1 | RSpec.describe TranslateEnum do 2 | let(:model_base) do 3 | Class.new do 4 | include ActiveModel::Model 5 | include TranslateEnum 6 | 7 | def self.model_name 8 | ActiveModel::Name.new(nil, nil, 'model') 9 | end 10 | 11 | def self.genders 12 | { male: 0, female: 1 } 13 | end 14 | 15 | def gender 16 | @gender ||= :male 17 | end 18 | 19 | def gender=(value) 20 | @gender = value 21 | end 22 | end 23 | end 24 | 25 | subject do 26 | model.new 27 | end 28 | 29 | context 'default' do 30 | let(:model) do 31 | Class.new(model_base) do 32 | translate_enum :gender 33 | end 34 | end 35 | 36 | let(:translations) do 37 | { 38 | male: 'I am a default of the male', 39 | female: 'Female (not default)' 40 | } 41 | end 42 | 43 | before do 44 | I18n.backend.store_translations(:en, attributes: { gender_list: { male: translations.fetch(:male) } }) 45 | I18n.backend.store_translations(:en, activemodel: { attributes: { model: { gender_list: { female: translations.fetch(:female) } } } }) 46 | end 47 | 48 | after do 49 | I18n.reload! 50 | end 51 | 52 | it 'uses default path as a fallback' do 53 | expect(model.translated_gender(:male)).to eq(translations.fetch(:male)) 54 | expect(model.translated_gender(:female)).to eq(translations.fetch(:female)) 55 | end 56 | end 57 | 58 | context 'default options' do 59 | let(:model) do 60 | Class.new(model_base) do 61 | translate_enum :gender 62 | end 63 | end 64 | 65 | let(:translations) do 66 | { 67 | male: 'translation missing: en.activemodel.attributes.model.gender_list.male', 68 | female: 'translation missing: en.activemodel.attributes.model.gender_list.female' 69 | } 70 | end 71 | 72 | it '#translated_gender' do 73 | expect(subject.gender).to eq(:male) 74 | 75 | expect { subject.gender = :female }.to change { 76 | subject.translated_gender 77 | }.from(translations.fetch(:male)).to(translations.fetch(:female)) 78 | 79 | expect(subject.gender).to eq(:female) 80 | end 81 | 82 | it '.translated_gender' do 83 | expect(model.translated_gender(:male)).to eq(translations.fetch(:male)) 84 | expect(model.translated_gender(:female)).to eq(translations.fetch(:female)) 85 | end 86 | 87 | it '.translated_genders' do 88 | expectation = [ 89 | [translations.fetch(:male), :male, 0], 90 | [translations.fetch(:female), :female, 1] 91 | ] 92 | expect(model.translated_genders).to match_array(expectation) 93 | end 94 | end 95 | 96 | context 'redefined options' do 97 | let(:model) do 98 | Class.new(model_base) do 99 | translate_enum :gender do |config| 100 | config.i18n_scope = 'scope' 101 | config.i18n_key = 'key' 102 | config.enum_klass_method_name = 'my_genders_method' 103 | config.enum_instance_method_name = 'my_gender_method' 104 | config.method_name_singular = 'russian_gender' 105 | config.method_name_plural = 'russian_genders' 106 | end 107 | 108 | class << self 109 | undef :genders 110 | end 111 | 112 | undef :gender 113 | undef :gender= 114 | 115 | def self.my_genders_method 116 | { male: 0, female: 1 } 117 | end 118 | 119 | def my_gender_method 120 | @my_gender_method ||= :male 121 | end 122 | 123 | def my_gender_method=(value) 124 | @my_gender_method = value 125 | end 126 | end 127 | end 128 | 129 | let(:translations) do 130 | { 131 | male: 'translation missing: en.scope.model.key.male', 132 | female: 'translation missing: en.scope.model.key.female' 133 | } 134 | end 135 | 136 | it '.russian_gender' do 137 | expect(model.russian_gender(:male)).to eq(translations.fetch(:male)) 138 | expect(model.russian_gender(:female)).to eq(translations.fetch(:female)) 139 | end 140 | 141 | it '.russian_genders' do 142 | expectation = [ 143 | [translations.fetch(:male), :male, 0], 144 | [translations.fetch(:female), :female, 1] 145 | ] 146 | expect(model.russian_genders).to match_array(expectation) 147 | end 148 | 149 | it '#russian_gender' do 150 | expect(subject.russian_gender).to eq(translations.fetch(:male)) 151 | end 152 | end 153 | 154 | context 'redefined methods' do 155 | let(:model) do 156 | Class.new(model_base) do 157 | translate_enum :gender 158 | 159 | def self.translated_gender(key, **options) 160 | "foo_bar_#{key}" 161 | end 162 | end 163 | end 164 | 165 | it 'returns redefined value' do 166 | expect(subject.translated_gender).to eq('foo_bar_male') 167 | end 168 | end 169 | 170 | context 'count' do 171 | let(:model) do 172 | Class.new(model_base) do 173 | translate_enum :gender 174 | end 175 | end 176 | 177 | let(:translations) do 178 | { 179 | male: { 180 | zero: 'no males', 181 | one: 'one male', 182 | other: '%{count} males' 183 | } 184 | } 185 | end 186 | 187 | before do 188 | I18n.backend.store_translations(:en, activemodel: { attributes: { model: { gender_list: translations } } } ) 189 | end 190 | 191 | after do 192 | I18n.reload! 193 | end 194 | 195 | it 'uses count' do 196 | expect(model.translated_gender(:male, count: 0)).to eq('no males') 197 | expect(model.translated_gender(:male, count: 1)).to eq('one male') 198 | expect(model.translated_gender(:male, count: 3)).to eq('3 males') 199 | 200 | expect(subject.translated_gender(count: 0)).to eq('no males') 201 | expect(subject.translated_gender(count: 1)).to eq('one male') 202 | expect(subject.translated_gender(count: 3)).to eq('3 males') 203 | expect(subject.translated_gender).to eq('one male') 204 | 205 | expect(subject.translated_gender).to eq('one male') 206 | expect(model.translated_genders.to_s).to include('one male') 207 | end 208 | end 209 | 210 | context 'trow' do 211 | let(:model) do 212 | Class.new(model_base) do 213 | translate_enum :gender 214 | end 215 | end 216 | 217 | 218 | it 'it works' do 219 | expect { model.translated_gender(:male, raise: true) }.to raise_error(I18n::MissingTranslationData) 220 | expect { model.translated_genders(raise: true) }.to raise_error(I18n::MissingTranslationData) 221 | expect { subject.translated_gender(raise: true) }.to raise_error(I18n::MissingTranslationData) 222 | end 223 | end 224 | end 225 | -------------------------------------------------------------------------------- /translate_enum.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/translate_enum/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'translate_enum' 7 | spec.version = TranslateEnum::VERSION 8 | spec.authors = ['Aliaksandr Shylau'] 9 | spec.email = ['alex.shilov.by@gmail.com'] 10 | spec.summary = 'Rails translate enum' 11 | spec.description = 'Simple, zero-dependant enum translation gem for Rails' 12 | spec.homepage = 'https://github.com/shlima/translate_enum' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = Gem::Requirement.new('>= 2.6.0') 15 | spec.metadata['rubygems_mfa_required'] = 'true' 16 | 17 | spec.files = Dir.chdir(File.expand_path(__dir__)) do 18 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 19 | end 20 | 21 | spec.bindir = 'exe' 22 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 23 | spec.require_paths = %w[lib] 24 | 25 | spec.add_dependency 'activesupport' 26 | spec.add_development_dependency 'actionview' 27 | spec.add_development_dependency 'activemodel' 28 | spec.add_development_dependency 'bundler' 29 | spec.add_development_dependency 'pry' 30 | spec.add_development_dependency 'rake' 31 | spec.add_development_dependency 'rspec' 32 | spec.add_development_dependency 'rubocop' 33 | spec.add_development_dependency 'rubocop-performance' 34 | end 35 | --------------------------------------------------------------------------------