From 6e73678f389d310e39c412f499bc1205473c5088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:45:06 +0200 Subject: [PATCH 01/15] Require i18n-js through bundler --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 6a35a28c72..18927ad2ba 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 915cb165ce..751a7e99ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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 From d65e527d166cea4e44906ed80cd5b3797b056a8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:45:59 +0200 Subject: [PATCH 02/15] Use external dependency for i18n in webpack --- app/assets/javascripts/application.js.erb | 2 + frontend/app/global.js | 2 - frontend/app/init-app.js | 12 - frontend/app/vendor/i18n.js | 831 ---------------------- frontend/webpack-main-config.js | 5 +- 5 files changed, 6 insertions(+), 846 deletions(-) delete mode 100644 frontend/app/vendor/i18n.js diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index be9b6f28c8..811ef772d5 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -28,6 +28,8 @@ //= require ./bundles/openproject-global +//= require i18n + //# require jquery_noconflict //= require prototype //= require effects diff --git a/frontend/app/global.js b/frontend/app/global.js index 1bc33d5491..af211b4f53 100644 --- a/frontend/app/global.js +++ b/frontend/app/global.js @@ -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'); diff --git a/frontend/app/init-app.js b/frontend/app/init-app.js index df635e37f2..3182689f0a 100644 --- a/frontend/app/init-app.js +++ b/frontend/app/init-app.js @@ -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'); diff --git a/frontend/app/vendor/i18n.js b/frontend/app/vendor/i18n.js deleted file mode 100644 index dd2726adde..0000000000 --- a/frontend/app/vendor/i18n.js +++ /dev/null @@ -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); diff --git a/frontend/webpack-main-config.js b/frontend/webpack-main-config.js index 15aca5f1e6..b10d09ac92 100644 --- a/frontend/webpack-main-config.js +++ b/frontend/webpack-main-config.js @@ -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( @@ -142,6 +141,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) From 0553efa76d215366940256f3f033ba4b98f042c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:47:38 +0200 Subject: [PATCH 03/15] Generate one file per locale key and require that explicitly --- .gitignore | 1 + app/views/layouts/base.html.erb | 1 + config/i18n-js.yml | 4 ++++ config/initializers/assets.rb | 1 + 4 files changed, 7 insertions(+) create mode 100644 config/i18n-js.yml diff --git a/.gitignore b/.gitignore index a921e1bcd0..ab054bf14c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/app/views/layouts/base.html.erb b/app/views/layouts/base.html.erb index 98d91a51f0..ca5db63635 100644 --- a/app/views/layouts/base.html.erb +++ b/app/views/layouts/base.html.erb @@ -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_javascript_includes %> diff --git a/config/i18n-js.yml b/config/i18n-js.yml new file mode 100644 index 0000000000..a43e2561fc --- /dev/null +++ b/config/i18n-js.yml @@ -0,0 +1,4 @@ +fallbacks: false +translations: + - file: "app/assets/javascripts/locales/%{locale}.js" + only: '*.js' diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 7af6698242..e5771a5cdf 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -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 From 58e4a642dad53a5668e7a0918b19c2b7a7883988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:49:04 +0200 Subject: [PATCH 04/15] Automatically build locales in dev mode --- config/environments/development.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/environments/development.rb b/config/environments/development.rb index 8f72175c0c..04b077592f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -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 From 2f57685327adf8e1cae331fb92cf52f79165efd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:49:19 +0200 Subject: [PATCH 05/15] Export locales as assets as part of assets:precompile --- lib/tasks/assets.rake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index a1d96c8648..04dee48a3d 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -34,7 +34,7 @@ # Otherwise Sprockets cannot find the files that webpack produces. Rake::Task['assets:precompile'] .clear_prerequisites - .enhance(['assets:compile_environment']) + .enhance(['assets:compile_environment', 'assets:export_locales']) namespace :assets do # In this task, set prerequisites for the assets:precompile task @@ -49,6 +49,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 From 23149619656e9381762858ab9cd49ef47505e1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:54:25 +0200 Subject: [PATCH 06/15] Remove LoadLocales module --- frontend/app/plugins/load-locales.module.ts | 51 --------------------- 1 file changed, 51 deletions(-) delete mode 100644 frontend/app/plugins/load-locales.module.ts diff --git a/frontend/app/plugins/load-locales.module.ts b/frontend/app/plugins/load-locales.module.ts deleted file mode 100644 index 3d57559ada..0000000000 --- a/frontend/app/plugins/load-locales.module.ts +++ /dev/null @@ -1,51 +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. -// ++ - -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); - }); - } -} From db22e5c4cb070e811462a8dc22f200f92d9e75b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 08:54:33 +0200 Subject: [PATCH 07/15] Update documentation --- doc/development/create-openproject-plugin.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/doc/development/create-openproject-plugin.md b/doc/development/create-openproject-plugin.md index 65086e32c7..ed0fb405ef 100644 --- a/doc/development/create-openproject-plugin.md +++ b/doc/development/create-openproject-plugin.md @@ -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-.js`. Pure frontend plugins should be considered _a work in progress_. As such, it is currently recommended to create hybrid plugins (see below). From ef43f1ac4cd8e1563d9734d76bb3e4b8deabcecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 13:08:10 +0200 Subject: [PATCH 08/15] Remove yaml/json loaders for locales --- frontend/npm-shrinkwrap.json | 85 +++++++++++---------------------- frontend/package.json | 4 +- frontend/rails-plugins.conf.js | 2 - frontend/webpack-main-config.js | 1 - 4 files changed, 30 insertions(+), 62 deletions(-) diff --git a/frontend/npm-shrinkwrap.json b/frontend/npm-shrinkwrap.json index fd44ce2ed9..5709b8599e 100644 --- a/frontend/npm-shrinkwrap.json +++ b/frontend/npm-shrinkwrap.json @@ -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", diff --git a/frontend/package.json b/frontend/package.json index ede00e0e6c..e6839e8bd0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/rails-plugins.conf.js b/frontend/rails-plugins.conf.js index 72f3462a71..51cb3af280 100644 --- a/frontend/rails-plugins.conf.js +++ b/frontend/rails-plugins.conf.js @@ -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; }, {}); diff --git a/frontend/webpack-main-config.js b/frontend/webpack-main-config.js index b10d09ac92..86ab6968d4 100644 --- a/frontend/webpack-main-config.js +++ b/frontend/webpack-main-config.js @@ -67,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'} ]; From 2d7f85de0c9b04294acfdaf7ef0e11fa8a8d3f8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 13:09:59 +0200 Subject: [PATCH 09/15] Fix undefined translation key an `undefined` key passed to I18n didn't raise an exception in the past. --- frontend/app/components/wp-buttons/wp-buttons.module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/app/components/wp-buttons/wp-buttons.module.ts b/frontend/app/components/wp-buttons/wp-buttons.module.ts index 4298929c28..e72fac38f0 100644 --- a/frontend/app/components/wp-buttons/wp-buttons.module.ts +++ b/frontend/app/components/wp-buttons/wp-buttons.module.ts @@ -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) : '' }; } From aedcd2b31eacda02da37bc7ec38305d52ff0f2a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 13:16:35 +0200 Subject: [PATCH 10/15] Explicitly enable fallbacks This will cause exported files to return the more general default when a specific translation is not available. --- config/i18n-js.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/i18n-js.yml b/config/i18n-js.yml index a43e2561fc..0f870163da 100644 --- a/config/i18n-js.yml +++ b/config/i18n-js.yml @@ -1,4 +1,4 @@ -fallbacks: false +fallbacks: :en translations: - file: "app/assets/javascripts/locales/%{locale}.js" only: '*.js' From d9baf5748d3cfa1a2e55474819f0fab5a0cc8a00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 15:22:26 +0200 Subject: [PATCH 11/15] Execute i18n export for tests The middleware is only used for dev mode, and thus we need to enhance the existing rake task to not only run webpack, but also the i18n export. --- .teatro.yml | 2 +- lib/tasks/assets.rake | 7 +++++-- lib/tasks/cucumber.rake | 6 +++--- lib/tasks/plugin_specs.rake | 4 ++-- lib/tasks/testing.rake | 2 +- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/.teatro.yml b/.teatro.yml index e4a14d6862..d38b6ece02 100644 --- a/.teatro.yml +++ b/.teatro.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 diff --git a/lib/tasks/assets.rake b/lib/tasks/assets.rake index 04dee48a3d..0758b8ca4e 100644 --- a/lib/tasks/assets.rake +++ b/lib/tasks/assets.rake @@ -34,14 +34,17 @@ # Otherwise Sprockets cannot find the files that webpack produces. Rake::Task['assets:precompile'] .clear_prerequisites - .enhance(['assets:compile_environment', 'assets:export_locales']) + .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 diff --git a/lib/tasks/cucumber.rake b/lib/tasks/cucumber.rake index a553b63c68..92b78781d2 100644 --- a/lib/tasks/cucumber.rake +++ b/lib/tasks/cucumber.rake @@ -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] diff --git a/lib/tasks/plugin_specs.rake b/lib/tasks/plugin_specs.rake index af85d3d195..0b682441ee 100644 --- a/lib/tasks/plugin_specs.rake +++ b/lib/tasks/plugin_specs.rake @@ -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 diff --git a/lib/tasks/testing.rake b/lib/tasks/testing.rake index 1f79aa0eeb..84a0b70e77 100644 --- a/lib/tasks/testing.rake +++ b/lib/tasks/testing.rake @@ -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 From c1a910dab090b28bac9f8d136b1797685cc1f6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 15:28:14 +0200 Subject: [PATCH 12/15] Use correct branch of openproject-translations This is required for webpack to function properly --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 18927ad2ba..bfc9b3afda 100644 --- a/Gemfile +++ b/Gemfile @@ -239,7 +239,7 @@ platforms :jruby do end group :opf_plugins do - gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'dev' + gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'housekeeping/locales' end # TODO: Make this group :optional when bundler v10.x diff --git a/Gemfile.lock b/Gemfile.lock index 751a7e99ab..d92af5d462 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,8 +70,8 @@ GIT GIT remote: https://github.com/opf/openproject-translations.git - revision: 34ba769f4719478c68c553feb7024e7e5bbaf0a0 - branch: dev + revision: 2e6705f15db42b1a6cbb356fa01cccfcf35fa51b + branch: housekeeping/locales specs: openproject-translations (5.1.0) crowdin-api (~> 0.4.0) From 561ec0c88ced6b8eca4c7a75df1ae2e62e7fe41f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 20:03:39 +0200 Subject: [PATCH 13/15] Remove global default translation Translations are not yet available on `.run()` of angular and cgBusyDefaults will never be updated. --- frontend/app/init-app.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/frontend/app/init-app.js b/frontend/app/init-app.js index 3182689f0a..69ed73a01c 100644 --- a/frontend/app/init-app.js +++ b/frontend/app/init-app.js @@ -83,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', From 3da1446eee0f14bbe3a5e06de50d2691a965a174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Oliver=20G=C3=BCnther?= Date: Wed, 13 Jul 2016 20:04:21 +0200 Subject: [PATCH 14/15] Add very basic shim of I18n for unit testing As I18n.js is now provided outside of webpack, we could include the full version through webpack again (risking incompatible version). Since we don't use actual translations, I argue it's better to shim the external I18n object just for karma. --- frontend/karma.conf.js | 4 +++ frontend/tests/unit/lib/i18n-js.shim.js | 38 +++++++++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 frontend/tests/unit/lib/i18n-js.shim.js diff --git a/frontend/karma.conf.js b/frontend/karma.conf.js index cc8c5a1dde..3534c70a33 100644 --- a/frontend/karma.conf.js +++ b/frontend/karma.conf.js @@ -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', diff --git a/frontend/tests/unit/lib/i18n-js.shim.js b/frontend/tests/unit/lib/i18n-js.shim.js new file mode 100644 index 0000000000..40ec744a00 --- /dev/null +++ b/frontend/tests/unit/lib/i18n-js.shim.js @@ -0,0 +1,38 @@ +//-- 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. +//++ + +var I18n = { + t: function(key) { + return '[missing "' + key + '" translation]'; + }, + defaultLocale: 'en', + locale: 'en', + translations: { + en: {} + } +}; From 7476f5efd6e752a78aefaa9f15e7066c3b8fc151 Mon Sep 17 00:00:00 2001 From: Jens Ulferts Date: Thu, 14 Jul 2016 11:57:11 +0200 Subject: [PATCH 15/15] use translations dev branch again --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index bfc9b3afda..18927ad2ba 100644 --- a/Gemfile +++ b/Gemfile @@ -239,7 +239,7 @@ platforms :jruby do end group :opf_plugins do - gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'housekeeping/locales' + gem 'openproject-translations', git:'https://github.com/opf/openproject-translations.git', branch: 'dev' end # TODO: Make this group :optional when bundler v10.x diff --git a/Gemfile.lock b/Gemfile.lock index d92af5d462..a1c4875a3d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -70,8 +70,8 @@ GIT GIT remote: https://github.com/opf/openproject-translations.git - revision: 2e6705f15db42b1a6cbb356fa01cccfcf35fa51b - branch: housekeeping/locales + revision: f331b3218ca082c6034568631e520e36043fa397 + branch: dev specs: openproject-translations (5.1.0) crowdin-api (~> 0.4.0)