JS中英文转写

上次已经讲了如何在MVC项目中实现中英文转写,但是本子资源文件只能直接作用于html页面或者后台代码中,对于引用中的js代码无法做到转写。

JS代码中的转写我们用到了jQuery.i18n.properties 这款插件,它能实现web前端的国际化。

使用jQuery.i18n.properties 时,我们用到两个核心的js脚本:(也可自行下载)

(1)  jquery.i18n.js

  1 /*!
  2  * jQuery i18n plugin
  3  * @requires jQuery v1.1 or later
  4  *
  5  * See https://github.com/recurser/jquery-i18n
  6  *
  7  * Licensed under the MIT license.
  8  *
  9  * Version: 1.1.1 (Sun, 05 Jan 2014 05:26:50 GMT)
 10  */
 11 (function($) {
 12   /**
 13    * i18n provides a mechanism for translating strings using a jscript dictionary.
 14    *
 15    */
 16 
 17   var __slice = Array.prototype.slice;
 18 
 19   /*
 20    * i18n property list
 21    */
 22   var i18n = {
 23 
 24       dict: null,
 25 
 26     /**
 27      * load()
 28      *
 29      * Load translations.
 30      *
 31      * @param  property_list i18n_dict : The dictionary to use for translation.
 32      */
 33       load: function(i18n_dict) {
 34       if (this.dict !== null) {
 35         $.extend(this.dict, i18n_dict);
 36       } else {
 37         this.dict = i18n_dict;
 38       }
 39       },
 40 
 41     /**
 42      * _()
 43      *
 44      * Looks the given string up in the dictionary and returns the translation if
 45      * one exists. If a translation is not found, returns the original word.
 46      *
 47      * @param  string str           : The string to translate.
 48      * @param  property_list params.. : params for using printf() on the string.
 49      *
 50      * @return string               : Translated word.
 51      */
 52       _: function (str) {
 53       dict = this.dict;
 54           if (dict && dict.hasOwnProperty(str)) {
 55               str = dict[str];
 56           }
 57       args = __slice.call(arguments);
 58       args[0] = str;
 59           // Substitute any params.
 60           return this.printf.apply(this, args);
 61       },
 62 
 63     /*
 64      * printf()
 65      *
 66      * Substitutes %s with parameters given in list. %%s is used to escape %s.
 67      *
 68      * @param  string str    : String to perform printf on.
 69      * @param  string args   : Array of arguments for printf.
 70      *
 71      * @return string result : Substituted string
 72      */
 73       printf: function(str, args) {
 74           if (arguments.length < 2) return str;
 75       args = $.isArray(args) ? args : __slice.call(arguments, 1);
 76       return str.replace(/([^%]|^)%(?:(\d+)\$)?s/g, function(p0, p, position) {
 77         if (position) {
 78           return p + args[parseInt(position)-1];
 79         }
 80         return p + args.shift();
 81       }).replace(/%%s/g, '%s');
 82       }
 83 
 84   };
 85 
 86   /*
 87    * _t()
 88    *
 89    * Allows you to translate a jQuery selector.
 90    *
 91    * eg $('h1')._t('some text')
 92    *
 93    * @param  string str           : The string to translate .
 94    * @param  property_list params : Params for using printf() on the string.
 95    *
 96    * @return element              : Chained and translated element(s).
 97   */
 98   $.fn._t = function(str, params) {
 99     return $(this).html(i18n._.apply(i18n, arguments));
100   };
101 
102   $.i18n = i18n;
103 })(jQuery);
View Code

(2) jquery.i18n.properties.js

  1 /******************************************************************************
  2  * jquery.i18n.properties
  3  * 
  4  * Dual licensed under the GPL (http://dev.jquery.com/browser/trunk/jquery/GPL-LICENSE.txt) and 
  5  * MIT (http://dev.jquery.com/browser/trunk/jquery/MIT-LICENSE.txt) licenses.
  6  * 
  7  * @version     1.1.x
  8  * @author      Nuno Fernandes
  9  *              Matthew Lohbihler
 10  * @url         www.codingwithcoffee.com
 11  * @inspiration Localisation assistance for jQuery (http://keith-wood.name/localisation.html)
 12  *              by Keith Wood (kbwood{at}iinet.com.au) June 2007
 13  * 
 14  *****************************************************************************/
 15 
 16 (function($) {
 17 $.i18n = {};
 18 
 19 /** Map holding bundle keys (if mode: 'map') */
 20 $.i18n.map = {};
 21     
 22 /**
 23  * Load and parse message bundle files (.properties),
 24  * making bundles keys available as javascript variables.
 25  * 
 26  * i18n files are named <name>.js, or <name>_<language>.js or <name>_<language>_<country>.js
 27  * Where:
 28  *      The <language> argument is a valid ISO Language Code. These codes are the lower-case, 
 29  *      two-letter codes as defined by ISO-639. You can find a full list of these codes at a 
 30  *      number of sites, such as: http://www.loc.gov/standards/iso639-2/englangn.html
 31  *      The <country> argument is a valid ISO Country Code. These codes are the upper-case,
 32  *      two-letter codes as defined by ISO-3166. You can find a full list of these codes at a
 33  *      number of sites, such as: http://www.iso.ch/iso/en/prods-services/iso3166ma/02iso-3166-code-lists/list-en1.html
 34  * 
 35  * Sample usage for a bundles/Messages.properties bundle:
 36  * $.i18n.properties({
 37  *      name:      'Messages', 
 38  *      language:  'en_US',
 39  *      path:      'bundles'
 40  * });
 41  * @param  name            (string/string[], optional) names of file to load (eg, 'Messages' or ['Msg1','Msg2']). Defaults to "Messages"
 42  * @param  language        (string, optional) language/country code (eg, 'en', 'en_US', 'pt_PT'). if not specified, language reported by the browser will be used instead.
 43  * @param  path            (string, optional) path of directory that contains file to load
 44  * @param  mode            (string, optional) whether bundles keys are available as JavaScript variables/functions or as a map (eg, 'vars' or 'map')
 45  * @param  cache        (boolean, optional) whether bundles should be cached by the browser, or forcibly reloaded on each page load. Defaults to false (i.e. forcibly reloaded)
 46  * @param  encoding     (string, optional) the encoding to request for bundles. Property file resource bundles are specified to be in ISO-8859-1 format. Defaults to UTF-8 for backward compatibility.
 47  * @param  callback     (function, optional) callback function to be called after script is terminated
 48  */
 49 $.i18n.properties = function(settings) {
 50     // set up settings
 51     var defaults = {
 52         name:           'Messages',
 53         language:       '',
 54         path:           '',  
 55         mode:           'vars',
 56         cache:            false,
 57         encoding:       'UTF-8',
 58         callback:       null
 59     };
 60     settings = $.extend(defaults, settings);    
 61     if(settings.language === null || settings.language == '') {
 62        settings.language = $.i18n.browserLang();
 63     }
 64     if(settings.language === null) {settings.language='';}
 65     
 66     // load and parse bundle files
 67     var files = getFiles(settings.name);
 68     for(i=0; i<files.length; i++) {
 69         // 1. load base (eg, Messages.properties)
 70         loadAndParseFile(settings.path + files[i] + '.properties', settings);
 71         // 2. with language code (eg, Messages_pt.properties)
 72         if(settings.language.length >= 2) {
 73             loadAndParseFile(settings.path + files[i] + '_' + settings.language.substring(0, 2) +'.properties', settings);
 74         }
 75         // 3. with language code and country code (eg, Messages_pt_PT.properties)
 76         if(settings.language.length >= 5) {
 77             loadAndParseFile(settings.path + files[i] + '_' + settings.language.substring(0, 5) +'.properties', settings);
 78         }
 79     }
 80     
 81     // call callback
 82     if(settings.callback){ settings.callback(); }
 83 };
 84 
 85 
 86 /**
 87  * When configured with mode: 'map', allows access to bundle values by specifying its key.
 88  * Eg, jQuery.i18n.prop('com.company.bundles.menu_add')
 89  */
 90 $.i18n.prop = function(key /* Add parameters as function arguments as necessary  */) {
 91     var value = $.i18n.map[key];
 92     if (value == null)
 93         return '[' + key + ']';
 94     
 95     var phvList;
 96     if (arguments.length == 2 && $.isArray(arguments[1]))
 97         // An array was passed as the only parameter, so assume it is the list of place holder values.
 98         phvList = arguments[1];
 99 
100     // Place holder replacement
101     /**
102      * Tested with:
103      *   test.t1=asdf ''{0}''
104      *   test.t2=asdf '{0}' '{1}'{1}'zxcv
105      *   test.t3=This is \"a quote" 'a''{0}''s'd{fgh{ij'
106      *   test.t4="'''{'0}''" {0}{a}
107      *   test.t5="'''{0}'''" {1}
108      *   test.t6=a {1} b {0} c
109      *   test.t7=a 'quoted \\ s\ttringy' \t\t x
110      *
111      * Produces:
112      *   test.t1, p1 ==> asdf 'p1'
113      *   test.t2, p1 ==> asdf {0} {1}{1}zxcv
114      *   test.t3, p1 ==> This is "a quote" a'{0}'sd{fgh{ij
115      *   test.t4, p1 ==> "'{0}'" p1{a}
116      *   test.t5, p1 ==> "'{0}'" {1}
117      *   test.t6, p1 ==> a {1} b p1 c
118      *   test.t6, p1, p2 ==> a p2 b p1 c
119      *   test.t6, p1, p2, p3 ==> a p2 b p1 c
120      *   test.t7 ==> a quoted \ s    tringy          x
121      */
122     
123     var i;
124     if (typeof(value) == 'string') {
125         // Handle escape characters. Done separately from the tokenizing loop below because escape characters are 
126         // active in quoted strings.
127         i = 0;
128         while ((i = value.indexOf('\\', i)) != -1) {
129              if (value.charAt(i+1) == 't')
130                  value = value.substring(0, i) + '\t' + value.substring((i++) + 2); // tab
131              else if (value.charAt(i+1) == 'r')
132                  value = value.substring(0, i) + '\r' + value.substring((i++) + 2); // return
133              else if (value.charAt(i+1) == 'n')
134                  value = value.substring(0, i) + '\n' + value.substring((i++) + 2); // line feed
135              else if (value.charAt(i+1) == 'f')
136                  value = value.substring(0, i) + '\f' + value.substring((i++) + 2); // form feed
137              else if (value.charAt(i+1) == '\\')
138                  value = value.substring(0, i) + '\\' + value.substring((i++) + 2); // \
139              else
140                  value = value.substring(0, i) + value.substring(i+1); // Quietly drop the character
141         }
142         
143         // Lazily convert the string to a list of tokens.
144         var arr = [], j, index;
145         i = 0;
146         while (i < value.length) {
147             if (value.charAt(i) == '\'') {
148                 // Handle quotes
149                 if (i == value.length-1)
150                     value = value.substring(0, i); // Silently drop the trailing quote
151                 else if (value.charAt(i+1) == '\'')
152                     value = value.substring(0, i) + value.substring(++i); // Escaped quote
153                 else {
154                     // Quoted string
155                     j = i + 2;
156                     while ((j = value.indexOf('\'', j)) != -1) {
157                         if (j == value.length-1 || value.charAt(j+1) != '\'') {
158                             // Found start and end quotes. Remove them
159                             value = value.substring(0,i) + value.substring(i+1, j) + value.substring(j+1);
160                             i = j - 1;
161                             break;
162                         }
163                         else {
164                             // Found a double quote, reduce to a single quote.
165                             value = value.substring(0,j) + value.substring(++j);
166                         }
167                     }
168                     
169                     if (j == -1) {
170                         // There is no end quote. Drop the start quote
171                         value = value.substring(0,i) + value.substring(i+1);
172                     }
173                 }
174             }
175             else if (value.charAt(i) == '{') {
176                 // Beginning of an unquoted place holder.
177                 j = value.indexOf('}', i+1);
178                 if (j == -1)
179                     i++; // No end. Process the rest of the line. Java would throw an exception
180                 else {
181                     // Add 1 to the index so that it aligns with the function arguments.
182                     index = parseInt(value.substring(i+1, j));
183                     if (!isNaN(index) && index >= 0) {
184                         // Put the line thus far (if it isn't empty) into the array
185                         var s = value.substring(0, i);
186                         if (s != "")
187                             arr.push(s);
188                         // Put the parameter reference into the array
189                         arr.push(index);
190                         // Start the processing over again starting from the rest of the line.
191                         i = 0;
192                         value = value.substring(j+1);
193                     }
194                     else
195                         i = j + 1; // Invalid parameter. Leave as is.
196                 }
197             }
198             else
199                 i++;
200         }
201         
202         // Put the remainder of the no-empty line into the array.
203         if (value != "")
204             arr.push(value);
205         value = arr;
206         
207         // Make the array the value for the entry.
208         $.i18n.map[key] = arr;
209     }
210     
211     if (value.length == 0)
212         return "";
213     if (value.lengh == 1 && typeof(value[0]) == "string")
214         return value[0];
215     
216     var s = "";
217     for (i=0; i<value.length; i++) {
218         if (typeof(value[i]) == "string")
219             s += value[i];
220         // Must be a number
221         else if (phvList && value[i] < phvList.length)
222             s += phvList[value[i]];
223         else if (!phvList && value[i] + 1 < arguments.length)
224             s += arguments[value[i] + 1];
225         else
226             s += "{"+ value[i] +"}";
227     }
228     
229     return s;
230 };
231 
232 /** Language reported by browser, normalized code */
233 $.i18n.browserLang = function() {
234     return normaliseLanguageCode(navigator.language /* Mozilla */ || navigator.userLanguage /* IE */);
235 };
236 
237 
238 /** Load and parse .properties files */
239 function loadAndParseFile(filename, settings) {
240     $.ajax({
241         url:        filename,
242         async:      false,
243         cache:        settings.cache,
244         contentType:'text/plain;charset='+ settings.encoding,
245         dataType:   'text',
246         success:    function(data, status) {
247                         parseData(data, settings.mode); 
248                     }
249     });
250 }
251 
252 /** Parse .properties files */
253 function parseData(data, mode) {
254    var parsed = '';
255    var parameters = data.split( /\n/ );
256    var regPlaceHolder = /(\{\d+\})/g;
257    var regRepPlaceHolder = /\{(\d+)\}/g;
258    var unicodeRE = /(\\u.{4})/ig;
259    for(var i=0; i<parameters.length; i++ ) {
260        parameters[i] = parameters[i].replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
261        if(parameters[i].length > 0 && parameters[i].match("^#")!="#") { // skip comments
262            var pair = parameters[i].split('=');
263            if(pair.length > 0) {
264                /** Process key & value */
265                var name = unescape(pair[0]).replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
266                var value = pair.length == 1 ? "" : pair[1];
267                // process multi-line values
268                while(value.match(/\\$/)=="\\") {
269                        value = value.substring(0, value.length - 1);
270                        value += parameters[++i].replace( /\s\s*$/, '' ); // right trim
271                }               
272                // Put values with embedded '='s back together
273                for(var s=2;s<pair.length;s++){ value +='=' + pair[s]; }
274                value = value.replace( /^\s\s*/, '' ).replace( /\s\s*$/, '' ); // trim
275                
276                /** Mode: bundle keys in a map */
277                if(mode == 'map' || mode == 'both') {
278                    // handle unicode chars possibly left out
279                    var unicodeMatches = value.match(unicodeRE);
280                    if(unicodeMatches) {
281                      for(var u=0; u<unicodeMatches.length; u++) {
282                         value = value.replace( unicodeMatches[u], unescapeUnicode(unicodeMatches[u]));
283                      }
284                    }
285                    // add to map
286                    $.i18n.map[name] = value;
287                }
288                
289                /** Mode: bundle keys as vars/functions */
290                if(mode == 'vars' || mode == 'both') {
291                    value = value.replace( /"/g, '\\"' ); // escape quotation mark (")
292                    
293                    // make sure namespaced key exists (eg, 'some.key') 
294                    checkKeyNamespace(name);
295                    
296                    // value with variable substitutions
297                    if(regPlaceHolder.test(value)) {
298                        var parts = value.split(regPlaceHolder);
299                        // process function args
300                        var first = true;
301                        var fnArgs = '';
302                        var usedArgs = [];
303                        for(var p=0; p<parts.length; p++) {
304                            if(regPlaceHolder.test(parts[p]) && (usedArgs.length == 0 || usedArgs.indexOf(parts[p]) == -1)) {
305                                if(!first) {fnArgs += ',';}
306                                fnArgs += parts[p].replace(regRepPlaceHolder, 'v$1');
307                                usedArgs.push(parts[p]);
308                                first = false;
309                            }
310                        }
311                        parsed += name + '=function(' + fnArgs + '){';
312                        // process function body
313                        var fnExpr = '"' + value.replace(regRepPlaceHolder, '"+v$1+"') + '"';
314                        parsed += 'return ' + fnExpr + ';' + '};';
315                        
316                    // simple value
317                    }else{
318                        parsed += name+'="'+value+'";';
319                    }
320                } // END: Mode: bundle keys as vars/functions
321            } // END: if(pair.length > 0)
322        } // END: skip comments
323    }
324    eval(parsed);
325 }
326 
327 /** Make sure namespace exists (for keys with dots in name) */
328 // TODO key parts that start with numbers quietly fail. i.e. month.short.1=Jan
329 function checkKeyNamespace(key) {
330     var regDot = /\./;
331     if(regDot.test(key)) {
332         var fullname = '';
333         var names = key.split( /\./ );
334         for(var i=0; i<names.length; i++) {
335             if(i>0) {fullname += '.';}
336             fullname += names[i];
337             if(eval('typeof '+fullname+' == "undefined"')) {
338                 eval(fullname + '={};');
339             }
340         }
341     }
342 }
343 
344 /** Make sure filename is an array */
345 function getFiles(names) {
346     return (names && names.constructor == Array) ? names : [names];
347 }
348 
349 /** Ensure language code is in the format aa_AA. */
350 function normaliseLanguageCode(lang) {
351     lang = lang.toLowerCase();
352     if(lang.length > 3) {
353         lang = lang.substring(0, 3) + lang.substring(3).toUpperCase();
354     }
355     return lang;
356 }
357 
358 /** Unescape unicode chars ('\u00e3') */
359 function unescapeUnicode(str) {
360   // unescape unicode codes
361   var codes = [];
362   var code = parseInt(str.substr(2), 16);
363   if (code >= 0 && code < Math.pow(2, 16)) {
364      codes.push(code);
365   }
366   // convert codes to text
367   var unescaped = '';
368   for (var i = 0; i < codes.length; ++i) {
369     unescaped += String.fromCharCode(codes[i]);
370   }
371   return unescaped;
372 }
373 
374 /* Cross-Browser Split 1.0.1
375 (c) Steven Levithan <stevenlevithan.com>; MIT License
376 An ECMA-compliant, uniform cross-browser split method */
377 var cbSplit;
378 // avoid running twice, which would break `cbSplit._nativeSplit`'s reference to the native `split`
379 if (!cbSplit) {    
380   cbSplit = function(str, separator, limit) {
381       // if `separator` is not a regex, use the native `split`
382       if (Object.prototype.toString.call(separator) !== "[object RegExp]") {
383         if(typeof cbSplit._nativeSplit == "undefined")
384           return str.split(separator, limit);
385         else
386           return cbSplit._nativeSplit.call(str, separator, limit);
387       }
388   
389       var output = [],
390           lastLastIndex = 0,
391           flags = (separator.ignoreCase ? "i" : "") +
392                   (separator.multiline  ? "m" : "") +
393                   (separator.sticky     ? "y" : ""),
394           separator = RegExp(separator.source, flags + "g"), // make `global` and avoid `lastIndex` issues by working with a copy
395           separator2, match, lastIndex, lastLength;
396   
397       str = str + ""; // type conversion
398       if (!cbSplit._compliantExecNpcg) {
399           separator2 = RegExp("^" + separator.source + "$(?!\\s)", flags); // doesn't need /g or /y, but they don't hurt
400       }
401   
402       /* behavior for `limit`: if it's...
403       - `undefined`: no limit.
404       - `NaN` or zero: return an empty array.
405       - a positive number: use `Math.floor(limit)`.
406       - a negative number: no limit.
407       - other: type-convert, then use the above rules. */
408       if (limit === undefined || +limit < 0) {
409           limit = Infinity;
410       } else {
411           limit = Math.floor(+limit);
412           if (!limit) {
413               return [];
414           }
415       }
416   
417       while (match = separator.exec(str)) {
418           lastIndex = match.index + match[0].length; // `separator.lastIndex` is not reliable cross-browser
419   
420           if (lastIndex > lastLastIndex) {
421               output.push(str.slice(lastLastIndex, match.index));
422   
423               // fix browsers whose `exec` methods don't consistently return `undefined` for nonparticipating capturing groups
424               if (!cbSplit._compliantExecNpcg && match.length > 1) {
425                   match[0].replace(separator2, function () {
426                       for (var i = 1; i < arguments.length - 2; i++) {
427                           if (arguments[i] === undefined) {
428                               match[i] = undefined;
429                           }
430                       }
431                   });
432               }
433   
434               if (match.length > 1 && match.index < str.length) {
435                   Array.prototype.push.apply(output, match.slice(1));
436               }
437   
438               lastLength = match[0].length;
439               lastLastIndex = lastIndex;
440   
441               if (output.length >= limit) {
442                   break;
443               }
444           }
445   
446           if (separator.lastIndex === match.index) {
447               separator.lastIndex++; // avoid an infinite loop
448           }
449       }
450   
451       if (lastLastIndex === str.length) {
452           if (lastLength || !separator.test("")) {
453               output.push("");
454           }
455       } else {
456           output.push(str.slice(lastLastIndex));
457       }
458   
459       return output.length > limit ? output.slice(0, limit) : output;
460   };
461   
462   cbSplit._compliantExecNpcg = /()??/.exec("")[1] === undefined; // NPCG: nonparticipating capturing group
463   cbSplit._nativeSplit = String.prototype.split;
464 
465 } // end `if (!cbSplit)`
466 String.prototype.split = function (separator, limit) {
467     return cbSplit(this, separator, limit);
468 };
469 
470 })(jQuery);
471                 
View Code

(3)定义调用的核心函数,此处做简单封装

 1   var simpleLauage = simpleLauage || {
 2             htmlLang: function () {
 3                 return $("html").attr("lang").replace("-", "_");
 4             },
 5             lang: null,
 6             init: function () {
 7                 jQuery.i18n.properties({
 8                     name: 'lang',
 9                     path: '/i18n/',
10                     mode: 'map',
11                     language: simpleLauage.htmlLang(),
12                     callback: function () {
13                         simpleLauage.lang = $.i18n.prop;
14                     }
15                 });
16             },
17             merge: function () {
18                 var a = arguments;
19                 if (a.length < 2) {
20                     return;
21                 }
22                 if (a[0] != null) {
23                     for (var i = 1; i < a.length; i++) {
24                         for (var r in a[i]) {
25                             a[0][r] = a[i][r];
26                         }
27                     }
28                 }
29                 return a[0];
30             },
31             mergeIfNotExist: function () {
32                 var a = arguments;
33                 if (a.length < 2) {
34                     return;
35                 }
36                 for (var i = 1; i < a.length; i++) {
37                     for (var r in a[i]) {
38                         if (a[0][r] == null) {
39                             a[0][r] = a[i][r];
40                         }
41                     }
42                 }
43                 return a[0];
44             }
45         };
46 
47         // 使用前记得先初始化哦
48         simpleLauage.init();
View Code

(4)在上面封装函数中,可以看到path中指向的是 资源文件路径,所以我们需要在项目中添加此路径,并在路径中添加如下文件

只需要在lang_en.properties 和lang_zh.properties中添加内容即可,内容格式如:  Signin=登录  这种形式。

步骤完毕,我们直接调用 alert(simpleLauage.lang("Signin")); 是不是会提示配置错误?没错,我们需要在IIS中添加

.properties的MIME映射(application/octet-stream即可)。

常用的 HttpContent-Type映射可参考:http://tool.oschina.net/commons

这些都是对日常常用功能的一个总结,对于碰到这种业务无法处理的同学来说可能会有一些帮助,加油!为自己!

 

 

转载于:https://www.cnblogs.com/wlxFighting/articles/4526833.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值