Skip to content

Strings in JS files cannot be externalized in form of __(`xxx`)

Summary

bin/rake gettext:find will detect the externalized strings in both backend and frontend. It calls gettext_i18n_rails_js for detecting the externalized strings in Javascript. However, only __('xxx') and __("xxx") can be detected in JS. Some files in app/assets/javascripts/ contain invalid forms of externalized strings, which cannot be detected and will not be presented in the gitlab.pot, so they cannot be translated later.

  • Backtick quote
    errorMsg.textContent = __(`
      Cannot show preview. For previews on sketch files, they must have the file format
      introduced by Sketch version 43 and above.
    `);

or

          <div slot="description">
            {{ s__(`ClusterIntegration|Helm streamlines installing
              and managing Kubernetes applications.
              Tiller runs inside of your Kubernetes Cluster,
              and manages releases of your charts.`) }}
          </div>
  • Put string in a variable
      message =
        this.items && this.items.length
          ? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
          : 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';

      return sprintf(
        s__(message),
        {
          docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
          docsLinkStart: `<a href="${_.escape(
            this.docsUrl,
          )}" target="_blank" rel="noopener noreferrer">`,
        },
        false,
      );
  • New line after __(
      return s__(
        'Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored.',
      );

All three above forms cannot be detected by gettext_i18n_rails_js, and I found many such cases in files under app/assets/javascripts.

$ egrep "[^a-z][a-z]__\(([^'\"]|$)" -R app/assets/javascripts
app/assets/javascripts/notes/stores/collapse_utils.js:    s__(`MergeRequest|
app/assets/javascripts/pipelines/components/empty_state.vue:            {{ s__(`Pipelines|Continuous Integration can help
app/assets/javascripts/pipelines/components/pipelines.vue:        :message="s__(`Pipelines|There was an error fetching the pipelines.
app/assets/javascripts/commit/pipelines/pipelines_table.vue:      :message="s__(`Pipelines|There was an error fetching the pipelines.
app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue:        {{ s__(`mrWidget|The pipeline for this merge request failed.
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue:        {{ s__(`mrWidget|Fast-forward merge is not possible.
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue:            {{ s__(`mrWidget|Resolve these conflicts or ask someone
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue:        {{ s__(`mrWidget|Ready to be merged automatically.
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue:        {{ s__(`mrWidget|Pipeline blocked.
app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue:      return n__(
app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue:        {{ s__(`mrWidget|The source branch HEAD has recently changed.
app/assets/javascripts/groups/components/group_folder.vue:      return n__(
app/assets/javascripts/groups/components/app.vue:      this.groupLeaveConfirmationMessage = s__(
app/assets/javascripts/clusters/components/applications.vue:          s__(
app/assets/javascripts/clusters/components/applications.vue:          s__(
app/assets/javascripts/clusters/components/applications.vue:          s__(
app/assets/javascripts/clusters/components/applications.vue:          s__(
app/assets/javascripts/clusters/components/applications.vue:            {{ s__(`ClusterIntegration|Helm streamlines installing
app/assets/javascripts/clusters/components/applications.vue:              {{ s__(`ClusterIntegration|Ingress gives you a way to route
app/assets/javascripts/clusters/components/applications.vue:                {{ s__(`ClusterIntegration|The IP address is in
app/assets/javascripts/clusters/components/applications.vue:                {{ s__(`ClusterIntegration|Point a wildcard DNS to this
app/assets/javascripts/clusters/components/applications.vue:            {{ s__(`ClusterIntegration|GitLab Runner connects to this
app/assets/javascripts/clusters/components/applications.vue:              {{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
app/assets/javascripts/clusters/components/applications.vue:                {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
app/assets/javascripts/badges/components/badge_settings.vue:      return s__(
app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue:        s__(
app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue:        s__(message),
app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue:          return s__(
app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue:          s__(
app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js:export const GCP_API_ERROR = s__(
app/assets/javascripts/cycle_analytics/components/limit_warning_component.vue:      :title="n__(
app/assets/javascripts/environments/components/empty_state.vue:        {{ s__(`Environments|Environments are places where
app/assets/javascripts/sidebar/components/confidential/edit_form.vue:      return s__(
app/assets/javascripts/sidebar/components/confidential/edit_form.vue:      return s__(
app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue:        const keepContributionsText = s__(`AdminArea|
app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue:        const deleteContributionsText = s__(`AdminArea|
app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue:        return sprintf(s__(`AdminProjects|
app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue:        return sprintf(s__(`Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}.
app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue:            s__(`Milestones|
app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue:          s__(`Milestones|
app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue:        return sprintf(s__(`Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. 
app/assets/javascripts/profile/account/components/update_username.vue:        s__(`Profiles|
app/assets/javascripts/profile/account/components/delete_account_modal.vue:          s__(`Profiles|
app/assets/javascripts/diffs/components/compare_versions_dropdown.vue:      return n__(

Or, the files can be detected via regular expression: [^a-z][a-z]__\(([^'"]|$).

This is a known issue to gettext_i18n_rails_js, https://github.com/webhippie/gettext_i18n_rails_js/issues/41.

To varify the issue, just find the above externalization form in .js or .vue file, and check whether locale/gitlab.pot contains the externalized strings or not.

Edited by Tao Wang