提交 8f9442fc 编写于 作者: 🌴 🌴🇿🇦 Lukas Eipert (OOO until Summit) 提交者: Mike Greiling

Frontend: Proper gettext extraction with gettext-extractor

上级 76cd1dd6
......@@ -87,7 +87,7 @@ class ImporterStatus {
details = error.response.data.errors;
}
flash(__(`An error occurred while importing project: ${details}`));
flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
}
......
/* eslint-disable import/no-commonjs */
const SPLIT_REGEX = /\s*[\r\n]+\s*/;
/**
*
* strips newlines from strings and replaces them with a single space
*
* @example
*
* ensureSingleLine('foo \n bar') === 'foo bar'
*
* @param {String} str
* @returns {String}
*/
module.exports = function ensureSingleLine(str) {
// This guard makes the function significantly faster
if (str.includes('\n') || str.includes('\r')) {
return str
.split(SPLIT_REGEX)
.filter(s => s !== '')
.join(' ');
}
return str;
};
import Jed from 'jed';
import ensureSingleLine from './ensure_single_line';
import sprintf from './sprintf';
const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en';
......@@ -10,7 +11,7 @@ delete window.translations;
@param text The text to be translated
@returns {String} The translated text
*/
const gettext = locale.gettext.bind(locale);
const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text));
/**
Translate the text with a number
......@@ -23,7 +24,10 @@ const gettext = locale.gettext.bind(locale);
@returns {String} Translated text with the number replaced (eg. '2 days')
*/
const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
const translated = locale
.ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count)
.replace(/%d/g, count)
.split('|');
return translated[translated.length - 1];
};
......@@ -40,7 +44,7 @@ const ngettext = (text, pluralText, count) => {
@returns {String} Translated context based text
*/
const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext);
const translated = gettext(normalizedKey).split('|');
return translated[translated.length - 1];
......@@ -52,8 +56,7 @@ const pgettext = (keyOrContext, key) => {
@param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
@returns {Intl.DateTimeFormat}
*/
const createDateTimeFormat =
formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
const createDateTimeFormat = formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
export { languageCode };
export { gettext as __ };
......
<script>
import { __ } from '~/locale';
import $ from 'jquery';
import eventHub from '../../event_hub';
......@@ -17,7 +18,7 @@ export default {
computed: {
buttonText() {
return this.isLocked ? this.__('Unlock') : this.__('Lock');
return this.isLocked ? __('Unlock') : __('Lock');
},
toggleLock() {
......
<script>
import { __ } from '~/locale';
import { __, sprintf } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
......@@ -79,11 +79,9 @@ export default {
.then(() => window.location.reload())
.catch(() =>
Flash(
this.__(
`Something went wrong trying to change the locked state of this ${
this.issuableDisplayName
}`,
),
sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), {
issuableDisplayName: this.issuableDisplayName,
}),
),
);
},
......
require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
require 'json'
VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/
......@@ -36,6 +37,20 @@ module GettextI18nRailsJs
".vue"
].include? ::File.extname(file)
end
def collect_for(file)
gettext_messages_by_file[file] || []
end
private
def gettext_messages_by_file
@gettext_messages_by_file ||= JSON.parse(load_messages)
end
def load_messages
`node scripts/frontend/extract_gettext_all.js --all`
end
end
end
end
......
......@@ -235,6 +235,9 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
msgstr ""
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
......@@ -346,6 +349,12 @@ msgstr ""
msgid "Admin area"
msgstr ""
msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "AdminArea|Stop all jobs"
msgstr ""
......@@ -364,6 +373,9 @@ msgstr ""
msgid "AdminHealthPageLink|health page"
msgstr ""
msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered."
msgstr ""
msgid "AdminProjects|Delete"
msgstr ""
......@@ -499,7 +511,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
msgid "An error occurred while importing project: ${details}"
msgid "An error occurred while importing project: %{details}"
msgstr ""
msgid "An error occurred while loading commit signatures"
......@@ -805,6 +817,9 @@ msgstr ""
msgid "Badges|This project has no badges"
msgstr ""
msgid "Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored."
msgstr ""
msgid "Badges|Your badges"
msgstr ""
......@@ -1248,6 +1263,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
msgstr ""
msgid "ClusterIntegration|API URL"
msgstr ""
......@@ -1257,12 +1275,21 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}"
msgstr ""
msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
msgstr ""
msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
msgstr ""
msgid "ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}"
msgstr ""
msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
msgstr ""
msgid "ClusterIntegration|Applications"
msgstr ""
......@@ -1329,6 +1356,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
msgid "ClusterIntegration|GitLab Runner connects to this project's repository and executes CI/CD jobs, pushing results back and deploying, applications to production."
msgstr ""
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
......@@ -1341,6 +1371,9 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts."
msgstr ""
msgid "ClusterIntegration|Hide"
msgstr ""
......@@ -1350,9 +1383,15 @@ msgstr ""
msgid "ClusterIntegration|Ingress IP Address"
msgstr ""
msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}"
msgstr ""
msgid "ClusterIntegration|Installed"
msgstr ""
......@@ -1371,6 +1410,9 @@ msgstr ""
msgid "ClusterIntegration|JupyterHub"
msgstr ""
msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
msgstr ""
msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
......@@ -1458,6 +1500,9 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
......@@ -1467,6 +1512,9 @@ msgstr ""
msgid "ClusterIntegration|Prometheus"
msgstr ""
msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
......@@ -1479,6 +1527,9 @@ msgstr ""
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
......@@ -1533,6 +1584,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
msgstr ""
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
msgstr ""
......@@ -1551,6 +1605,9 @@ msgstr ""
msgid "ClusterIntegration|Validating project billing status"
msgstr ""
msgid "ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again."
msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
......@@ -1963,6 +2020,9 @@ msgstr ""
msgid "Cycle Analytics"
msgstr ""
msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
msgstr ""
msgid "CycleAnalyticsStage|Code"
msgstr ""
......@@ -2361,6 +2421,9 @@ msgstr ""
msgid "Environments|Environments"
msgstr ""
msgid "Environments|Environments are places where code gets deployed, such as staging or production."
msgstr ""
msgid "Environments|Job"
msgstr ""
......@@ -2373,6 +2436,9 @@ msgstr ""
msgid "Environments|No deployments yet"
msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
msgstr ""
......@@ -3181,6 +3247,9 @@ msgstr ""
msgid "Job|The artifacts will be removed"
msgstr ""
msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it."
msgstr ""
msgid "Jul"
msgstr ""
......@@ -3262,6 +3331,9 @@ msgstr ""
msgid "Labels|Promote Label"
msgstr ""
msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed."
msgstr ""
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] ""
......@@ -3321,6 +3393,11 @@ msgstr ""
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
msgstr ""
msgid "Limited to showing %d event at most"
msgid_plural "Limited to showing %d events at most"
msgstr[0] ""
msgstr[1] ""
msgid "LinkedIn"
msgstr ""
......@@ -3357,6 +3434,9 @@ msgstr ""
msgid "Lock not found"
msgstr ""
msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment."
msgstr ""
msgid "Lock to current projects"
msgstr ""
......@@ -3483,6 +3563,9 @@ msgstr ""
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
msgstr ""
msgid "Merged"
msgstr ""
......@@ -3531,6 +3614,12 @@ msgstr ""
msgid "Milestones"
msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
msgstr ""
msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project. %{milestoneTitle} is not currently used in any issues or merge requests."
msgstr ""
msgid "Milestones|Delete milestone"
msgstr ""
......@@ -3549,6 +3638,9 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged. This action cannot be reversed."
msgstr ""
msgid "Mirror a repository"
msgstr ""
......@@ -3719,6 +3811,9 @@ msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
msgid "No container images stored for this project. Add one by following the instructions above."
msgstr ""
msgid "No due date"
msgstr ""
......@@ -3764,6 +3859,12 @@ msgstr ""
msgid "None"
msgstr ""
msgid "Not all comments are displayed because you're comparing two versions of the diff."
msgstr ""
msgid "Not all comments are displayed because you're viewing an old version of the diff."
msgstr ""
msgid "Not allowed to merge"
msgstr ""
......@@ -3875,6 +3976,11 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
msgid "One more item"
msgid_plural "%d more items"
msgstr[0] ""
msgstr[1] ""
msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
......@@ -3983,6 +4089,9 @@ msgstr ""
msgid "Pending"
msgstr ""
msgid "People without permission will never get a notification and won't be able to comment."
msgstr ""
msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
......@@ -4085,6 +4194,9 @@ msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
msgstr ""
msgid "Pipelines|Get started with Pipelines"
msgstr ""
......@@ -4106,6 +4218,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
msgstr ""
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
......@@ -4220,6 +4335,12 @@ msgstr ""
msgid "Profile Settings"
msgstr ""
msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
msgstr ""
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
msgstr ""
msgid "Profiles|Account scheduled for removal."
msgstr ""
......@@ -4394,6 +4515,9 @@ msgstr ""
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
msgid "PrometheusDashboard|Time"
msgstr ""
......@@ -4520,6 +4644,11 @@ msgstr ""
msgid "Reference:"
msgstr ""
msgid "Refreshing in a second to show the updated status..."
msgid_plural "Refreshing in %d seconds to show the updated status..."
msgstr[0] ""
msgstr[1] ""
msgid "Register / Sign In"
msgstr ""
......@@ -4983,6 +5112,12 @@ msgstr ""
msgid "Something went wrong on our end. Please try again!"
msgstr ""
msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr ""
msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
msgstr ""
msgid "Something went wrong when toggling the button"
msgstr ""
......@@ -5321,6 +5456,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
msgstr ""
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
......@@ -5351,6 +5489,9 @@ msgstr ""
msgid "The phase of the development lifecycle."
msgstr ""
msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
msgstr ""
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
......@@ -5453,6 +5594,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
msgid "This branch has changed since you started editing. Would you like to create a new branch?"
msgstr ""
msgid "This diff is collapsed."
msgstr ""
......@@ -5504,6 +5648,12 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:"
msgstr ""
msgid "This job is stuck, because you don't have any active runners that can run this job."
msgstr ""
msgid "This job requires a manual action"
msgstr ""
......@@ -5513,6 +5663,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
msgid "This option is disabled as you don't have write permissions for the current branch"
msgstr ""
msgid "This option is disabled while you still have unstaged changes"
msgstr ""
......@@ -5528,6 +5681,9 @@ msgstr ""
msgid "This project does not belong to a group and can therefore not make use of group Runners."
msgstr ""
msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
msgstr ""
msgid "This repository"
msgstr ""
......@@ -5820,6 +5976,9 @@ msgstr ""
msgid "Unlock"
msgstr ""
msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
msgstr ""
msgid "Unlocked"
msgstr ""
......@@ -6333,6 +6492,12 @@ msgstr ""
msgid "command line instructions"
msgstr ""
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr ""
msgid "confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue."
msgstr ""
msgid "connecting"
msgstr ""
......@@ -6436,6 +6601,9 @@ msgstr ""
msgid "mrWidget|Failed to load deployment statistics"
msgstr ""
msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
msgstr ""
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr ""
......@@ -6463,9 +6631,15 @@ msgstr ""
msgid "mrWidget|Open in Web IDE"
msgstr ""
msgid "mrWidget|Pipeline blocked. The pipeline for this merge request requires a manual action to proceed"
msgstr ""
msgid "mrWidget|Plain diff"
msgstr ""
msgid "mrWidget|Ready to be merged automatically. Ask someone with write access to this repository to merge this request"
msgstr ""
msgid "mrWidget|Refresh"
msgstr ""
......@@ -6487,6 +6661,9 @@ msgstr ""
msgid "mrWidget|Resolve conflicts"
msgstr ""
msgid "mrWidget|Resolve these conflicts or ask someone with write access to this repository to merge it locally"
msgstr ""
msgid "mrWidget|Revert"
msgstr ""
......@@ -6505,6 +6682,12 @@ msgstr ""
msgid "mrWidget|The changes will be merged into"
msgstr ""
msgid "mrWidget|The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure"
msgstr ""
msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging"
msgstr ""
msgid "mrWidget|The source branch has been removed"
msgstr ""
......
const argumentsParser = require('commander');
const { GettextExtractor, JsExtractors } = require('gettext-extractor');
const {
decorateJSParserWithVueSupport,
decorateExtractorWithHelpers,
} = require('gettext-extractor-vue');
const ensureSingleLine = require('../../app/assets/javascripts/locale/ensure_single_line.js');
const arguments = argumentsParser
.option('-f, --file <file>', 'Extract message from one single file')
.option('-a, --all', 'Extract message from all js/vue files')
.parse(process.argv);
const extractor = decorateExtractorWithHelpers(new GettextExtractor());
extractor.addMessageTransformFunction(ensureSingleLine);
const jsParser = extractor.createJsParser([
// Place all the possible expressions to extract here:
JsExtractors.callExpression('__', {
arguments: {
text: 0,
},
}),
JsExtractors.callExpression('n__', {
arguments: {
text: 0,
textPlural: 1,
},
}),
JsExtractors.callExpression('s__', {
arguments: {
text: 0,
},
}),
]);
const vueParser = decorateJSParserWithVueSupport(jsParser);
function printJson() {
const messages = extractor.getMessages().reduce((result, message) => {
let text = message.text;
if (message.textPlural) {
text += `\u0000${message.textPlural}`;
}
message.references.forEach(reference => {
const filename = reference.replace(/:\d+$/, '');
if (!Array.isArray(result[filename])) {
result[filename] = [];
}
result[filename].push([text, reference]);
});
return result;
}, {});
console.log(JSON.stringify(messages));
}
if (arguments.file) {
vueParser.parseFile(arguments.file).then(() => printJson());
} else if (arguments.all) {
vueParser.parseFilesGlob('{ee/app,app}/assets/javascripts/**/*.{js,vue}').then(() => printJson());
} else {
console.warn('ERROR: Please use the script correctly:');
arguments.outputHelp();
process.exit(1);
}
import ensureSingleLine from '~/locale/ensure_single_line';
describe('locale', () => {
describe('ensureSingleLine', () => {
it('should remove newlines at the start of the string', () => {
const result = 'Test';
expect(ensureSingleLine(`\n${result}`)).toBe(result);
expect(ensureSingleLine(`\t\n\t${result}`)).toBe(result);
expect(ensureSingleLine(`\r\n${result}`)).toBe(result);
expect(ensureSingleLine(`\r\n ${result}`)).toBe(result);
expect(ensureSingleLine(`\r ${result}`)).toBe(result);
expect(ensureSingleLine(` \n ${result}`)).toBe(result);
});
it('should remove newlines at the end of the string', () => {
const result = 'Test';
expect(ensureSingleLine(`${result}\n`)).toBe(result);
expect(ensureSingleLine(`${result}\t\n\t`)).toBe(result);
expect(ensureSingleLine(`${result}\r\n`)).toBe(result);
expect(ensureSingleLine(`${result}\r`)).toBe(result);
expect(ensureSingleLine(`${result} \r`)).toBe(result);
expect(ensureSingleLine(`${result} \r\n `)).toBe(result);
});
it('should replace newlines in the middle of the string with a single space', () => {
const result = 'Test';
expect(ensureSingleLine(`${result}\n${result}`)).toBe(`${result} ${result}`);
expect(ensureSingleLine(`${result}\t\n\t${result}`)).toBe(`${result} ${result}`);
expect(ensureSingleLine(`${result}\r\n${result}`)).toBe(`${result} ${result}`);
expect(ensureSingleLine(`${result}\r${result}`)).toBe(`${result} ${result}`);
expect(ensureSingleLine(`${result} \r${result}`)).toBe(`${result} ${result}`);
expect(ensureSingleLine(`${result} \r\n ${result}`)).toBe(`${result} ${result}`);
});
});
});
......@@ -1104,7 +1104,7 @@ export const collapsedSystemNotes = [
resolvable: false,
noteable_iid: 12,
note: 'changed the description',
note_html: '\n <p dir="auto">changed the description 2 times within 1 minute </p>',
note_html: ' <p dir="auto">changed the description 2 times within 1 minute </p>',
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
......
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import Jed from 'jed';
Vue.use(Translate);
import locale from '~/locale';
import Translate from '~/vue_shared/translate';
import { trimText } from 'spec/helpers/vue_component_helper';
describe('Vue translate filter', () => {
let el;
const createTranslationMock = (key, ...translations) => {
const fakeLocale = new Jed({
domain: 'app',
locale_data: {
app: {
'': {
domain: 'app',
lang: 'vo',
plural_forms: 'nplurals=2; plural=(n != 1);',
},
[key]: translations,
},
},
});
// eslint-disable-next-line no-underscore-dangle
locale.__Rewire__('locale', fakeLocale);
};
afterEach(() => {
// eslint-disable-next-line no-underscore-dangle
locale.__ResetDependency__('locale');
});
beforeEach(() => {
Vue.use(Translate);
el = document.createElement('div');
document.body.appendChild(el);
});
it('translate single text', (done) => {
const comp = new Vue({
it('translate singular text (`__`)', done => {
const key = 'singular';
const translation = 'singular_translated';
createTranslationMock(key, translation);
const vm = new Vue({
el,
template: `
<span>
{{ __('${key}') }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
});
it('translate plural text (`n__`) without any substituting text', done => {
const key = 'plural';
const translationPlural = 'plural_multiple translation';
createTranslationMock(key, 'plural_singular translation', translationPlural);
const vm = new Vue({
el,
template: `
<span>
{{ __('testing') }}
{{ n__('${key}', 'plurals', 2) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(
comp.$el.textContent.trim(),
).toBe('testing');
expect(trimText(vm.$el.textContent)).toBe(translationPlural);
done();
});
});
it('translate plural text with single count', (done) => {
const comp = new Vue({
describe('translate plural text (`n__`) with substituting %d', () => {
const key = '%d day';
beforeEach(() => {
createTranslationMock(key, '%d singular translated', '%d plural translated');
});
it('and n === 1', done => {
const vm = new Vue({
el,
template: `
<span>
{{ n__('${key}', '%d days', 1) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(trimText(vm.$el.textContent)).toBe('1 singular translated');
done();
});
});
it('and n > 1', done => {
const vm = new Vue({
el,
template: `
<span>
{{ n__('${key}', '%d days', 2) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(trimText(vm.$el.textContent)).toBe('2 plural translated');
done();
});
});
});
describe('translates text with context `s__`', () => {
const key = 'Context|Foobar';
const translation = 'Context|Foobar translated';
const expectation = 'Foobar translated';
beforeEach(() => {
createTranslationMock(key, translation);
});
it('and using two parameters', done => {
const vm = new Vue({
el,
template: `
<span>
{{ s__('Context', 'Foobar') }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(trimText(vm.$el.textContent)).toBe(expectation);
done();
});
});
it('and using the pipe syntax', done => {
const vm = new Vue({
el,
template: `
<span>
{{ s__('${key}') }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(trimText(vm.$el.textContent)).toBe(expectation);
done();
});
});
});
it('translate multi line text', done => {
const translation = 'multiline string translated';
createTranslationMock('multiline string', translation);
const vm = new Vue({
el,
template: `
<span>
{{ n__('%d day', '%d days', 1) }}
{{ __(\`
multiline
string
\`) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(
comp.$el.textContent.trim(),
).toBe('1 day');
expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
});
it('translate plural text with multiple count', (done) => {
const comp = new Vue({
it('translate pluralized multi line text', done => {
const translation = 'multiline string plural';
createTranslationMock('multiline string', 'multiline string singular', translation);
const vm = new Vue({
el,
template: `
<span>
{{ n__('%d day', '%d days', 2) }}
{{ n__(
\`
multiline
string
\`,
\`
multiline
strings
\`,
2
) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(
comp.$el.textContent.trim(),
).toBe('2 days');
expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
});
it('translate plural without replacing any text', (done) => {
const comp = new Vue({
it('translate pluralized multi line text with context', done => {
const translation = 'multiline string with context';
createTranslationMock('Context| multiline string', translation);
const vm = new Vue({
el,
template: `
<span>
{{ n__('day', 'days', 2) }}
{{ s__(
\`
Context|
multiline
string
\`
) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
expect(
comp.$el.textContent.trim(),
).toBe('days');
expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
......
......@@ -94,10 +94,34 @@
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
"@types/events@*":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
"@types/glob@^5":
version "5.0.35"
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a"
dependencies:
"@types/events" "*"
"@types/minimatch" "*"
"@types/node" "*"
"@types/jquery@^2.0.40":
version "2.0.48"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
"@types/minimatch@*":
version "3.0.3"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
"@types/node@*":
version "10.5.2"
resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
"@types/parse5@^5":
version "5.0.0"
resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11"
"@vue/component-compiler-utils@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6"
......@@ -2099,6 +2123,10 @@ css-loader@^1.0.0:
postcss-value-parser "^3.3.0"
source-list-map "^2.0.0"
css-selector-parser@^1.3:
version "1.3.0"
resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
css-selector-tokenizer@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
......@@ -3520,6 +3548,26 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
gettext-extractor-vue@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.1.tgz#69d2737eb8f1938803ffcf9317133ed59fb2372f"
dependencies:
bluebird "^3.5.1"
glob "^7.1.2"
vue-template-compiler "^2.5.0"
gettext-extractor@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.3.2.tgz#d5172ba8d175678bd40a5abe7f908fa2a9d9473b"
dependencies:
"@types/glob" "^5"
"@types/parse5" "^5"
css-selector-parser "^1.3"
glob "5 - 7"
parse5 "^5"
pofile "^1"
typescript "^2"
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
......@@ -3527,24 +3575,24 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "2 || 3"
minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
glob@^5.0.15:
version "5.0.15"
resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^3.0.4"
minimatch "2 || 3"
once "^1.3.0"
path-is-absolute "^1.0.0"
......@@ -5750,6 +5798,10 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
parse5@^5:
version "5.0.0"
resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.0.0.tgz#4d02710d44f3c3846197a11e205d4ef17842b81a"
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
......@@ -5888,6 +5940,10 @@ pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
pofile@^1:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
popper.js@^1.12.9, popper.js@^1.14.3:
version "1.14.3"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
......@@ -7460,6 +7516,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
typescript@^2:
version "2.9.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
uglify-es@^3.3.4:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
......@@ -7756,7 +7816,7 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-template-compiler@^2.5.16:
vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
dependencies:
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册