Просмотр исходного кода

Fixes #4875 - Translation Management Improvements.

Co-authored-by: Dusan Vuckovic <dv@zammad.com>
Co-authored-by: Benjamin Scharf <bs@zammad.com>
Co-authored-by: Dominik Klein <dk@zammad.com>
Co-authored-by: Martin Gruner <mg@zammad.com>
Benjamin Scharf 11 месяцев назад
Родитель
Сommit
cffdc2c66e

+ 0 - 2
.rubocop/todo.rspec.yml

@@ -162,7 +162,6 @@ RSpec/ContextWording:
     - 'spec/system/system/integration/slack_spec.rb'
     - 'spec/system/system/integration/smime_spec.rb'
     - 'spec/system/system/object_manager_spec.rb'
-    - 'spec/system/system/translations_spec.rb'
     - 'spec/system/ticket/create_spec.rb'
     - 'spec/system/ticket/shared_draft_start_spec.rb'
     - 'spec/system/ticket/shared_draft_zoom_spec.rb'
@@ -322,7 +321,6 @@ RSpec/MessageSpies:
     - 'spec/models/ticket/number_spec.rb'
     - 'spec/models/ticket_spec.rb'
     - 'spec/models/user_spec.rb'
-    - 'spec/system/system/translations_spec.rb'
 
 RSpec/MultipleExpectations:
   Exclude:

+ 309 - 234
app/assets/javascripts/app/controllers/translation.coffee

@@ -1,88 +1,131 @@
 class Translation extends App.ControllerSubContent
   @requiredPermission: 'admin.translation'
   header: __('Translations')
+
   events:
-    'click .js-resetChanges': 'resetChanges'
-  initialRenderingDone: false
+    'click .js-description': 'showDescriptionModal'
+    'click .js-new-translation': 'showNewTranslationModal'
 
   constructor: ->
     super
-    @locale = App.i18n.get()
-    @render()
-    @controllerBind('i18n:translation_update_todo', =>
-      @load('i18n:translation_update_todo')
-    )
-    @controllerBind('i18n:translation_update_list', =>
-      @load('i18n:translation_update_list')
+
+    @load()
+    @controllerBind('i18n:translation_update_todo', @load)
+    @controllerBind('i18n:translation_update_list', @load)
+    @controllerBind('i18n:translation_update', @load)
+
+  load: =>
+    @startLoading()
+
+    @ajax(
+      id: 'translation_index'
+      type: 'GET'
+      url: "#{@apiPath}/translations/customized"
+      processData: true
+      success: (data) =>
+        @stopLoading()
+        @render(data)
     )
-    @controllerBind('i18n:translation_update', =>
-      @load()
+
+  getLocaleString: (value) ->
+    App.Locale.findByAttribute('locale', value)?.name or value
+
+  removeTranslation: (id) =>
+    new App.ControllerConfirm(
+      message: __('Are you sure?')
+      buttonClass: 'btn--danger'
+      callback: =>
+        @startLoading(@$('.js-table-translations-container'))
+        @ajax(
+          id: 'translation_remove'
+          type: 'DELETE'
+          url: "#{@apiPath}/translations/#{id}"
+          success: =>
+            @stopLoading()
+            @hasModifiedTranslations = true
+            @load()
+        )
+      container: @el.closest('.content')
     )
 
-  render: =>
-    locales = App.Locale.all()
-    currentLanguage = @locale
-    for locale in locales
-      if locale.locale is @locale
-        currentLanguage = locale.name
-    @html App.view('translation/index')(
-      currentLanguage: currentLanguage
-      inlineTranslationKey: App.Browser.hotkeys().split('+').reverse().join('+') + '+t'
+  resetTranslation: (id) =>
+    new App.ControllerConfirm(
+      message: __('Are you sure?')
+      buttonClass: 'btn--danger'
+      callback: =>
+        @startLoading(@$('.js-table-translations-container'))
+        @ajax(
+          id: 'translation_reset'
+          type: 'PUT'
+          url: "#{@apiPath}/translations/reset/#{id}"
+          success: =>
+            @stopLoading()
+            @hasModifiedTranslations = true
+            @load()
+        )
+      container: @el.closest('.content')
     )
-    @load('render')
 
-  load: (event) =>
-    @ajax(
-      id:    'translations_admin'
-      type:  'GET'
-      url:   "#{@apiPath}/translations/admin/lang/#{@locale}"
-      processData: true
-      success: (data, status, xhr) =>
-        @initialRenderingDone = true
-        @times                = []
-        @stringsNotTranslated = []
-        @stringsTranslated    = []
-        for item in data.list
-          if item[1] is 'FORMAT_DATE' or item[1] is 'FORMAT_DATETIME'
-            @times.push item
-          else
-            if item[2] is ''
-              @stringsNotTranslated.push item
-            else
-              @stringsTranslated.push item
-
-        if !@translationToDo || event is 'render'
-          @translationToDo = new TranslationToDo(
-            el:             @$('.js-ToDo')
-            locale:         @locale
-            updateOnServer: @updateOnServer
-            getAttributes:  @getAttributes
-          )
-        if !event || event is 'i18n:translation_update_todo'|| event is 'render'
-          @translationToDo.update(
-            stringsNotTranslated: @stringsNotTranslated
-            stringsTranslated:    @stringsTranslated
-            times:                @times
-          )
-        if !@translationList || event is 'render'
-          @translationList = new TranslationList(
-            el:             @$('.js-List')
-            locale:         @locale
-            updateOnServer: @updateOnServer
-            getAttributes:  @getAttributes
-          )
-        if !event || event is 'i18n:translation_update_list'|| event is 'render'
-          @translationList.update(
-            stringsNotTranslated: @stringsNotTranslated
-            stringsTranslated:    @stringsTranslated
-            times:                @times
-          )
-        @toggleAction()
+  editTranslation: (data, id) =>
+    translationData = _.find(data, (translationData) -> translationData.id is id)
+    @showEditTranslationModal(translationData)
+
+  renderTable: (customTranslationData, el) =>
+    new App.ControllerTable(
+      el: el
+      overviewAttributes: ['source', 'target_initial', 'target', 'locale']
+      attribute_list: [
+        { name: 'source', display: __('Translation Source'), unsortable: true },
+        { name: 'target_initial', display: __('Original Translation'), unsortable: true },
+        { name: 'target', display: __('Custom Translation'), unsortable: true },
+        { name: 'locale', display: __('Target Language'), unsortable: true },
+      ]
+      objects: customTranslationData
+      bindRow:
+        events:
+          'click': (id) => @editTranslation(customTranslationData, id)
+      customActions: [
+        {
+          name: 'edit',
+          display: __('Edit')
+          icon: 'pen'
+          class: 'js-edit'
+          callback: (id) => @editTranslation(customTranslationData, id)
+        },
+        {
+          name: 'reset',
+          display: __('Reset')
+          icon: 'reload'
+          class: 'btn--danger js-reset'
+          callback: @resetTranslation
+          available: (translationData) ->
+            translationData.is_synchronized_from_codebase
+        },
+        {
+          name: 'remove',
+          display: __('Remove')
+          icon: 'trash'
+          class: 'btn--danger js-remove'
+          callback: @removeTranslation
+          available: (translationData) ->
+            not translationData.is_synchronized_from_codebase
+        },
+      ]
+      callbackAttributes:
+        locale: [@getLocaleString]
     )
 
-  show: =>
-    return if @initialRenderingDone is false
-    @render()
+  render: (customTranslationData) =>
+    content = $(App.view('translation/index')(
+      hasDescriptionButton: customTranslationData.length > 0,
+    ))
+
+    if customTranslationData.length > 0
+      @renderTable(customTranslationData, content.find('.js-content-container'))
+    else
+      content.find('.js-content-container').html(@description())
+
+    @html content
 
   hide: =>
     @rerender()
@@ -91,189 +134,221 @@ class Translation extends App.ControllerSubContent
     @rerender()
 
   rerender: =>
-    rerender = ->
-      App.Event.trigger('ui:rerender')
-    if @translationList && @translationList.changes()
-      App.Delay.set(rerender, 400)
+    return if not @hasModifiedTranslations
 
-  showAction: =>
-    @$('.js-changes').removeClass('hidden')
+    App.Delay.set(->
+      App.Event.trigger('ui:rerender')
+    , 400)
 
-  hideAction: =>
-    @el.closest('.content').find('.js-changes').addClass('hidden')
+  description: ->
+    $(App.view('translation/description')(
+      inlineTranslationKey: App.Browser.hotkeys().split('+').reverse().join('+') + '+t'
+    ))
+
+  showDescriptionModal: =>
+    new TranslationDescriptionModal
+      contentInline: @description()
+      container: $('.content')
+
+  showNewTranslationModal: ->
+    new TranslationModal
+      headPrefix: __('New')
+      container: $('.content')
+      successCallback: =>
+        @hasModifiedTranslations = true
+        @load()
+
+  showEditTranslationModal: (translationData) ->
+    new TranslationModal
+      headPrefix: __('Edit')
+      data: translationData
+      container: $('.content')
+      successCallback: =>
+        @hasModifiedTranslations = true
+        @load()
+
+class TranslationDescriptionModal extends App.ControllerModal
+  head: __('Description')
+  buttonSubmit: __('Close')
+  shown: true
+
+  onSubmit: =>
+    @close()
+
+class TranslationModal extends App.ControllerModal
+  head: __('Translation')
+  shown: true
+  buttonSubmit: true
+  buttonCancel: true
+  large: true
+
+  content: ->
+    false
 
-  toggleAction: =>
-    if @$('.js-Reset:visible').length > 0
-      @showAction()
-    else
-      @hideAction()
+  render: =>
+    super
 
-  resetChanges: =>
-    @loader = new App.ControllerModalLoading(
-      head:      __('Reset changes')
-      message:   __('Resetting changes…')
-      container: @el.closest('.content')
+    # If data is present, it means we are editing an existing translation.
+    isEditingTranslation = Boolean(@data)
+
+    content = $(App.view('translation/form')(isEditingTranslation: isEditingTranslation))
+
+    # Always default to current user's language.
+    locale = App.Locale.findByAttribute('locale', App.i18n.get())
+
+    defaults =
+      locale: locale.locale
+
+    # Prepare locale options for the target language field.
+    locale_options = _.reduce(App.Locale.all(), (acc, locale) ->
+      acc[locale.locale] = locale.name
+      acc
+    , {})
+
+    @form = new App.ControllerForm(
+      el: content.find('.js-form')
+      model:
+        name: 'translation'
+        configure_attributes: [
+          { name: 'source', display: __('Translation Source'), tag: 'textarea', rows: 3, null: false, disabled: isEditingTranslation, item_class: 'formG^p--halfSize' },
+          { name: 'target', display: __('Custom Translation'), tag: 'textarea', rows: 3, null: false, item_class: 'formGroup--halfSize' },
+          { name: 'locale', display: __('Target Language'),    tag: 'searchable_select', options: locale_options, null: true, disabled: isEditingTranslation },
+        ]
+      params: @data || defaults
     )
-    @ajax(
-      id:          'translations'
-      type:        'POST'
-      url:         "#{@apiPath}/translations/reset"
-      data:        JSON.stringify(locale: @locale)
-      processData: false
-      success: (data, status, xhr) =>
-        App.Event.trigger('i18n:translation_update')
-        @hideAction()
-        @loader.hide()
-      error: =>
-        @loader.hide()
-    )
-
-  updateOnServer: (params, event) =>
-
-    # update runtime if same language is used
-    if App.i18n.get() is params.locale
-      App.i18n.setMap(params.source, params.target)
 
-    # remove not needed attributes
-    delete params.field
+    callback = (data) =>
+      @objects = data.items
+      table = @suggestionsTable()
+      @el.find('.js-suggestionsTable').html(table.el)
+      return if data.items.length >= data.total_count
+      translatedMessageText = App.i18n.translateContent('The limit of displayable suggestions was reached, please narrow down your search.')
+      $('<div />')
+        .addClass('centered text-small text-muted')
+        .text(translatedMessageText)
+        .appendTo(@el.find('.js-suggestionsTable'))
 
-    if params.id
-      if params.target is ''
-        method = 'DELETE'
-        url    = "#{@apiPath}/translations/#{params.id}"
-      else
-        method = 'PUT'
-        url    = "#{@apiPath}/translations/#{params.id}"
-    else
-      method = 'POST'
-      url    = "#{@apiPath}/translations"
-
-    @ajax(
-      id:          'translations'
-      type:        method
-      url:         url
-      data:        JSON.stringify(params)
-      processData: false
-      success: (data, status, xhr) =>
-        if event
-          App.Event.trigger(event)
-        @toggleAction()
-    )
+    # Set up change handler on the locale selection and trigger refresh of suggestions.
+    content.find('[name="locale"]')
+      .off('change.loadSuggestions')
+      .on('change.loadSuggestions', (e) =>
+        console.debug('change.loadSuggestions', e.target)
+        @loadSuggestions($(e.target).val(), $('.js-suggestionsSearchInput').val(), callback)
+      )
 
-  getAttributes: (e) =>
-    field  = $(e.target).closest('tr').find('.js-Item')
-    params =
-      id:      field.data('id')
-      source:  field.data('source')
-      initial: field.data('initial') || ''
-      target:  field.val()
-      locale:  @locale
-      field:   field
-
-class TranslationToDo extends App.Controller
-  events:
-    'click .js-create':  'create'
-    'click .js-theSame': 'same'
+    debouncedLoadSuggestions = _.debounce(@loadSuggestions, 300)
 
-  constructor: ->
-    super
+    # Set up input handler on the search input and trigger refresh of suggestions.
+    content.find('.js-suggestionsSearchInput')
+      .off('input.loadSuggestions')
+      .on('input.loadSuggestions', (e) -> debouncedLoadSuggestions($('[name="locale"]').val(), $(e.target).val(), callback))
 
-  update: (data) =>
-    for key, value of data
-      @[key] = value
-    @render()
+    @el.find('.modal-body').html(content)
 
-  render: =>
+    if isEditingTranslation is false
+      @loadSuggestions(locale.locale, '', callback)
 
-    if _.isEmpty(@stringsNotTranslated)
-      @html ''
-      return
+  loadSuggestions: (locale, query, callback) =>
+    @el.find('.js-suggestionsLoader').removeClass('hide')
 
-    @html App.view('translation/todo')(
-      list: @stringsNotTranslated
+    @ajax(
+      id: 'translation_suggestions'
+      type: 'GET'
+      url: "#{@apiPath}/translations/search/#{locale}?query=#{encodeURIComponent(query)}"
+      processData: true
+      success: (data) =>
+        @el.find('.js-suggestionsCounter').text(data.total_count)
+        @el.find('.js-suggestionsCounterContainer').removeClass('hide')
+        @el.find('.js-suggestionsLoader').addClass('hide')
+        callback(data)
     )
 
-  create: (e) =>
-    e.preventDefault()
-    params = @getAttributes(e)
-    return if !params.target
-
-    # remove from not translated list
-    $(e.target).closest('tr').remove()
+  suggestionsTable: =>
+    typeCallback = (value, object, attribute, attributes) ->
+      return App.i18n.translateContent('system') if object.is_synchronized_from_codebase
+      App.i18n.translateContent('custom')
+
+    new App.ControllerTable(
+      class: 'table-hover-in-modal'
+      overviewAttributes: ['source', 'target_initial', 'type']
+      attribute_list: [
+        { name: 'source', display: __('Translation Source') },
+        { name: 'target_initial', display: __('Original Translation') },
+        { name: 'type', display: __('Type'), width: '50px' },
+      ]
+      objects: @objects
+      radio: true
+      bindRow:
+        events:
+          'click': @suggestionRowClick
+      callbackAttributes:
+        type: [typeCallback]
+    )
 
-    # remote update
-    params.target_initial = ''
-    @updateOnServer(params, 'i18n:translation_update_list')
+  suggestionRowClick: (id, e) =>
+    $(e.target).parents('tr').find('input[name="radio"]').prop('checked', true)
+    object = _.find(@objects, (object) -> object.id is id)
+    @acceptSuggestion(object)
 
-  same: (e) =>
-    e.preventDefault()
-    @hasChanges = true
-    params = @getAttributes(e)
+  acceptSuggestion: (object) =>
+    $('[name="source"]').val(object.source)
+    $('[name="target"]').prop('placeholder', object.target_initial).val('').focus()
 
-    # remove from not translated list
-    $(e.target).closest('tr').remove()
+    @handleContributionAlert(object.is_synchronized_from_codebase)
 
-    # remote update
-    params.target_initial = ''
-    params.target = params.source
-    @updateOnServer(params, 'i18n:translation_update_list')
+  handleContributionAlert: (display = false) =>
+    alert = @el.find('.js-contribution-alert')
 
-class TranslationList extends App.Controller
-  hasChanges: false
-  events:
-    'blur .js-translated input':      'updateItem'
-    'click .js-translated .js-Reset': 'resetItem'
-
-  constructor: ->
-    super
-
-  update: (data) =>
-    for key, value of data
-      @[key] = value
-    @render()
+    if not display
+      alert.remove()
+      return
 
-  render: =>
-    return if _.isEmpty(@stringsTranslated) && _.isEmpty(@times)
-    @html App.view('translation/list')(
-      times:                @times
-      strings:              @stringsTranslated
-    )
+    return if alert.length
+
+    $('<div />')
+      .attr('role', 'alert')
+      .addClass('alert')
+      .addClass('alert--warning')
+      .addClass('js-contribution-alert')
+      .html(App.i18n.translateContent('Did you know that system translations can be contributed and shared with the community on our public platform %l? It sports a very convenient user interface based on Weblate, give it a try!', 'https://translations.zammad.org'))
+      .appendTo(@el.find('.modal-alerts-container'))
+
+  onSubmit: (e) =>
+    params = @formParam(e.target)
+    error = @form.validate(params)
+
+    if error
+      @formValidate(form: e.target, errors: error)
+      return false
+
+    @formDisable(e)
+
+    @ajax({
+      id: 'translation_upsert',
+      type: 'POST',
+      url: "#{@apiPath}/translations/upsert",
+      data: JSON.stringify(params),
+      processData: true,
+      success: =>
+        @close()
+        @successCallback()
+
+        # Show toast if neeeded (means that in string inside the current user language was changed).
+        # Later we should add real subscription handling for this situation in the new tech stack.
+        currentLocale = App.i18n.get()
+        if params.locale == currentLocale
+          @notify(
+            type: 'success'
+            msg:  App.i18n.translateContent('To see the updated translation, please reload your browser.')
+          )
 
-  changes: =>
-    @hasChanges
-
-  resetItem: (e) ->
-    e.preventDefault()
-    @hasChanges = true
-    params = @getAttributes(e)
-
-    # remote reset
-    params.target = params.initial
-    @updateOnServer(params, 'i18n:translation_update')
-
-  updateItem: (e) ->
-    e.preventDefault()
-    @hasChanges = true
-    params = @getAttributes(e)
-    return if !params.target
-
-    # local update
-    @updateRow(params.id)
-
-    # remote update
-    @updateOnServer(params)
-
-  updateRow: (id) =>
-    field   = @$("[data-id=#{id}]")
-    current = field.val()
-    initial = field.data('initial')
-    reset   = field.closest('tr').find('.js-Reset')
-    if current isnt initial
-      @changesAvailable = true
-      reset.removeClass('hidden')
-      reset.closest('tr').addClass('warning')
-    else
-      reset.addClass('hidden')
-      reset.closest('tr').removeClass('warning')
+    })
 
-App.Config.set('Translation', { prio: 1800, parent: '#system', name: __('Translations'), target: '#system/translation', controller: Translation, permission: ['admin.translation'] }, 'NavBarAdmin' )
+App.Config.set('Translation', {
+  prio: 1800,
+  parent: '#system',
+  name: __('Translations'),
+  target: '#system/translation',
+  controller: Translation,
+  permission: ['admin.translation']
+}, 'NavBarAdmin')

+ 1 - 1
app/assets/javascripts/app/models/locale.coffee

@@ -1,4 +1,4 @@
 class App.Locale extends App.Model
   @configure 'Locale', 'name', 'alias', 'locale'
   @extend Spine.Model.Ajax
-  @url: @apiPath + '/locales'
+  @url: @apiPath + '/locales'

+ 1 - 1
app/assets/javascripts/app/models/translation.coffee

@@ -1,4 +1,4 @@
 class App.Translation extends App.Model
   @configure 'Translation', 'source', 'target', 'target_initial', 'locale'
   @extend Spine.Model.Ajax
-  @url: @apiPath + '/translations'
+  @url: @apiPath + '/translations'

+ 14 - 0
app/assets/javascripts/app/views/translation/description.jst.eco

@@ -0,0 +1,14 @@
+<p><%- @T('Here you can override any translation string for a more customized look and feel. Both recognized system and custom strings are supported, simply add a new translation in order to identify the source string and provide its new translation for the target language.') %></p>
+
+<div>
+  <h2><%- @T('Contributing Translations') %></h2>
+  <p><%- @T('System translations can be contributed and shared with the community on our public platform %l. It sports a very convenient user interface based on Weblate, give it a try!', 'https://translations.zammad.org') %></p>
+</div>
+
+<div>
+  <h2><%- @T('On-screen Translation') %></h2>
+  <p><%- @T('To make translations easier you can enable and disable the inline translation feature by pressing "%s" at any time.', @inlineTranslationKey ) %></p>
+  <p><%- @T('Text with disabled inline translations looks like') %> <button class="btn btn-primary u-unclickable"><%- @Ti('Some Text') %></button></p>
+  <p><%- @T('Text with enabled inline translations looks like') %> <button class="btn btn-primary u-unclickable"><span class="translation"><%- @Ti('Some Text') %></button></span></p>
+  <p><%- @T('Just click into the highlighted area and update the words right there. Enjoy!') %></p>
+</div>

+ 12 - 0
app/assets/javascripts/app/views/translation/form.jst.eco

@@ -0,0 +1,12 @@
+<div>
+  <div class="js-form"></div>
+    <% if @isEditingTranslation is false: %>
+      <div class="js-suggestions">
+        <h3><%- @T('Find translation suggestion') %><span class="subtitle js-suggestionsCounterContainer hide"> (<span class="js-suggestionsCounter">?</span> <%- @T('matches found') %>)</span> <span class="tiny loading icon js-suggestionsLoader hide" style="margin-left: 3px;"></span></h3>
+          <div class="form-group">
+            <input class="form-control js-suggestionsSearchInput" type="text" placeholder="<%= @T('Search…') %>">
+          </div>
+          <div class="scroll-table-container js-suggestionsTable"></div>
+      </div>
+    <% end %>
+</div>

+ 12 - 26
app/assets/javascripts/app/views/translation/index.jst.eco

@@ -1,30 +1,16 @@
-<div class="page-header">
-  <div class="page-header-title">
-    <h1><%- @T('Translations') %> <small><%= @currentLanguage %></small></h1>
-  </div>
-  <div class="page-header-meta">
-    <a class="btn btn--danger hidden js-changes js-resetChanges"><%- @T('Reset changes') %></a>
-  </div>
-</div>
-<div class="page-content">
-
-  <div class="box box--message">
-    <h2><%- @T('Contributing Translations') %></h2>
-    <p><%- @T('Starting with Zammad 5.1, translations can be contributed exclusively via "translations.zammad.org" %l.', 'https://translations.zammad.org') %></p>
-    <p>
-      <%- @T('While it will be no longer possible to directly push changed translations from Zammad, they can be contributed in a very convenient user interface based on Weblate.') %>
-      </p>
-  </div>
+<div>
+  <div class="page-header">
+    <div class="page-header-title">
+      <h1><%- @T('Translations') %> <small><%- @T('Management') %></small></h1>
+    </div>
 
-  <div class="box box--message">
-    <h2><%- @T('Inline translation') %></h2>
-    <p><%- @T('To make translations easier you can enable and disable the inline translation feature by pressing "%s".', @inlineTranslationKey ) %></p>
-    <p><%- @T('Text with disabled inline translations looks like') %> <button class="btn btn-primary"><%- @Ti('Some Text') %></button></p>
-    <p><%- @T('Text with enabled inline translations looks like') %> <button class="btn btn-primary"><span class="translation" contenteditable="true"><%- @Ti('Some Text') %></button></span></p>
-    <p><%- @T('Just click into the highlighted area and update the words right there. Enjoy!') %></p>
-    <p><%- @T('If you want to translate it via the translation table, just go ahead below.') %></p>
+    <div class="page-header-meta">
+    <% if @hasDescriptionButton: %>
+      <button class="btn js-description"><%- @T('Description') %></button>
+    <% end %>
+      <button class="btn btn--success js-new-translation"><%- @T('New Translation') %></button>
+    </div>
   </div>
 
-  <div class="js-ToDo"></div>
-  <div class="js-List"></div>
+  <div class="page-content js-content-container"></div>
 </div>

+ 0 - 45
app/assets/javascripts/app/views/translation/list.jst.eco

@@ -1,45 +0,0 @@
-<h2><%- @T('Date & Time') %></h2>
-<table class="translationOverview js-translated table table-striped table-hover">
-  <thead>
-    <tr>
-      <th class="translationOverview-source"><%- @T('Type') %></th>
-      <th class="translationOverview-target"><%- @T('Target') %></th>
-      <th class="translationOverview-initial"><%- @T('Original') %></th>
-      <th class="translationOverview-action"><%- @T('Action') %></th>
-    </tr>
-  </thead>
-  <tbody>
-  <% for time in @times: %>
-    <% changed = false %>
-    <% changed = true if time[2] isnt time[3] %>
-    <tr <% if changed: %>class="warning"<% end %>>
-      <td title="<%= time[1] %>"><%= time[1] %>
-      <td class="translationOverview-itemContainer"><input class="js-Item translationOverview-item form-control" value="<%= time[2] %>" data-source="<%= time[1] %>" data-initial="<%= time[3] %>" data-id="<%= time[0] %>" data-format="<%= time[4] %>">
-      <td title="<%= time[3] %>"><%= time[3]%>
-      <td><a href="#" class="js-Reset btn btn--text<% if !changed: %> hidden<% end %>"><%- @T('Reset') %></a>
-  <% end %>
-  </tbody>
-</table>
-
-<h2><%- @T('Words') %></h2>
-<table class="translationOverview js-translated table table-striped table-hover">
-  <thead>
-    <tr>
-      <th class="translationOverview-source"><%- @T('Source') %></th>
-      <th class="translationOverview-target"><%- @T('Target') %></th>
-      <th class="translationOverview-initial"><%- @T('Original') %></th>
-      <th class="translationOverview-action"><%- @T('Action') %></th>
-    </tr>
-  </thead>
-  <tbody>
-    <% for item in @strings: %>
-    <% changed = false %>
-    <% changed = true if item[2] isnt item[3] %>
-    <tr <% if changed: %>class="warning"<% end %>>
-      <td class="noTruncate" title="<%= item[1] %>"><%= item[1] %>
-      <td class="translationOverview-itemContainer"><input class="js-Item translationOverview-item form-control" value="<%= item[2] %>" data-source="<%= item[1] %>" data-initial="<%= item[3] %>" data-id="<%= item[0] %>" data-format="<%= item[4] %>">
-      <td class="noTruncate" title="<%= item[3] %>"><%= item[3]%>
-      <td><a href="#" class="js-Reset btn btn--text<% if !changed: %> hidden<% end %>"><%- @T('Reset') %></a>
-    <% end %>
-  </tbody>
-</table>

+ 0 - 18
app/assets/javascripts/app/views/translation/todo.jst.eco

@@ -1,18 +0,0 @@
-<h2><%- @T('Words:') %> <%- @T('not translated') %></h2>
-<table class="translationOverview table table-striped table-hover">
-  <thead>
-    <tr>
-      <th class="translationOverview-source"><%- @T('Source') %></th>
-      <th class="translationOverview-target"><%- @T('Target') %></th>
-      <th class="translationOverview-action"><%- @T('Action') %></th>
-    </tr>
-  </thead>
-  <tbody>
-    <% for item in @list: %>
-    <tr>
-      <td title="<%= item[1] %>"><%= item[1] %>
-      <td class="translationOverview-itemContainer"><input class="js-Item translationOverview-item form-control" value="<%= item[2] %>" data-source="<%= item[1] %>" data-id="<%= item[0] %>">
-      <td><a href="#" class="js-create btn btn--text"><%- @T('Create') %></a> / <a href="#" class="js-theSame btn btn--text"><%- @T('is the same') %></a>
-    <% end %>
-  </tbody>
-</table>

+ 13 - 0
app/assets/stylesheets/zammad.scss

@@ -1457,6 +1457,19 @@ table {
   table-layout: fixed;
 }
 
+.scroll-table-container {
+  overflow: hidden;
+  overflow-y: scroll;
+  max-height: 244px;
+
+  table > thead > tr > th {
+    @extend .zIndex-2;
+
+    top: 0;
+    position: sticky;
+  }
+}
+
 .table {
   display: table;
 

Некоторые файлы не были показаны из-за большого количества измененных файлов