Merge branch 'housekeeping/locales' into dev

pull/4658/head
Jens Ulferts 8 years ago
commit f8d8b99f2c
  1. 1
      .gitignore
  2. 2
      .teatro.yml
  3. 1
      Gemfile
  4. 5
      Gemfile.lock
  5. 2
      app/assets/javascripts/application.js.erb
  6. 1
      app/views/layouts/base.html.erb
  7. 3
      config/environments/development.rb
  8. 4
      config/i18n-js.yml
  9. 1
      config/initializers/assets.rb
  10. 6
      doc/development/create-openproject-plugin.md
  11. 4
      frontend/app/components/wp-buttons/wp-buttons.module.ts
  12. 2
      frontend/app/global.js
  13. 15
      frontend/app/init-app.js
  14. 831
      frontend/app/vendor/i18n.js
  15. 4
      frontend/karma.conf.js
  16. 85
      frontend/npm-shrinkwrap.json
  17. 4
      frontend/package.json
  18. 2
      frontend/rails-plugins.conf.js
  19. 35
      frontend/tests/unit/lib/i18n-js.shim.js
  20. 6
      frontend/webpack-main-config.js
  21. 10
      lib/tasks/assets.rake
  22. 6
      lib/tasks/cucumber.rake
  23. 4
      lib/tasks/plugin_specs.rake
  24. 2
      lib/tasks/testing.rake

1
.gitignore vendored

@ -49,6 +49,7 @@ npm-debug.log*
/.project
/.loadpath
/app/assets/javascripts/bundles/*.*
/app/assets/javascripts/locales/*.*
/config/additional_environment.rb
/config/configuration.yml
/config/database.yml

@ -15,5 +15,5 @@ stage:
- bundle exec rake db:create db:migrate
- bundle exec rake db:seed RAILS_ENV=development
assets: bundle exec rake assets:webpack RAILS_ENV=development
assets: bundle exec rake assets:precompile RAILS_ENV=development
run: foreman start -f Procfile.dev -c all=1,assets=0

@ -122,6 +122,7 @@ gem 'sass-rails', '~> 5.0.3'
gem 'sass', '~> 3.4.12'
gem 'autoprefixer-rails'
gem 'bourbon', '~> 4.2.0'
gem 'i18n-js', '>= 3.0.0.rc13'
gem 'prototype-rails', git: 'https://github.com/rails/prototype-rails.git', branch: '4.2'
# remove once we no longer use the deprecated "link_to_remote", "remote_form_for" and alike methods

@ -70,7 +70,7 @@ GIT
GIT
remote: https://github.com/opf/openproject-translations.git
revision: 89b142d39dd4df38717bc1ba2ca9960ae4d0f766
revision: f331b3218ca082c6034568631e520e36043fa397
branch: dev
specs:
openproject-translations (5.1.0)
@ -306,6 +306,8 @@ GEM
http-cookie (1.0.2)
domain_name (~> 0.5)
i18n (0.7.0)
i18n-js (3.0.0.rc13)
i18n (~> 0.6, >= 0.6.6)
ice_nine (0.11.2)
interception (0.5)
ipaddress (0.8.3)
@ -623,6 +625,7 @@ DEPENDENCIES
gravatar_image_tag (~> 1.2.0)
health_check
htmldiff
i18n-js (>= 3.0.0.rc13)
jruby-openssl
json_spec
launchy

@ -28,6 +28,8 @@
//= require ./bundles/openproject-global
//= require i18n
//# require jquery_noconflict
//= require prototype
//= require effects

@ -42,6 +42,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render 'common/favicons' %>
<%= stylesheet_link_tag current_theme.stylesheet_manifest, media: "all" %>
<%= javascript_include_tag 'application' %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
<!-- user specific tags -->
<%= user_specific_javascript_includes %>
<!-- project specific tags -->

@ -35,6 +35,9 @@ OpenProject::Application.configure do
# since you don't have to restart the web server when you make code changes.
config.cache_classes = false
# Automatically refresh translations with I18n middleware
config.middleware.use ::I18n::JS::Middleware
# Do not eager load code on boot.
config.eager_load = false

@ -0,0 +1,4 @@
fallbacks: :en
translations:
- file: "app/assets/javascripts/locales/%{locale}.js"
only: '*.js'

@ -13,6 +13,7 @@ OpenProject::Application.configure do
date-de-DE.js
date-en-US.js
jstoolbar/lang/*.js
locales/*.js
members_form.js
members_select_boxes.js
new_user.js

@ -129,11 +129,7 @@ These plugins must contain a `package.json` in the root directory of the plugin.
Plugins are responsible for loading their own assets, including additional
images, styles and I18n translations.
To load translation strings use the provided `I18n.addTranslation` function:
```js
I18n.addTranslations('en', require('../../config/locales/js-en.yml').en);
```
Translations are processed by I18n.js through Rails and will be picked up from `config/locales/js-<locale>.js`.
Pure frontend plugins should be considered _a work in progress_. As such, it is
currently recommended to create hybrid plugins (see below).

@ -51,8 +51,8 @@ export abstract class WorkPackageButtonController {
this.text = {
activate: this.I18n.t('js.label_activate'),
deactivate: this.I18n.t('js.label_deactivate'),
label: this.I18n.t(this.labelKey),
buttonText: this.I18n.t(this.textKey)
label: this.labelKey ? this.I18n.t(this.labelKey) : '',
buttonText: this.textKey ? this.I18n.t(this.textKey) : ''
};
}

@ -34,8 +34,6 @@
// See: https://github.com/webpack/style-loader/issues/31
require('polyfill-function-prototype-bind');
require('i18n');
require('jquery');
require('jquery-migrate/jquery-migrate');
require('jquery-ujs');

@ -26,18 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details.
// ++
// standard locales
I18n.translations.en = require("locales/js-en.yml").en;
I18n.addTranslations = function(locale, translations) {
if (I18n.translations[locale] === undefined) {
I18n.translations[locale] = translations;
}
else {
I18n.translations[locale] = _.merge(I18n.translations[locale], translations);
}
};
require('angular-animate');
require('angular-aria');
require('angular-modal');
@ -95,9 +83,6 @@ opApp
angular.element('body').attr('global-drag-and-drop-handler','');
}
])
.value('cgBusyDefaults', {
message: I18n.t('js.label_please_wait')
})
.run([
'$http',
'$rootScope',

@ -1,831 +0,0 @@
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 3.
//
// OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
// Copyright (C) 2006-2013 Jean-Philippe Lang
// Copyright (C) 2010-2013 the ChiliProject Team
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
//++
// Extracted from https://github.com/fnando/i18n-js
// Commit: 839979169d13a3d43a84a15f8dea8d40adbc3924
// I18n.js
// =======
//
// This small library provides the Rails I18n API on the Javascript.
// You don't actually have to use Rails (or even Ruby) to use I18n.js.
// Just make sure you export all translations in an object like this:
//
// I18n.translations.en = {
// hello: "Hello World"
// };
//
// See tests for specific formatting like numbers and dates.
//
;(function(I18n){
"use strict";
// Just cache the Array#slice function.
var slice = Array.prototype.slice;
// Apply number padding.
var padding = function(number) {
return ("0" + number.toString()).substr(-2);
};
// Set default days/months translations.
var DATE = {
day_names: ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]
, abbr_day_names: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
, month_names: [null, "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]
, abbr_month_names: [null, "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
, meridian: ["AM", "PM"]
};
// Set default number format.
var NUMBER_FORMAT = {
precision: 3
, separator: "."
, delimiter: ","
, strip_insignificant_zeros: false
};
// Set default currency format.
var CURRENCY_FORMAT = {
unit: "$"
, precision: 2
, format: "%u%n"
, delimiter: ","
, separator: "."
};
// Set default percentage format.
var PERCENTAGE_FORMAT = {
precision: 3
, separator: "."
, delimiter: ""
};
// Set default size units.
var SIZE_UNITS = [null, "kb", "mb", "gb", "tb"];
// Other default options
var DEFAULT_OPTIONS = {
defaultLocale: "en",
locale: "en",
defaultSeparator: ".",
placeholder: /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm,
fallbacks: false,
translations: {}
};
I18n.reset = function() {
// Set default locale. This locale will be used when fallback is enabled and
// the translation doesn't exist in a particular locale.
this.defaultLocale = DEFAULT_OPTIONS.defaultLocale;
// Set the current locale to `en`.
this.locale = DEFAULT_OPTIONS.locale;
// Set the translation key separator.
this.defaultSeparator = DEFAULT_OPTIONS.defaultSeparator;
// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
this.placeholder = DEFAULT_OPTIONS.placeholder;
// Set if engine should fallback to the default locale when a translation
// is missing.
this.fallbacks = DEFAULT_OPTIONS.fallbacks;
// Set the default translation object.
this.translations = DEFAULT_OPTIONS.translations;
};
// Much like `reset`, but only assign options if not already assigned
I18n.initializeOptions = function() {
if (typeof(this.defaultLocale) === "undefined" && this.defaultLocale !== null)
this.defaultLocale = DEFAULT_OPTIONS.defaultLocale;
if (typeof(this.locale) === "undefined" && this.locale !== null)
this.locale = DEFAULT_OPTIONS.locale;
if (typeof(this.defaultSeparator) === "undefined" && this.defaultSeparator !== null)
this.defaultSeparator = DEFAULT_OPTIONS.defaultSeparator;
if (typeof(this.placeholder) === "undefined" && this.placeholder !== null)
this.placeholder = DEFAULT_OPTIONS.placeholder;
if (typeof(this.fallbacks) === "undefined" && this.fallbacks !== null)
this.fallbacks = DEFAULT_OPTIONS.fallbacks;
if (typeof(this.translations) === "undefined" && this.translations !== null)
this.translations = DEFAULT_OPTIONS.translations;
};
I18n.initializeOptions();
// Return a list of all locales that must be tried before returning the
// missing translation message. By default, this will consider the inline option,
// current locale and fallback locale.
//
// I18n.locales.get("de-DE");
// // ["de-DE", "de", "en"]
//
// You can define custom rules for any locale. Just make sure you return a array
// containing all locales.
//
// // Default the Wookie locale to English.
// I18n.locales["wk"] = function(locale) {
// return ["en"];
// };
//
I18n.locales = {};
// Retrieve locales based on inline locale, current locale or default to
// I18n's detection.
I18n.locales.get = function(locale) {
var result = this[locale] || this[I18n.locale] || this["default"];
if (typeof(result) === "function") {
result = result(locale);
}
if (result instanceof Array === false) {
result = [result];
}
return result;
};
// The default locale list.
I18n.locales["default"] = function(locale) {
var locales = []
, list = []
, countryCode
, count
;
// Handle the inline locale option that can be provided to
// the `I18n.t` options.
if (locale) {
locales.push(locale);
}
// Add the current locale to the list.
if (!locale && I18n.locale) {
locales.push(I18n.locale);
}
// Add the default locale if fallback strategy is enabled.
if (I18n.fallbacks && I18n.defaultLocale) {
locales.push(I18n.defaultLocale);
}
// Compute each locale with its country code.
// So this will return an array containing both
// `de-DE` and `de` locales.
locales.forEach(function(locale){
countryCode = locale.split("-")[0];
if (!~list.indexOf(locale)) {
list.push(locale);
}
if (I18n.fallbacks && countryCode && countryCode !== locale && !~list.indexOf(countryCode)) {
list.push(countryCode);
}
});
// No locales set? English it is.
if (!locales.length) {
locales.push("en");
}
return list;
};
// Hold pluralization rules.
I18n.pluralization = {};
// Return the pluralizer for a specific locale.
// If no specify locale is found, then I18n's default will be used.
I18n.pluralization.get = function(locale) {
return this[locale] || this[I18n.locale] || this["default"];
};
// The default pluralizer rule.
// It detects the `zero`, `one`, and `other` scopes.
I18n.pluralization["default"] = function(count) {
switch (count) {
case 0: return ["zero", "other"];
case 1: return ["one"];
default: return ["other"];
}
};
// Return current locale. If no locale has been set, then
// the current locale will be the default locale.
I18n.currentLocale = function() {
return this.locale || this.defaultLocale;
};
// Check if value is different than undefined and null;
I18n.isSet = function(value) {
return value !== undefined && value !== null;
};
// Find and process the translation using the provided scope and options.
// This is used internally by some functions and should not be used as an
// public API.
I18n.lookup = function(scope, options) {
options = this.prepareOptions(options);
var locales = this.locales.get(options.locale).slice()
, requestedLocale = locales[0]
, locale
, scopes
, translations
;
// Deal with the scope as an array.
if (scope.constructor === Array) {
scope = scope.join(this.defaultSeparator);
}
// Deal with the scope option provided through the second argument.
//
// I18n.t('hello', {scope: 'greetings'});
//
if (options.scope) {
scope = [options.scope, scope].join(this.defaultSeparator);
}
while (locales.length) {
locale = locales.shift();
scopes = scope.split(this.defaultSeparator);
translations = this.translations[locale];
if (!translations) {
continue;
}
while (scopes.length) {
translations = translations[scopes.shift()];
if (translations === undefined || translations === null) {
break;
}
}
if (translations !== undefined && translations !== null) {
return translations;
}
}
if (this.isSet(options.defaultValue)) {
return options.defaultValue;
}
};
// Rails changed the way the meridian is stored.
// It started with `date.meridian` returning an array,
// then it switched to `time.am` and `time.pm`.
// This function abstracts this difference and returns
// the correct meridian or the default value when none is provided.
I18n.meridian = function() {
var time = this.lookup("time");
var date = this.lookup("date");
if (time && time.am && time.pm) {
return [time.am, time.pm];
} else if (date && date.meridian) {
return date.meridian;
} else {
return DATE.meridian;
}
};
// Merge serveral hash options, checking if value is set before
// overwriting any value. The precedence is from left to right.
//
// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
// #=> {name: "John Doe", role: "user"}
//
I18n.prepareOptions = function() {
var args = slice.call(arguments)
, options = {}
, subject
;
while (args.length) {
subject = args.shift();
if (typeof(subject) != "object") {
continue;
}
for (var attr in subject) {
if (!subject.hasOwnProperty(attr)) {
continue;
}
if (this.isSet(options[attr])) {
continue;
}
options[attr] = subject[attr];
}
}
return options;
};
// Generate a list of translation options for default fallbacks.
// `defaultValue` is also deleted from options as it is returned as part of
// the translationOptions array.
I18n.createTranslationOptions = function(scope, options) {
var translationOptions = [{scope: scope}];
// Defaults should be an array of hashes containing either
// fallback scopes or messages
if (this.isSet(options.defaults)) {
translationOptions = translationOptions.concat(options.defaults);
}
// Maintain support for defaultValue. Since it is always a message
// insert it in to the translation options as such.
if (this.isSet(options.defaultValue)) {
translationOptions.push({ message: options.defaultValue });
delete options.defaultValue;
}
return translationOptions;
};
// Translate the given scope with the provided options.
I18n.translate = function(scope, options) {
options = this.prepareOptions(options);
var translationOptions = this.createTranslationOptions(scope, options);
var translation;
// Iterate through the translation options until a translation
// or message is found.
var translationFound =
translationOptions.some(function(translationOption) {
if (this.isSet(translationOption.scope)) {
translation = this.lookup(translationOption.scope, options);
} else if (this.isSet(translationOption.message)) {
translation = translationOption.message;
}
if (translation !== undefined && translation !== null) {
return true;
}
}, this);
if (!translationFound) {
return this.missingTranslation(scope);
}
if (typeof(translation) === "string") {
translation = this.interpolate(translation, options);
} else if (translation instanceof Object && this.isSet(options.count)) {
translation = this.pluralize(options.count, translation, options);
}
return translation;
};
// This function interpolates the all variables in the given message.
I18n.interpolate = function(message, options) {
options = this.prepareOptions(options);
var matches = message.match(this.placeholder)
, placeholder
, value
, name
, regex
;
if (!matches) {
return message;
}
var value;
while (matches.length) {
placeholder = matches.shift();
name = placeholder.replace(this.placeholder, "$1");
if (this.isSet(options[name])) {
value = options[name].toString().replace(/\$/gm, "_#$#_");
} else {
value = this.missingPlaceholder(placeholder, message);
}
regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm, "\\}"));
message = message.replace(regex, value);
}
return message.replace(/_#\$#_/g, "$");
};
// Pluralize the given scope using the `count` value.
// The pluralized translation may have other placeholders,
// which will be retrieved from `options`.
I18n.pluralize = function(count, scope, options) {
options = this.prepareOptions(options);
var translations, pluralizer, keys, key, message;
if (scope instanceof Object) {
translations = scope;
} else {
translations = this.lookup(scope, options);
}
if (!translations) {
return this.missingTranslation(scope);
}
pluralizer = this.pluralization.get(options.locale);
keys = pluralizer(Math.abs(count));
while (keys.length) {
key = keys.shift();
if (this.isSet(translations[key])) {
message = translations[key];
break;
}
}
options.count = String(count);
return this.interpolate(message, options);
};
// Return a missing translation message for the given parameters.
I18n.missingTranslation = function(scope) {
var message = '[missing "';
message += this.currentLocale() + ".";
message += slice.call(arguments).join(".");
message += '" translation]';
return message;
};
// Return a missing placeholder message for given parameters
I18n.missingPlaceholder = function(placeholder, message) {
return "[missing " + placeholder + " value]";
};
// Format number using localization rules.
// The options will be retrieved from the `number.format` scope.
// If this isn't present, then the following options will be used:
//
// - `precision`: `3`
// - `separator`: `"."`
// - `delimiter`: `","`
// - `strip_insignificant_zeros`: `false`
//
// You can also override these options by providing the `options` argument.
//
I18n.toNumber = function(number, options) {
options = this.prepareOptions(
options
, this.lookup("number.format")
, NUMBER_FORMAT
);
var negative = number < 0
, string = Math.abs(number).toFixed(options.precision).toString()
, parts = string.split(".")
, precision
, buffer = []
, formattedNumber
;
number = parts[0];
precision = parts[1];
while (number.length > 0) {
buffer.unshift(number.substr(Math.max(0, number.length - 3), 3));
number = number.substr(0, number.length -3);
}
formattedNumber = buffer.join(options.delimiter);
if (options.strip_insignificant_zeros && precision) {
precision = precision.replace(/0+$/, "");
}
if (options.precision > 0 && precision) {
formattedNumber += options.separator + precision;
}
if (negative) {
formattedNumber = "-" + formattedNumber;
}
return formattedNumber;
};
// Format currency with localization rules.
// The options will be retrieved from the `number.currency.format` and
// `number.format` scopes, in that order.
//
// Any missing option will be retrieved from the `I18n.toNumber` defaults and
// the following options:
//
// - `unit`: `"$"`
// - `precision`: `2`
// - `format`: `"%u%n"`
// - `delimiter`: `","`
// - `separator`: `"."`
//
// You can also override these options by providing the `options` argument.
//
I18n.toCurrency = function(number, options) {
options = this.prepareOptions(
options
, this.lookup("number.currency.format")
, this.lookup("number.format")
, CURRENCY_FORMAT
);
number = this.toNumber(number, options);
number = options.format
.replace("%u", options.unit)
.replace("%n", number)
;
return number;
};
// Localize several values.
// You can provide the following scopes: `currency`, `number`, or `percentage`.
// If you provide a scope that matches the `/^(date|time)/` regular expression
// then the `value` will be converted by using the `I18n.toTime` function.
//
// It will default to the value's `toString` function.
//
I18n.localize = function(scope, value) {
switch (scope) {
case "currency":
return this.toCurrency(value);
case "number":
scope = this.lookup("number.format");
return this.toNumber(value, scope);
case "percentage":
return this.toPercentage(value);
default:
if (scope.match(/^(date|time)/)) {
return this.toTime(scope, value);
} else {
return value.toString();
}
}
};
// Parse a given `date` string into a JavaScript Date object.
// This function is time zone aware.
//
// The following string formats are recognized:
//
// yyyy-mm-dd
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ss
// yyyy-mm-dd[ T]hh:mm::ssZ
// yyyy-mm-dd[ T]hh:mm::ss+0000
// yyyy-mm-dd[ T]hh:mm::ss+00:00
// yyyy-mm-dd[ T]hh:mm::ss.123Z
//
I18n.parseDate = function(date) {
var matches, convertedDate, fraction;
// we have a date, so just return it.
if (typeof(date) == "object") {
return date;
};
matches = date.toString().match(/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2})([\.,]\d{1,3})?)?(Z|\+00:?00)?/);
if (matches) {
for (var i = 1; i <= 6; i++) {
matches[i] = parseInt(matches[i], 10) || 0;
}
// month starts on 0
matches[2] -= 1;
fraction = matches[7] ? 1000 * ("0" + matches[7]) : null;
if (matches[8]) {
convertedDate = new Date(Date.UTC(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction));
} else {
convertedDate = new Date(matches[1], matches[2], matches[3], matches[4], matches[5], matches[6], fraction);
}
} else if (typeof(date) == "number") {
// UNIX timestamp
convertedDate = new Date();
convertedDate.setTime(date);
} else if (date.match(/([A-Z][a-z]{2}) ([A-Z][a-z]{2}) (\d+) (\d+:\d+:\d+) ([+-]\d+) (\d+)/)) {
// This format `Wed Jul 20 13:03:39 +0000 2011` is parsed by
// webkit/firefox, but not by IE, so we must parse it manually.
convertedDate = new Date();
convertedDate.setTime(Date.parse([
RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$6, RegExp.$4, RegExp.$5
].join(" ")));
} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
// a valid javascript format with timezone info
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
} else {
// an arbitrary javascript string
convertedDate = new Date();
convertedDate.setTime(Date.parse(date));
}
return convertedDate;
};
// Formats time according to the directives in the given format string.
// The directives begins with a percent (%) character. Any text not listed as a
// directive will be passed through to the output string.
//
// The accepted formats are:
//
// %a - The abbreviated weekday name (Sun)
// %A - The full weekday name (Sunday)
// %b - The abbreviated month name (Jan)
// %B - The full month name (January)
// %c - The preferred local date and time representation
// %d - Day of the month (01..31)
// %-d - Day of the month (1..31)
// %H - Hour of the day, 24-hour clock (00..23)
// %-H - Hour of the day, 24-hour clock (0..23)
// %I - Hour of the day, 12-hour clock (01..12)
// %-I - Hour of the day, 12-hour clock (1..12)
// %m - Month of the year (01..12)
// %-m - Month of the year (1..12)
// %M - Minute of the hour (00..59)
// %-M - Minute of the hour (0..59)
// %p - Meridian indicator (AM or PM)
// %S - Second of the minute (00..60)
// %-S - Second of the minute (0..60)
// %w - Day of the week (Sunday is 0, 0..6)
// %y - Year without a century (00..99)
// %-y - Year without a century (0..99)
// %Y - Year with century
// %z - Timezone offset (+0545)
//
I18n.strftime = function(date, format) {
var options = this.lookup("date")
, meridianOptions = I18n.meridian()
;
if (!options) {
options = {};
}
options = this.prepareOptions(options, DATE);
var weekDay = date.getDay()
, day = date.getDate()
, year = date.getFullYear()
, month = date.getMonth() + 1
, hour = date.getHours()
, hour12 = hour
, meridian = hour > 11 ? 1 : 0
, secs = date.getSeconds()
, mins = date.getMinutes()
, offset = date.getTimezoneOffset()
, absOffsetHours = Math.floor(Math.abs(offset / 60))
, absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60)
, timezoneoffset = (offset > 0 ? "-" : "+") +
(absOffsetHours.toString().length < 2 ? "0" + absOffsetHours : absOffsetHours) +
(absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes : absOffsetMinutes)
;
if (hour12 > 12) {
hour12 = hour12 - 12;
} else if (hour12 === 0) {
hour12 = 12;
}
format = format.replace("%a", options.abbr_day_names[weekDay]);
format = format.replace("%A", options.day_names[weekDay]);
format = format.replace("%b", options.abbr_month_names[month]);
format = format.replace("%B", options.month_names[month]);
format = format.replace("%d", padding(day));
format = format.replace("%e", day);
format = format.replace("%-d", day);
format = format.replace("%H", padding(hour));
format = format.replace("%-H", hour);
format = format.replace("%I", padding(hour12));
format = format.replace("%-I", hour12);
format = format.replace("%m", padding(month));
format = format.replace("%-m", month);
format = format.replace("%M", padding(mins));
format = format.replace("%-M", mins);
format = format.replace("%p", meridianOptions[meridian]);
format = format.replace("%S", padding(secs));
format = format.replace("%-S", secs);
format = format.replace("%w", weekDay);
format = format.replace("%y", padding(year));
format = format.replace("%-y", padding(year).replace(/^0+/, ""));
format = format.replace("%Y", year);
format = format.replace("%z", timezoneoffset);
return format;
};
// Convert the given dateString into a formatted date.
I18n.toTime = function(scope, dateString) {
var date = this.parseDate(dateString)
, format = this.lookup(scope)
;
if (date.toString().match(/invalid/i)) {
return date.toString();
}
if (!format) {
return date.toString();
}
return this.strftime(date, format);
};
// Convert a number into a formatted percentage value.
I18n.toPercentage = function(number, options) {
options = this.prepareOptions(
options
, this.lookup("number.percentage.format")
, this.lookup("number.format")
, PERCENTAGE_FORMAT
);
number = this.toNumber(number, options);
return number + "%";
};
// Convert a number into a readable size representation.
I18n.toHumanSize = function(number, options) {
var kb = 1024
, size = number
, iterations = 0
, unit
, precision
;
while (size >= kb && iterations < 4) {
size = size / kb;
iterations += 1;
}
if (iterations === 0) {
unit = this.t("number.human.storage_units.units.byte", {count: size});
precision = 0;
} else {
unit = this.t("number.human.storage_units.units." + SIZE_UNITS[iterations]);
precision = (size - Math.floor(size) === 0) ? 0 : 1;
}
options = this.prepareOptions(
options
, {precision: precision, format: "%n%u", delimiter: ""}
);
number = this.toNumber(size, options);
number = options.format
.replace("%u", unit)
.replace("%n", number)
;
return number;
};
// Set aliases, so we can save some typing.
I18n.t = I18n.translate;
I18n.l = I18n.localize;
I18n.p = I18n.pluralize;
})(typeof(exports) === "undefined" ? (this.I18n || (this.I18n = {})) : exports);

@ -48,6 +48,10 @@ module.exports = function (config) {
// list of files / patterns to load in the browser
files: [
// I18n.js is provided by the Asset pipeline,
// which is unavailable for unit tests.
// For testing, shim its functionality
'tests/unit/lib/i18n-js.shim.js',
'../app/assets/javascripts/bundles/openproject-global.css',
'../app/assets/javascripts/bundles/openproject-global.js',

@ -1215,9 +1215,6 @@
}
}
},
"json-loader": {
"version": "0.5.1"
},
"json5": {
"version": "0.4.0"
},
@ -2049,15 +2046,15 @@
"bl": {
"version": "1.0.3"
},
"boom": {
"version": "2.10.1"
},
"block-stream": {
"version": "0.0.8"
},
"caseless": {
"version": "0.11.0"
},
"boom": {
"version": "2.10.1"
},
"chalk": {
"version": "1.1.3"
},
@ -2067,12 +2064,12 @@
"commander": {
"version": "2.9.0"
},
"cryptiles": {
"version": "2.0.5"
},
"core-util-is": {
"version": "1.0.2"
},
"cryptiles": {
"version": "2.0.5"
},
"debug": {
"version": "2.2.0"
},
@ -2085,6 +2082,9 @@
"delegates": {
"version": "1.0.0"
},
"ecc-jsbn": {
"version": "0.1.1"
},
"escape-string-regexp": {
"version": "1.0.5"
},
@ -2094,9 +2094,6 @@
"extsprintf": {
"version": "1.0.2"
},
"ecc-jsbn": {
"version": "0.1.1"
},
"forever-agent": {
"version": "0.6.1"
},
@ -2109,6 +2106,9 @@
"gauge": {
"version": "1.2.7"
},
"generate-object-property": {
"version": "1.2.0"
},
"generate-function": {
"version": "2.0.0"
},
@ -2121,27 +2121,24 @@
"har-validator": {
"version": "2.0.6"
},
"generate-object-property": {
"version": "1.2.0"
},
"has-ansi": {
"version": "2.0.0"
},
"has-unicode": {
"version": "2.0.0"
},
"hawk": {
"version": "3.1.3"
},
"hoek": {
"version": "2.16.3"
},
"inherits": {
"version": "2.0.1"
"hawk": {
"version": "3.1.3"
},
"http-signature": {
"version": "1.1.1"
},
"inherits": {
"version": "2.0.1"
},
"ini": {
"version": "1.3.4"
},
@ -2163,12 +2160,12 @@
"jodid25519": {
"version": "1.0.2"
},
"json-schema": {
"version": "0.2.2"
},
"jsbn": {
"version": "0.1.0"
},
"json-schema": {
"version": "0.2.2"
},
"json-stringify-safe": {
"version": "5.0.1"
},
@ -2211,12 +2208,12 @@
"node-uuid": {
"version": "1.4.7"
},
"npmlog": {
"version": "2.0.3"
},
"oauth-sign": {
"version": "0.8.1"
},
"npmlog": {
"version": "2.0.3"
},
"once": {
"version": "1.3.3"
},
@ -2253,12 +2250,12 @@
"stringstream": {
"version": "0.0.5"
},
"strip-json-comments": {
"version": "1.0.4"
},
"strip-ansi": {
"version": "3.0.1"
},
"strip-json-comments": {
"version": "1.0.4"
},
"supports-color": {
"version": "2.0.0"
},
@ -2274,15 +2271,15 @@
"tunnel-agent": {
"version": "0.4.2"
},
"tweetnacl": {
"version": "0.14.3"
},
"uid-number": {
"version": "0.0.6"
},
"util-deprecate": {
"version": "1.0.2"
},
"tweetnacl": {
"version": "0.14.3"
},
"verror": {
"version": "1.3.6"
},
@ -2419,30 +2416,6 @@
}
}
}
},
"yaml-loader": {
"version": "0.1.0",
"dependencies": {
"js-yaml": {
"version": "3.2.7",
"dependencies": {
"argparse": {
"version": "1.0.2",
"dependencies": {
"lodash": {
"version": "3.6.0"
},
"sprintf-js": {
"version": "1.0.2"
}
}
},
"esprima": {
"version": "2.0.0"
}
}
}
}
}
},
"name": "openproject-frontend",

@ -39,7 +39,6 @@
"file-loader": "^0.8.1",
"glob": "^4.5.3",
"html-loader": "^0.2.3",
"json-loader": "^0.5.1",
"json5": "^0.4.0",
"lodash": "^2.4.2",
"ng-annotate-loader": "0.0.10",
@ -51,8 +50,7 @@
"ts-loader": "^0.7.2",
"typescript": "^1.7.5",
"url-loader": "^0.5.5",
"webpack": "^1.13.1",
"yaml-loader": "^0.1.0"
"webpack": "^1.13.1"
},
"scripts": {
"pretest": "npm run webpack",

@ -53,8 +53,6 @@ var OpenProjectPlugins = {
return _.reduce(this.allPluginNamesPaths(), function(obj, pluginPath, pluginName) {
if (test('-e', path.join(pluginPath, 'package.json'))) {
obj[pluginName] = pluginPath;
} else {
console.info('INFO: plugin "%s" does not provide a package.json', pluginName);
}
return obj;
}, {});

@ -1,4 +1,4 @@
// -- copyright
//-- copyright
// OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
//
@ -24,28 +24,15 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
//
// See doc/COPYRIGHT.rdoc for more details.
// ++
//++
export class LoadLocales {
public static I18n:op.I18n;
public static files(localeFiles) {
localeFiles.keys().forEach(function(localeFile) {
var locale_matches = localeFile.match(/js-((\w{2})(-\w{2})?)\.yml/);
var locale_with_country = locale_matches[1];
var locale_without_country = locale_matches[2];
var localizations = localeFiles(localeFile)[locale_without_country];
var locale = locale_without_country;
// Some locales e.g. es-ES have a language postfix but within the yml files
// that postfix is lacking.
if (!localizations) {
localizations = localeFiles(localeFile)[locale_with_country];
locale = locale_with_country;
}
this.I18n.addTranslations(locale, localizations);
});
var I18n = {
t: function(key) {
return '[missing "' + key + '" translation]';
},
defaultLocale: 'en',
locale: 'en',
translations: {
en: {}
}
}
};

@ -57,7 +57,6 @@ var loaders = [
{test: /[\/]dragula\.js$/, loader: 'expose?dragula'},
{test: /[\/]moment\.js$/, loader: 'expose?moment'},
{test: /[\/]mousetrap\.js$/, loader: 'expose?Mousetrap'},
{test: /[\/]vendor[\/]i18n\.js$/, loader: 'expose?I18n'},
{
test: /\.css$/,
loader: ExtractTextPlugin.extract(
@ -68,7 +67,6 @@ var loaders = [
{test: /\.png$/, loader: 'url-loader?limit=100000&mimetype=image/png'},
{test: /\.gif$/, loader: 'file-loader'},
{test: /\.jpg$/, loader: 'file-loader'},
{test: /js-[\w|-]{2,5}\.yml$/, loader: 'json!yaml'},
{test: /[\/].*\.js$/, loader: 'ng-annotate?map=true'}
];
@ -142,6 +140,10 @@ function getWebpackMainConfig() {
configFileName: path.resolve(__dirname, 'tsconfig.json')
},
externals: {
"I18n": "I18n"
},
plugins: [
// Add a simple fail plugin to return a status code of 2 if
// errors are detected (this includes TS warnings)

@ -34,14 +34,17 @@
# Otherwise Sprockets cannot find the files that webpack produces.
Rake::Task['assets:precompile']
.clear_prerequisites
.enhance(['assets:compile_environment'])
.enhance(['assets:compile_environment', 'assets:prepare_op'])
namespace :assets do
# In this task, set prerequisites for the assets:precompile task
task compile_environment: :webpack do
task compile_environment: :prepare_op do
Rake::Task['assets:environment'].invoke
end
desc 'Prepare locales and webpack assets'
task prepare_op: [:webpack, :export_locales]
desc 'Compile assets with webpack'
task :webpack do
Dir.chdir Rails.root.join('frontend') do
@ -49,6 +52,9 @@ namespace :assets do
end
end
desc 'Export frontend locale files'
task export_locales: ['i18n:js:export']
task :clobber do
rm_rf FileList["#{Rails.root}/app/assets/javascripts/bundles/*"]
end

@ -40,7 +40,7 @@ unless ARGV.any? { |a| a =~ /\Agems/ } # Don't load anything when running the ge
require 'cucumber/rake/task'
namespace :cucumber do
Cucumber::Rake::Task.new({ ok: ['db:test:prepare', 'assets:webpack'] }, 'Run features that should pass') do |t|
Cucumber::Rake::Task.new({ ok: ['db:test:prepare', 'assets:prepare_op'] }, 'Run features that should pass') do |t|
t.fork = true # You may get faster startup if you set this to false
end
@ -52,7 +52,7 @@ unless ARGV.any? { |a| a =~ /\Agems/ } # Don't load anything when running the ge
def define_cucumber_task(name, description, arguments = [])
desc description
# task name, arguments => ['db:test:prepare', 'assets:webpack'] do |_t, args|
# task name, arguments => ['db:test:prepare', 'assets:prepare_op'] do |_t, args|
task name, arguments => ['db:test:prepare'] do |_t, args|
if name == :custom
if not args[:features]
@ -78,7 +78,7 @@ unless ARGV.any? { |a| a =~ /\Agems/ } # Don't load anything when running the ge
end
end
Cucumber::Rake::Task.new({ cucumber_run: ['db:test:prepare', 'assets:webpack'] }, 'Run features that should pass') do |t|
Cucumber::Rake::Task.new({ cucumber_run: ['db:test:prepare', 'assets:prepare_op'] }, 'Run features that should pass') do |t|
opts = (ENV['CUCUMBER_OPTS'] ? ENV['CUCUMBER_OPTS'].split(/\s+/) : [])
ENV.delete('CUCUMBER_OPTS')
opts += args[:options].split(/\s+/) if args[:options]

@ -47,12 +47,12 @@ begin
namespace :spec do
desc 'Run core and plugin specs'
RSpec::Core::RakeTask.new(all: [:environment, 'assets:webpack']) do |t|
RSpec::Core::RakeTask.new(all: [:environment, 'assets:prepare_op']) do |t|
t.pattern = [Rails.root.join('spec').to_s] + Plugins::LoadPathHelper.spec_load_paths
end
desc 'Run plugin specs'
RSpec::Core::RakeTask.new(plugins: [:environment, 'assets:webpack']) do |t|
RSpec::Core::RakeTask.new(plugins: [:environment, 'assets:prepare_op']) do |t|
t.pattern = Plugins::LoadPathHelper.spec_load_paths
# in case we want to run plugins' specs and there are none

@ -125,6 +125,6 @@ end
%w(spec).each do |type|
if Rake::Task.task_defined?("#{type}:prepare")
Rake::Task["#{type}:prepare"].enhance(['assets:webpack'])
Rake::Task["#{type}:prepare"].enhance(['assets:prepare_op'])
end
end

Loading…
Cancel
Save