diff --git a/.coffeelint/rules/detect_translatable_string.coffee b/.coffeelint/rules/detect_translatable_string.coffee new file mode 100644 index 000000000..a0d30a47b --- /dev/null +++ b/.coffeelint/rules/detect_translatable_string.coffee @@ -0,0 +1,91 @@ +module.exports = class DetectTranslatableString + + # coffeelint: disable=detect_translatable_string + rule: + name: 'detect_translatable_string' + level: 'ignore' + message: 'The following string looks like it should be marked as translatable via __(...)' + description: ''' + ''' + + constructor: -> + @callTokens = [] + + tokens: ['STRING', 'CALL_START', 'CALL_END'] + + lintToken: (token, tokenApi) -> + [type, tokenValue] = token + + if type in ['CALL_START', 'CALL_END'] + @trackCall token, tokenApi + return + + return false if @isInIgnoredMethod() + + return @lintString(token, tokenApi) + + lintString: (token, tokenApi) -> + [type, tokenValue] = token + + # Remove quotes. + string = tokenValue[1..-2] + + # Ignore strings with less than two words. + return false if string.split(' ').length < 2 + + # Ignore strings that are being used as exception; unlike Ruby exceptions, these should not reach the user. + return false if tokenApi.peek(-3)[1] == 'throw' + return false if tokenApi.peek(-2)[1] == 'throw' + return false if tokenApi.peek(-1)[1] == 'throw' + + # Ignore strings that are being used for comparison + return false if tokenApi.peek(-1)[1] == '==' + + # String interpolation is handled via concatenation, ignore such strings. + return false if tokenApi.peek(1)[1] == '+' + return false if tokenApi.peek(2)[1] == '+' + + BLOCKLIST = [ + # Only look at strings starting with upper case letters + /^[^A-Z]/, + # # Ignore strings starting with three upper case letters like SELECT, POST etc. + # /^[A-Z]{3}/, + ] + + return false if BLOCKLIST.some (entry) -> + #console.log([string, entry, string.match(entry), token, tokenApi.peek(-1), tokenApi.peek(1)]) + string.match(entry) + + # console.log(tokenApi.peek(-3)) + # console.log(tokenApi.peek(-2)) + # console.log(tokenApi.peek(-1)) + # console.log(token) + + return { context: "Found: #{token[1]}" } + + ignoredMethods: { + '__': true, + 'log': true, + 'T': true, + 'controllerBind': true, + 'error': true, # App.Log.error + 'set': true, # App.Config.set + 'translateInline': true, + 'translateContent': true, + 'translatePlain': true, + } + + isInIgnoredMethod: -> + #console.log(@callTokens) + for t in @callTokens + return true if t.isIgnoredMethod + return false + + trackCall: (token, tokenApi) -> + if token[0] is 'CALL_START' + p = tokenApi.peek(-1) + token.isIgnoredMethod = p and @ignoredMethods[p[1]] + @callTokens.push(token) + else + @callTokens.pop() + return null \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1553bb770..e3b376028 100644 --- a/.gitignore +++ b/.gitignore @@ -37,8 +37,8 @@ /db/*.sqlite3 /db/schema.rb -# translation cache files -/config/locales*.yml +# legacy translation cache files +/config/locales-*.yml /config/translations/*.yml # NPM / Yarn diff --git a/.gitlab/ci/pre.yml b/.gitlab/ci/pre.yml index 8b02c64bd..0b667323b 100644 --- a/.gitlab/ci/pre.yml +++ b/.gitlab/ci/pre.yml @@ -22,7 +22,26 @@ shellcheck: script: - shellcheck -S warning $(find . -name "*.sh" -o -name "functions" | grep -v "/vendor/") -zeitwerk_check: +gettext lint: + <<: *template_pre + before_script: + - echo "Disable default before_script." + script: + - for FILE in i18n/*.pot i18n/*.po; do echo "Checking $FILE"; msgfmt -o /dev/null -c $FILE; done + +gettext catalog consistency: + <<: *template_pre + extends: + - .tags_docker + - .services_postgresql + script: + - bundle install -j $(nproc) --path vendor + - bundle exec ruby .gitlab/configure_environment.rb + - source .gitlab/environment.env + - bundle exec rake zammad:db:init + - bundle exec rails generate translation_catalog --check + +zeitwerk:check: <<: *template_pre extends: - .tags_docker @@ -48,7 +67,7 @@ brakeman: coffeelint: <<: *template_pre script: - - coffeelint app/ + - coffeelint --rules ./.coffeelint/rules/* app/ stylelint: <<: *template_pre diff --git a/.overcommit.yml b/.overcommit.yml index e480513fe..e6503b4f6 100644 --- a/.overcommit.yml +++ b/.overcommit.yml @@ -9,10 +9,18 @@ PreCommit: enabled: false RuboCop: enabled: true - on_warn: fail # Treat all warnings as failures + on_warn: fail CoffeeLint: + # .coffeelint/rules/* not supported in YAML, specify all rules separately. + flags: ['--reporter=csv', '--rules', './.coffeelint/rules/detect_translatable_string.coffee'] enabled: true - on_warn: fail # Treat all warnings as failures + on_warn: fail + exclude: public/assets/chat/**/* + CustomScript: + enabled: true + description: 'Check if translation catalog is up-to-date' + required_executable: 'rails' + flags: ['generate', 'translation_catalog', '--check'] Stylelint: enabled: true @@ -43,4 +51,3 @@ PreRebase: PrepareCommitMsg: ALL: enabled: false - diff --git a/.rubocop/cop/zammad/detect_translatable_string.rb b/.rubocop/cop/zammad/detect_translatable_string.rb new file mode 100644 index 000000000..d56f4a702 --- /dev/null +++ b/.rubocop/cop/zammad/detect_translatable_string.rb @@ -0,0 +1,90 @@ +# Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ + +module RuboCop + module Cop + module Zammad + class DetectTranslatableString < Base + extend AutoCorrector + + MSG = 'This string looks like it should be marked as translatable via __(...).'.freeze + + def on_str(node) + # Constants like __FILE__ are handled as strings, but don't respond to begin. + return if !node.loc.respond_to?(:begin) || !node.loc.begin + return if part_of_ignored_node?(node) + + return if !offense?(node) + + add_offense(node) do |corrector| + corrector.replace(node, "__(#{node.source})") + end + end + + def on_regexp(node) + ignore_node(node) + end + + METHOD_NAME_BLOCKLIST = %i[ + __ translate + include? eql? parse + debug info warn error fatal unknown log log_error + ].freeze + + def on_send(node) + ignore_node(node) if METHOD_NAME_BLOCKLIST.include? node.method_name + end + + private + + PARENT_SOURCE_BLOCKLIST = [ + # Ignore logged strings + 'Rails.logger' + ].freeze + + NODE_START_BLOCKLIST = [ + # Only look at strings starting with upper case letters + %r{[^A-Z]}, + # Ignore strings starting with three upper case letters like SELECT, POST etc. + %r{[A-Z]{3}}, + ].freeze + + NODE_CONTAIN_BLOCKLIST = [ + # Ignore strings with interpolation. + '#{', + # Ignore Email addresses + '@' + ].freeze + + def offense?(node) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + + # Ignore Hash Keys + return false if node.parent.type.eql?(:pair) && node.parent.children.first.equal?(node) + + # Ignore equality checks like ... == 'My String' + return false if node.left_sibling.eql?(:==) + + # Remove quotes + node_source = node.source[1..-2] + + # Only match strings with at least two words + return false if node_source.split.count < 2 + + NODE_START_BLOCKLIST.each do |entry| + return false if node_source.start_with? entry + end + + NODE_CONTAIN_BLOCKLIST.each do |entry| + return false if node_source.include? entry + end + + parent_source = node.parent.source + PARENT_SOURCE_BLOCKLIST.each do |entry| + return false if parent_source.include? entry + end + + true + end + end + end + end +end diff --git a/.rubocop/default.yml b/.rubocop/default.yml index f08267c10..c3725f7ed 100644 --- a/.rubocop/default.yml +++ b/.rubocop/default.yml @@ -317,6 +317,20 @@ Zammad/ExistsResetColumnInformation: - 'db/migrate/201*_*.rb' - 'db/migrate/2020*_*.rb' +Zammad/DetectTranslatableString: + Enabled: true + Include: + - "app/**/*.rb" + - "db/**/*.rb" + - "lib/**/*.rb" + Exclude: + - "db/migrate/**/*.rb" + - "db/addon/**/*.rb" + - "lib/generators/**/*.rb" + - "lib/sequencer/**/*.rb" + - "lib/import/**/*.rb" + - "lib/tasks/**/*.rb" + Zammad/ExistsDbStrategy: Include: - "spec/**/*.rb" diff --git a/.rubocop/rubocop_zammad.rb b/.rubocop/rubocop_zammad.rb index f3e8764cd..9e8817085 100644 --- a/.rubocop/rubocop_zammad.rb +++ b/.rubocop/rubocop_zammad.rb @@ -1,6 +1,7 @@ # Copyright (C) 2012-2021 Zammad Foundation, http://zammad-foundation.org/ require_relative 'cop/zammad/exists_condition' +require_relative 'cop/zammad/detect_translatable_string' require_relative 'cop/zammad/exists_date_time_precision' require_relative 'cop/zammad/exists_db_strategy' require_relative 'cop/zammad/exists_reset_column_information' diff --git a/Gemfile b/Gemfile index 6ce41c930..8c3226e5f 100644 --- a/Gemfile +++ b/Gemfile @@ -149,6 +149,9 @@ gem 'viewpoint', require: false # integrations - S/MIME gem 'openssl' +# Translation sync +gem 'PoParser', require: false + # Gems used only for develop/test and not required # in production environments by default. group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index ffd337b36..a66972b3f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,6 +19,8 @@ GIT GEM remote: https://rubygems.org/ specs: + PoParser (3.2.5) + simple_po_parser (~> 1.1.2) aasm (5.2.0) concurrent-ruby (~> 1.0) actioncable (6.0.4.1) @@ -555,6 +557,7 @@ GEM shoulda-matchers (5.0.0) activesupport (>= 5.2.0) simple_oauth (0.3.1) + simple_po_parser (1.1.5) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -655,6 +658,7 @@ PLATFORMS ruby DEPENDENCIES + PoParser aasm activerecord-import activerecord-nulldb-adapter diff --git a/app/assets/javascripts/app/controllers/_application_controller/_base.coffee b/app/assets/javascripts/app/controllers/_application_controller/_base.coffee index 93a5c2c87..fb786a20f 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/_base.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/_base.coffee @@ -114,7 +114,7 @@ class App.Controller extends Spine.Controller if window.clipboardData # IE window.clipboardData.setData('Text', text) else - window.prompt('Copy to clipboard: Ctrl+C, Enter', text) + window.prompt(__('Copy to clipboard: Ctrl+C, Enter'), text) # disable all delay's and interval's disconnectClient: -> diff --git a/app/assets/javascripts/app/controllers/_application_controller/_modal.coffee b/app/assets/javascripts/app/controllers/_application_controller/_modal.coffee index e7302425d..878fb29ff 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/_modal.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/_modal.coffee @@ -59,7 +59,7 @@ class App.ControllerModal extends App.Controller false content: -> - 'You need to implement a one @content()!' + __('You need to implement a one @content()!') update: => if @message @@ -106,7 +106,7 @@ class App.ControllerModal extends App.Controller if @buttonSubmit is true @buttonSubmit = 'Submit' if @buttonCancel is true - @buttonCancel = 'Cancel & Go Back' + @buttonCancel = __('Cancel & Go Back') @update() diff --git a/app/assets/javascripts/app/controllers/_application_controller/form.coffee b/app/assets/javascripts/app/controllers/_application_controller/form.coffee index f19423c96..b923a64b2 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/form.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/form.coffee @@ -1,5 +1,5 @@ class App.ControllerForm extends App.Controller - fullFormSubmitLabel: 'Submit' + fullFormSubmitLabel: __('Submit') fullFormSubmitAdditionalClasses: '' fullFormButtonsContainerClass: '' fullFormAdditionalButtons: [] # [{className: 'js-class', text: 'Label'}] @@ -173,7 +173,7 @@ class App.ControllerForm extends App.Controller # input text field with max. 100 size attribute_config = { name: 'subject' - display: 'Subject' + display: __('Subject') tag: 'input' type: 'text' limit: 100 @@ -185,7 +185,7 @@ class App.ControllerForm extends App.Controller # colection as relation with auto completion attribute_config = { name: 'customer_id' - display: 'Customer' + display: __('Customer') tag: 'autocompletion' # auto completion params, endpoints, ui,... type: 'text' @@ -193,7 +193,7 @@ class App.ControllerForm extends App.Controller null: false relation: 'User' autocapitalize: false - help: 'Select the customer of the ticket or create one.' + help: __('Select the customer of the ticket or create one.') helpLink: '»' callback: @userInfo class: 'span7' @@ -202,7 +202,7 @@ class App.ControllerForm extends App.Controller # colection as relation attribute_config = { name: 'priority_id' - display: 'Priority' + display: __('Priority') tag: 'select' multiple: false null: false @@ -216,7 +216,7 @@ class App.ControllerForm extends App.Controller # colection as options attribute_config = { name: 'priority_id' - display: 'Priority' + display: __('Priority') tag: 'select' multiple: false null: false diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_description.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_description.coffee index d48f18b11..abe6831dc 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_description.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_description.coffee @@ -1,8 +1,8 @@ class App.ControllerGenericDescription extends App.ControllerModal buttonClose: true buttonCancel: false - buttonSubmit: 'Close' - head: 'Description' + buttonSubmit: __('Close') + head: __('Description') content: => marked(App.i18n.translateContent(@description)) diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_destroy_confirm.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_destroy_confirm.coffee index 6baba4c44..35f34c211 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_destroy_confirm.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_destroy_confirm.coffee @@ -1,9 +1,9 @@ class App.ControllerGenericDestroyConfirm extends App.ControllerModal buttonClose: true buttonCancel: true - buttonSubmit: 'delete' + buttonSubmit: __('delete') buttonClass: 'btn--danger' - head: 'Confirm' + head: __('Confirm') small: true content: -> @@ -23,9 +23,9 @@ class App.ControllerGenericDestroyConfirm extends App.ControllerModal class App.ControllerConfirm extends App.ControllerModal buttonClose: true buttonCancel: true - buttonSubmit: 'yes' + buttonSubmit: __('yes') buttonClass: 'btn--danger' - head: 'Confirm' + head: __('Confirm') small: true content: -> diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee index 568737b6f..eb01ec94b 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_edit.coffee @@ -51,5 +51,5 @@ class App.ControllerGenericEdit extends App.ControllerModal App[ ui.genericObject ].fetch(id: @id) ui.log 'errors' ui.formEnable(e) - ui.controller.showAlert(details.error_human || details.error || 'Unable to update object!') + ui.controller.showAlert(details.error_human || details.error || __('Unable to update object!')) ) diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_error_modal.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_error_modal.coffee index ac2ee345c..4f094a063 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_error_modal.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_error_modal.coffee @@ -1,9 +1,9 @@ class App.ControllerErrorModal extends App.ControllerModal buttonClose: true buttonCancel: false - buttonSubmit: 'Close' + buttonSubmit: __('Close') #buttonClass: 'btn--danger' - head: 'Error' + head: __('Error') #small: true #shown: true showTrySupport: true diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_history.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_history.coffee index 29f29905a..f603d77c6 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_history.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_history.coffee @@ -5,7 +5,7 @@ class App.GenericHistory extends App.ControllerModal buttonClose: true buttonCancel: false buttonSubmit: false - head: 'History' + head: __('History') shown: false constructor: -> diff --git a/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee b/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee index e965d3895..9f0023e45 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/generic_new.coffee @@ -51,5 +51,5 @@ class App.ControllerGenericNew extends App.ControllerModal fail: (settings, details) -> ui.log 'errors', details ui.formEnable(e) - ui.controller.showAlert(details.error_human || details.error || 'Unable to create object!') + ui.controller.showAlert(details.error_human || details.error || __('Unable to create object!')) ) diff --git a/app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee b/app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee index 59957b9d2..0419c69eb 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/reorder_modal.coffee @@ -1,5 +1,5 @@ class App.ControllerReorderModal extends App.ControllerModal - head: 'Drag to reorder' + head: __('Drag to reorder') content: -> view = $(App.view('reorder_modal')()) @@ -10,7 +10,7 @@ class App.ControllerReorderModal extends App.ControllerModal true overview: ['title'] attribute_list: [ - { name: 'title', display: 'Name' } + { name: 'title', display: __('Name') } ] objects: @items ) @@ -49,4 +49,3 @@ class App.ControllerReorderModal extends App.ControllerModal onSubmit: -> super @save() - diff --git a/app/assets/javascripts/app/controllers/_application_controller/table.coffee b/app/assets/javascripts/app/controllers/_application_controller/table.coffee index 3056a6a32..30c878ade 100644 --- a/app/assets/javascripts/app/controllers/_application_controller/table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller/table.coffee @@ -28,7 +28,7 @@ # add new header item attribute = name: 'some name' - display: 'Some Name' + display: __('Some Name') headers.push attribute console.log('new header is', headers) headers @@ -78,12 +78,12 @@ el: element overview: ['time', 'area', 'level', 'browser', 'location', 'data'] attribute_list: [ - { name: 'time', display: 'Time', tag: 'datetime' }, - { name: 'area', display: 'Area', type: 'text' }, - { name: 'level', display: 'Level', type: 'text' }, - { name: 'browser', display: 'Browser', type: 'text' }, - { name: 'location', display: 'Location', type: 'text' }, - { name: 'data', display: 'Data', type: 'text' }, + { name: 'time', display: __('Time'), tag: 'datetime' }, + { name: 'area', display: __('Area'), type: 'text' }, + { name: 'level', display: __('Level'), type: 'text' }, + { name: 'browser', display: __('Browser'), type: 'text' }, + { name: 'location', display: __('Location'), type: 'text' }, + { name: 'data', display: __('Data'), type: 'text' }, ] objects: data ) @@ -641,7 +641,7 @@ class App.ControllerTable extends App.Controller if @clone @actions.push name: 'clone' - display: 'Clone' + display: __('Clone') icon: 'clipboard' class: 'create js-clone' callback: (id) => @@ -660,7 +660,7 @@ class App.ControllerTable extends App.Controller if @destroy @actions.push name: 'delete' - display: 'Delete' + display: __('Delete') icon: 'trash' class: 'danger js-delete' callback: (id) => @@ -672,7 +672,7 @@ class App.ControllerTable extends App.Controller if @actions.length @headers.push name: 'action' - display: 'Action' + display: __('Action') width: '50px' displayWidth: 50 align: 'right' diff --git a/app/assets/javascripts/app/controllers/_channel/_email_filter.coffee b/app/assets/javascripts/app/controllers/_channel/_email_filter.coffee index 17ec6f63a..a270f131b 100644 --- a/app/assets/javascripts/app/controllers/_channel/_email_filter.coffee +++ b/app/assets/javascripts/app/controllers/_channel/_email_filter.coffee @@ -11,7 +11,7 @@ class App.ChannelEmailFilter extends App.Controller template = $( '
' + App.i18n.translateContent('New') + '
' ) - description = 'With filters you can e. g. dispatch new tickets into certain groups or set a certain priority for tickets of a VIP customer.' + description = __('With filters you can e. g. dispatch new tickets into certain groups or set a certain priority for tickets of a VIP customer.') new App.ControllerTable( el: template.find('.overview') @@ -28,7 +28,7 @@ class App.ChannelEmailFilter extends App.Controller e.preventDefault() new App.ControllerGenericNew( pageData: - object: 'Postmaster Filter' + object: __('Postmaster Filter') genericObject: 'PostmasterFilter' container: @el.closest('.content') callback: @load @@ -40,7 +40,7 @@ class App.ChannelEmailFilter extends App.Controller new App.ControllerGenericEdit( id: id, pageData: - object: 'Postmaster Filter' + object: __('Postmaster Filter') genericObject: 'PostmasterFilter' container: @el.closest('.content') callback: @load diff --git a/app/assets/javascripts/app/controllers/_channel/_email_signature.coffee b/app/assets/javascripts/app/controllers/_channel/_email_signature.coffee index 5948003d8..c5f83bb06 100644 --- a/app/assets/javascripts/app/controllers/_channel/_email_signature.coffee +++ b/app/assets/javascripts/app/controllers/_channel/_email_signature.coffee @@ -11,11 +11,11 @@ class App.ChannelEmailSignature extends App.Controller template = $( '
' + App.i18n.translateContent('New') + '
' ) - description = ''' + description = __(''' You can define different signatures for each group. So you can have different email signatures for different departments. Once you have created a signature here, you need also to edit the groups where you want to use it. -''' +''') new App.ControllerTable( el: template.find('.overview') @@ -46,7 +46,7 @@ class ChannelEmailSignatureEdit extends App.ControllerModal buttonClose: true buttonCancel: true buttonSubmit: true - head: 'Signature' + head: __('Signature') content: => if @object @@ -90,5 +90,5 @@ class ChannelEmailSignatureEdit extends App.ControllerModal fail: (settings, details) => @log 'errors', details @formEnable(e) - @form.showAlert(details.error_human || details.error || 'Unable to create object!') + @form.showAlert(details.error_human || details.error || __('Unable to create object!')) ) diff --git a/app/assets/javascripts/app/controllers/_channel/chat.coffee b/app/assets/javascripts/app/controllers/_channel/chat.coffee index cbea22b48..5f29a4ac0 100644 --- a/app/assets/javascripts/app/controllers/_channel/chat.coffee +++ b/app/assets/javascripts/app/controllers/_channel/chat.coffee @@ -1,6 +1,6 @@ class ChannelChat extends App.ControllerSubContent requiredPermission: 'admin.channel_chat' - header: 'Chat' + header: __('Chat') events: 'change .js-params': 'updateParams' 'input .js-params': 'updateParams' @@ -32,74 +32,74 @@ class ChannelChat extends App.ControllerSubContent name: 'chatId' default: '1' type: 'Number' - description: 'Identifier of the chat-topic.' + description: __('Identifier of the chat-topic.') } { name: 'show' default: true type: 'Boolean' - description: 'Show the chat when ready.' + description: __('Show the chat when ready.') } { name: 'target' default: "$('body')" type: 'jQuery Object' - description: 'Where to append the chat to.' + description: __('Where to append the chat to.') } { name: 'host' default: '(Empty)' type: 'String' - description: "If left empty, the host gets auto-detected - in this case %s. The auto-detection reads out the host from the