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 /.project
/.loadpath /.loadpath
/app/assets/javascripts/bundles/*.* /app/assets/javascripts/bundles/*.*
/app/assets/javascripts/locales/*.*
/config/additional_environment.rb /config/additional_environment.rb
/config/configuration.yml /config/configuration.yml
/config/database.yml /config/database.yml

@ -15,5 +15,5 @@ stage:
- bundle exec rake db:create db:migrate - bundle exec rake db:create db:migrate
- bundle exec rake db:seed RAILS_ENV=development - 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 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 'sass', '~> 3.4.12'
gem 'autoprefixer-rails' gem 'autoprefixer-rails'
gem 'bourbon', '~> 4.2.0' 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' 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 # remove once we no longer use the deprecated "link_to_remote", "remote_form_for" and alike methods

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

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

@ -42,6 +42,7 @@ See doc/COPYRIGHT.rdoc for more details.
<%= render 'common/favicons' %> <%= render 'common/favicons' %>
<%= stylesheet_link_tag current_theme.stylesheet_manifest, media: "all" %> <%= stylesheet_link_tag current_theme.stylesheet_manifest, media: "all" %>
<%= javascript_include_tag 'application' %> <%= javascript_include_tag 'application' %>
<%= javascript_include_tag "locales/#{I18n.locale}" %>
<!-- user specific tags --> <!-- user specific tags -->
<%= user_specific_javascript_includes %> <%= user_specific_javascript_includes %>
<!-- project specific tags --> <!-- 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. # since you don't have to restart the web server when you make code changes.
config.cache_classes = false config.cache_classes = false
# Automatically refresh translations with I18n middleware
config.middleware.use ::I18n::JS::Middleware
# Do not eager load code on boot. # Do not eager load code on boot.
config.eager_load = false 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-de-DE.js
date-en-US.js date-en-US.js
jstoolbar/lang/*.js jstoolbar/lang/*.js
locales/*.js
members_form.js members_form.js
members_select_boxes.js members_select_boxes.js
new_user.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 Plugins are responsible for loading their own assets, including additional
images, styles and I18n translations. images, styles and I18n translations.
To load translation strings use the provided `I18n.addTranslation` function: Translations are processed by I18n.js through Rails and will be picked up from `config/locales/js-<locale>.js`.
```js
I18n.addTranslations('en', require('../../config/locales/js-en.yml').en);
```
Pure frontend plugins should be considered _a work in progress_. As such, it is Pure frontend plugins should be considered _a work in progress_. As such, it is
currently recommended to create hybrid plugins (see below). currently recommended to create hybrid plugins (see below).

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

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

@ -26,18 +26,6 @@
// See doc/COPYRIGHT.rdoc for more details. // 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-animate');
require('angular-aria'); require('angular-aria');
require('angular-modal'); require('angular-modal');
@ -95,9 +83,6 @@ opApp
angular.element('body').attr('global-drag-and-drop-handler',''); angular.element('body').attr('global-drag-and-drop-handler','');
} }
]) ])
.value('cgBusyDefaults', {
message: I18n.t('js.label_please_wait')
})
.run([ .run([
'$http', '$http',
'$rootScope', '$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 // list of files / patterns to load in the browser
files: [ 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.css',
'../app/assets/javascripts/bundles/openproject-global.js', '../app/assets/javascripts/bundles/openproject-global.js',

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

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

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

@ -1,4 +1,4 @@
// -- copyright //-- copyright
// OpenProject is a project management system. // OpenProject is a project management system.
// Copyright (C) 2012-2015 the OpenProject Foundation (OPF) // Copyright (C) 2012-2015 the OpenProject Foundation (OPF)
// //
@ -24,28 +24,15 @@
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
// //
// See doc/COPYRIGHT.rdoc for more details. // See doc/COPYRIGHT.rdoc for more details.
// ++ //++
export class LoadLocales { var I18n = {
public static I18n:op.I18n; t: function(key) {
return '[missing "' + key + '" translation]';
public static files(localeFiles) { },
localeFiles.keys().forEach(function(localeFile) { defaultLocale: 'en',
var locale_matches = localeFile.match(/js-((\w{2})(-\w{2})?)\.yml/); locale: 'en',
var locale_with_country = locale_matches[1]; translations: {
var locale_without_country = locale_matches[2]; en: {}
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);
});
} }
} };

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

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

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

@ -47,12 +47,12 @@ begin
namespace :spec do namespace :spec do
desc 'Run core and plugin specs' 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 t.pattern = [Rails.root.join('spec').to_s] + Plugins::LoadPathHelper.spec_load_paths
end end
desc 'Run plugin specs' 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 t.pattern = Plugins::LoadPathHelper.spec_load_paths
# in case we want to run plugins' specs and there are none # in case we want to run plugins' specs and there are none

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

Loading…
Cancel
Save