# This code was forked from the LiveJournal project owned and operated # by Live Journal, Inc. The code has been modified and expanded by # Dreamwidth Studios, LLC. These files were originally licensed under # the terms of the license supplied by Live Journal, Inc, which can # currently be found at: # # http://code.livejournal.org/trac/livejournal/browser/trunk/LICENSE-LiveJournal.txt # # In accordance with the original license, this code and all its # modifications are provided under the GNU General Public License. # A copy of that license can be found in the LICENSE file included as # part of this distribution. package LJ::Lang; use strict; use LJ::LangDatFile; use constant MAXIMUM_ITCODE_LENGTH => 120; my @day_short = (qw[Sun Mon Tue Wed Thu Fri Sat]); my @day_long = (qw[Sunday Monday Tuesday Wednesday Thursday Friday Saturday]); my @month_short = (qw[Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec]); my @month_long = (qw[January February March April May June July August September October November December]); # get entire array of days and months sub day_list_short { return @LJ::Lang::day_short; } sub day_list_long { return @LJ::Lang::day_long; } sub month_list_short { return @LJ::Lang::month_short; } sub month_list_long { return @LJ::Lang::month_long; } # access individual day or month given integer sub day_short { return $day_short[ $_[0] - 1 ]; } sub day_long { return $day_long[ $_[0] - 1 ]; } sub month_short { return $month_short[ $_[0] - 1 ]; } sub month_long { return $month_long[ $_[0] - 1 ]; } # lang codes for individual day or month given integer sub day_short_langcode { return "date.day." . lc( LJ::Lang::day_long(@_) ) . ".short"; } sub day_long_langcode { return "date.day." . lc( LJ::Lang::day_long(@_) ) . ".long"; } sub month_short_langcode { return "date.month." . lc( LJ::Lang::month_long(@_) ) . ".short"; } sub month_long_langcode { return "date.month." . lc( LJ::Lang::month_long(@_) ) . ".long"; } # Translated names for individual day or month given integer. You probably want # these, not the ones above. sub day_short_ml { return LJ::Lang::ml( LJ::Lang::day_short_langcode(@_) ); } sub day_long_ml { return LJ::Lang::ml( LJ::Lang::day_long_langcode(@_) ); } sub month_short_ml { return LJ::Lang::ml( LJ::Lang::month_short_langcode(@_) ); } sub month_long_ml { return LJ::Lang::ml( LJ::Lang::month_long_langcode(@_) ); } ## ordinal suffix sub day_ord { my $day = shift; # teens all end in 'th' if ( $day =~ /1\d$/ ) { return "th"; } # otherwise endings in 1, 2, 3 are special if ( $day % 10 == 1 ) { return "st"; } if ( $day % 10 == 2 ) { return "nd"; } if ( $day % 10 == 3 ) { return "rd"; } # everything else (0,4-9) end in "th" return "th"; } sub time_format { my ( $hours, $h, $m, $formatstring ) = @_; if ( $formatstring eq "short" ) { if ( $hours == 12 ) { my $ret; my $ap = "a"; if ( $h == 0 ) { $ret .= "12"; } elsif ( $h < 12 ) { $ret .= ( $h + 0 ); } elsif ( $h == 12 ) { $ret .= ( $h + 0 ); $ap = "p"; } else { $ret .= ( $h - 12 ); $ap = "p"; } $ret .= sprintf( ":%02d$ap", $m ); return $ret; } elsif ( $hours == 24 ) { return sprintf( "%02d:%02d", $h, $m ); } } return ""; } # args: secondsold - The number of seconds ago something happened. # returns: approximate English time span - "2 weeks", "20 hours", etc. sub ago_text { my $secondsold = $_[0] || 0; return LJ::Lang::ml('time.ago.never') unless $secondsold > 0; my $num; if ( $secondsold >= 60 * 60 * 24 * 7 ) { $num = int( $secondsold / ( 60 * 60 * 24 * 7 ) ); return LJ::Lang::ml( 'time.ago.week', { num => $num } ); } elsif ( $secondsold >= 60 * 60 * 24 ) { $num = int( $secondsold / ( 60 * 60 * 24 ) ); return LJ::Lang::ml( 'time.ago.day', { num => $num } ); } elsif ( $secondsold >= 60 * 60 ) { $num = int( $secondsold / ( 60 * 60 ) ); return LJ::Lang::ml( 'time.ago.hour', { num => $num } ); } elsif ( $secondsold >= 60 ) { $num = int( $secondsold / 60 ); return LJ::Lang::ml( 'time.ago.minute', { num => $num } ); } else { $num = $secondsold; return LJ::Lang::ml( 'time.ago.second', { num => $num } ); } } *LJ::ago_text = \&ago_text; # args: time in seconds of last activity; "current" time in seconds. # returns: result of ago_text for the difference. sub diff_ago_text { my ( $last, $time ) = @_; return ago_text(0) unless $last; $time = time() unless defined $time; my $diff = ( $time - $last ) || 1; return ago_text($diff); } *LJ::diff_ago_text = \&diff_ago_text; #### ml_ stuff: my $LS_CACHED = 0; my %DM_ID = (); # id -> { type, args, dmid, langs => { => 1, => 0, => 1 } } my %DM_UNIQ = (); # "$type/$args" => ^^^ my %LN_ID = (); # id -> { ..., ..., 'children' => [ $ids, .. ] } my %LN_CODE = (); # $code -> ^^^^ my $LAST_ERROR; my %TXT_CACHE; sub last_error { return $LAST_ERROR; } sub set_error { $LAST_ERROR = $_[0]; return 0; } sub get_lang { my $code = $_[0]; return unless defined $code; load_lang_struct() unless $LS_CACHED; return $LN_CODE{$code}; } sub get_lang_id { my $id = $_[0]; return unless defined $id; load_lang_struct() unless $LS_CACHED; return $LN_ID{$id}; } sub get_dom { my $dmcode = $_[0]; return unless defined $dmcode; load_lang_struct() unless $LS_CACHED; return $DM_UNIQ{$dmcode}; } sub get_dom_id { my $dmid = $_[0]; return unless defined $dmid; load_lang_struct() unless $LS_CACHED; return $DM_ID{$dmid}; } sub get_domains { load_lang_struct() unless $LS_CACHED; return values %DM_ID; } sub get_root_lang { my $dom = shift; # from, say, get_dom return undef unless ref $dom eq "HASH"; my $lang_override = LJ::Hooks::run_hook( "root_lang_override", $dom ); return get_lang($lang_override) if $lang_override; foreach ( keys %{ $dom->{'langs'} } ) { if ( $dom->{'langs'}->{$_} ) { return get_lang_id($_); } } return undef; } sub load_lang_struct { return 1 if $LS_CACHED; my $dbr = LJ::get_db_reader(); return set_error("No database available") unless $dbr; my $sth; $sth = $dbr->prepare("SELECT dmid, type, args FROM ml_domains"); $sth->execute; while ( my ( $dmid, $type, $args ) = $sth->fetchrow_array ) { my $uniq = $args ? "$type/$args" : $type; $DM_UNIQ{$uniq} = $DM_ID{$dmid} = { 'type' => $type, 'args' => $args, 'dmid' => $dmid, 'uniq' => $uniq, }; } $sth = $dbr->prepare("SELECT lnid, lncode, lnname, parenttype, parentlnid FROM ml_langs"); $sth->execute; while ( my ( $id, $code, $name, $ptype, $pid ) = $sth->fetchrow_array ) { $LN_ID{$id} = $LN_CODE{$code} = { 'lnid' => $id, 'lncode' => $code, 'lnname' => $name, 'parenttype' => $ptype, 'parentlnid' => $pid, }; } foreach ( values %LN_CODE ) { next unless $_->{'parentlnid'}; push @{ $LN_ID{ $_->{'parentlnid'} }->{'children'} }, $_->{'lnid'}; } $sth = $dbr->prepare("SELECT lnid, dmid, dmmaster FROM ml_langdomains"); $sth->execute; while ( my ( $lnid, $dmid, $dmmaster ) = $sth->fetchrow_array ) { $DM_ID{$dmid}->{'langs'}->{$lnid} = $dmmaster; } $LS_CACHED = 1; } sub relative_langdat_file_of_lang_itcode { my ( $lang, $itcode ) = @_; my $root_lang = "en"; my $root_lang_local = $LJ::DEFAULT_LANG; my $base_file = "bin/upgrading/$lang\.dat"; # not a root or root_local lang, just return base file location unless ( $lang eq $root_lang || $lang eq $root_lang_local ) { return $base_file; } my $is_local = $lang eq $root_lang_local && $lang ne $root_lang; # is this a filename-based itcode? if ( $itcode =~ m!^(/.+\.bml)! ) { my $file = $1; # given the filename of this itcode and the current # source, what langdat file should we use? my $langdat_file = "htdocs$file\.text"; $langdat_file .= $is_local ? ".local" : ""; return $langdat_file; } if ( $itcode =~ m!^(/.+\.tt)! ) { my $file = $1; my $langdat_file = "views$file\.text"; $langdat_file .= $is_local ? ".local" : ""; return $langdat_file; } # not a bml file, goes into base .dat file return $base_file; } sub itcode_for_langdat_file { my ( $langdat_file, $itcode ) = @_; # non-bml itcode, return full itcode path unless ( $langdat_file =~ m!^.+\.(?:bml|tt)\.text(?:\.local)?$! ) { return $itcode; } # bml itcode, strip filename and return if ( $itcode =~ m!^/.+\.(?:bml|tt)(\..+)! ) { return $1; } # fallback -- full $itcode return $itcode; } sub get_chgtime_unix { my ( $lncode, $dmid, $itcode ) = @_; load_lang_struct() unless $LS_CACHED; $dmid = int( $dmid || 1 ); my $l = get_lang($lncode); unless ($l) { warn "No lang info for lang $lncode. Make sure you've run\n" . " bin/upgrading/texttool.pl load"; return 0; } my $lnid = $l->{'lnid'} or die "Could not get lang_id for lang $lncode"; my $itid = LJ::Lang::get_itemid( $dmid, $itcode ) or return 0; my $dbr = LJ::get_db_reader(); $dmid += 0; my $chgtime = $dbr->selectrow_array( "SELECT chgtime FROM ml_latest WHERE dmid=? AND itid=? AND lnid=?", undef, $dmid, $itid, $lnid ); die $dbr->errstr if $dbr->err; return $chgtime ? LJ::mysqldate_to_time($chgtime) : 0; } sub get_itemid { my ( $dmid, $itcode, $opts ) = @_; load_lang_struct() unless $LS_CACHED; if ( length $itcode > MAXIMUM_ITCODE_LENGTH ) { warn "'$itcode' exceeds maximum code length, truncating to " . MAXIMUM_ITCODE_LENGTH . " symbols"; $itcode = substr( $itcode, 0, MAXIMUM_ITCODE_LENGTH ); } my $dbr = LJ::get_db_reader(); $dmid += 0; my $itid = $dbr->selectrow_array( "SELECT itid FROM ml_items WHERE dmid=$dmid AND itcode=?", undef, $itcode ); return $itid if defined $itid; my $dbh = LJ::get_db_writer(); return 0 unless $dbh; # allocate a new id LJ::DB::get_lock( $dbh, 'global', 'mlitem_dmid' ) || return 0; $itid = $dbh->selectrow_array( "SELECT MAX(itid)+1 FROM ml_items WHERE dmid=?", undef, $dmid ); $itid ||= 1; # if the table is empty, NULL+1 == NULL $dbh->do( "INSERT INTO ml_items (dmid, itid, itcode, notes) " . "VALUES (?, ?, ?, ?)", undef, $dmid, $itid, $itcode, $opts->{'notes'} ); LJ::DB::release_lock( $dbh, 'global', 'mlitem_dmid' ); if ( $dbh->err ) { return $dbh->selectrow_array( "SELECT itid FROM ml_items WHERE dmid=$dmid AND itcode=?", undef, $itcode ); } return $itid; } # this is called when editing text from a web UI. # first try and run a local hook to save the text, # if that fails then just call set_text # returns ($success, $responsemsg) where responsemsg can be output # from whatever saves the text sub web_set_text { my ( $dmid, $lncode, $itcode, $text, $opts ) = @_; my $resp = ''; my $hook_ran = 0; if ( LJ::Hooks::are_hooks('web_set_text') ) { $hook_ran = LJ::Hooks::run_hook( 'web_set_text', $dmid, $lncode, $itcode, $text, $opts ); } # save in the db my $save_success = LJ::Lang::set_text( $dmid, $lncode, $itcode, $text, $opts ); $resp = LJ::Lang::last_error() unless $save_success; warn $resp if !$save_success && $LJ::IS_DEV_SERVER; return ( $save_success, $resp ); } sub set_text { my ( $dmid, $lncode, $itcode, $text, $opts ) = @_; load_lang_struct() unless $LS_CACHED; my $l = $LN_CODE{$lncode} or return set_error("Language not defined."); my $lnid = $l->{'lnid'}; $dmid += 0; # is this domain/language request even possible? return set_error("Bogus domain") unless exists $DM_ID{$dmid}; return set_error("Bogus lang for that domain") unless exists $DM_ID{$dmid}->{'langs'}->{$lnid}; my $itid = get_itemid( $dmid, $itcode, { 'notes' => $opts->{'notes'} } ); return set_error("Couldn't allocate itid.") unless $itid; my $dbh = LJ::get_db_writer(); my $txtid = 0; my $oldtextid = $dbh->selectrow_array( "SELECT txtid FROM ml_text WHERE lnid=? AND dmid=? AND itid=?", undef, $lnid, $dmid, $itid ); if ( defined $text ) { my $userid = ( $opts->{userid} // 0 ) + 0; # Strip bad characters $text =~ s/\r//; my $qtext = $dbh->quote($text); LJ::DB::get_lock( $dbh, 'global', 'ml_text_txtid' ) || return 0; $txtid = $dbh->selectrow_array( "SELECT MAX(txtid)+1 FROM ml_text WHERE dmid=?", undef, $dmid ); $txtid ||= 1; $dbh->do( "INSERT INTO ml_text (dmid, txtid, lnid, itid, text, userid) " . "VALUES ($dmid, $txtid, $lnid, $itid, $qtext, $userid)" ); LJ::DB::release_lock( $dbh, 'global', 'ml_text_txtid' ); return set_error( "Error inserting ml_text: " . $dbh->errstr ) if $dbh->err; } if ( $opts->{'txtid'} ) { $txtid = $opts->{'txtid'} + 0; } my $staleness = ( $opts->{staleness} // 0 ) + 0; $dbh->do( "REPLACE INTO ml_latest (lnid, dmid, itid, txtid, chgtime, staleness) " . "VALUES ($lnid, $dmid, $itid, $txtid, NOW(), $staleness)" ); return set_error( "Error inserting ml_latest: " . $dbh->errstr ) if $dbh->err; LJ::MemCache::set( "ml.${lncode}.${dmid}.${itcode}", $text ) if defined $text; my $langids; { my $vals; my $rec = sub { my $l = shift; my $rec = shift; foreach my $cid ( @{ $l->{'children'} } ) { my $clid = $LN_ID{$cid}; if ( $opts->{'childrenlatest'} ) { my $stale = $clid->{'parenttype'} eq "diff" ? 3 : 0; $vals .= "," if $vals; $vals .= "($cid, $dmid, $itid, $txtid, NOW(), $stale)"; } $langids .= "," if $langids; $langids .= $cid + 0; LJ::MemCache::delete("ml.$clid->{'lncode'}.${dmid}.${itcode}"); $rec->( $clid, $rec ); } }; $rec->( $l, $rec ); # set descendants to use this mapping $dbh->do( "INSERT IGNORE INTO ml_latest (lnid, dmid, itid, txtid, chgtime, staleness) " . "VALUES $vals" ) if $vals; # update languages that have no translation yet if ($oldtextid) { $dbh->do( "UPDATE ml_latest SET txtid=$txtid WHERE dmid=$dmid " . "AND lnid IN ($langids) AND itid=$itid AND txtid=$oldtextid" ) if $langids; } else { $dbh->do( "UPDATE ml_latest SET txtid=$txtid WHERE dmid=$dmid " . "AND lnid IN ($langids) AND itid=$itid AND staleness >= 3" ) if $langids; } } if ( $opts->{'changeseverity'} && $langids ) { my $newstale = $opts->{'changeseverity'} == 2 ? 2 : 1; $dbh->do( "UPDATE ml_latest SET staleness=$newstale WHERE lnid IN ($langids) AND " . "dmid=$dmid AND itid=$itid AND txtid<>$txtid AND staleness < $newstale" ); } return 1; } sub remove_text { my ( $dmid, $itcode, $lncode ) = @_; my $dbh = LJ::get_db_writer(); my $itid = $dbh->selectrow_array( "SELECT itid FROM ml_items WHERE dmid=? AND itcode=?", undef, $dmid, $itcode ); die "Unknown item code $itcode." unless $itid; # need to delete everything from: ml_items ml_latest ml_text $dbh->do( "DELETE FROM ml_items WHERE dmid=? AND itid=?", undef, $dmid, $itid ); my @txtids = (); my $sth = $dbh->prepare("SELECT txtid FROM ml_latest WHERE dmid=? AND itid=?"); $sth->execute( $dmid, $itid ); while ( my $txtid = $sth->fetchrow_array ) { push @txtids, $txtid; } $dbh->do( "DELETE FROM ml_latest WHERE dmid=? AND itid=?", undef, $dmid, $itid ); my $txtid_bind = join( ",", map { "?" } @txtids ); $dbh->do( "DELETE FROM ml_text WHERE dmid=? AND txtid IN ($txtid_bind)", undef, $dmid, @txtids ); # delete from memcache if lncode is defined LJ::MemCache::delete("ml.${lncode}.${dmid}.${itcode}") if $lncode; return 1; } sub get_effective_lang { my $lang; if ( LJ::is_web_context() ) { $lang = BML::get_language(); } # did we get a valid language code? if ( $lang && $LN_CODE{$lang} ) { return $lang; } # had no language code, or invalid. return default return $LJ::DEFAULT_LANG; } sub ml { my ( $code, $vars ) = @_; if ( LJ::is_web_context() ) { # this means we should use BML::ml and not do our own handling my $text = BML::ml( $code, $vars ); $LJ::_ML_USED_STRINGS{$code} = $text if $LJ::IS_DEV_SERVER; return $text; } my $lang = LJ::Lang::get_effective_lang(); return get_text( $lang, $code, undef, $vars ); } sub string_exists { my ( $code, $vars ) = @_; my $string = LJ::Lang::ml( $code, $vars ); return LJ::Lang::is_missing_string($string) ? 0 : 1; } # LJ::Lang::ml will return a number of values for "invalid string" # -- this function will tell you if the value is one of # those values. gross. sub is_missing_string { my $string = $_[0]; return 1 unless defined $string; return ( $string eq "" || $string =~ /^\[missing string/ || $string =~ /^\[uhhh:/ ) ? 1 : 0; } sub get_text { my ( $lang, $code, $dmid, $vars ) = @_; $lang ||= $LJ::DEFAULT_LANG; my $from_db = sub { my $text = get_text_multi( $lang, $dmid, [$code] ); return $text->{$code}; }; my $from_files = sub { my ( $localcode, @files ); if ( $code =~ m!^(/.+\.bml)(\..+)! ) { my $file; ( $file, $localcode ) = ( "htdocs$1", $2 ); @files = ( "$file.text.local", "$file.text" ); } elsif ( $code =~ m!^(/.+\.tt)(\..+)! ) { my $file; ( $file, $localcode ) = ( "views$1", $2 ); @files = ( "$file.text.local", "$file.text" ); } else { $localcode = $code; @files = ( "bin/upgrading/$LJ::DEFAULT_LANG.dat", "bin/upgrading/en.dat" ); } foreach my $tf (@files) { $tf = LJ::resolve_file($tf); next unless defined $tf && -e $tf; # compare file modtime to when the string was updated in the DB. # whichever is newer is authoritative my $fmodtime = ( stat $tf )[9]; my $dbmodtime = LJ::Lang::get_chgtime_unix( $lang, $dmid, $code ); return $from_db->() if !$fmodtime || $dbmodtime > $fmodtime; my $ldf = $LJ::REQ_LANGDATFILE{$tf} ||= LJ::LangDatFile->new($tf); my $val = $ldf->value($localcode); return $val if $val; } return "[missing string $code]"; }; my $gen_mld = LJ::Lang::get_dom('general'); my $is_gen_dmid = defined $dmid ? $dmid == $gen_mld->{dmid} : 1; my $text = ( $LJ::IS_DEV_SERVER && $is_gen_dmid && ( $lang eq "en" || $lang eq $LJ::DEFAULT_LANG ) ) ? $from_files->() : $from_db->(); if ( defined $vars && ref $vars eq 'HASH' ) { $text =~ s/\[\[\?([\w\-]+)\|(.+?)\]\]/resolve_plural($lang, $vars, $1, $2)/eg; $text =~ s/\[\[([^\[]+?)\]\]/$vars->{$1}/g; } $LJ::_ML_USED_STRINGS{$code} = $text if $LJ::IS_DEV_SERVER; return $text || ( $LJ::IS_DEV_SERVER ? "[uhhh: $code]" : "" ); } # Sometimes we want to force $lang to be the default, because the user # generating the text display isn't the same user who will receive the # rendered text. These helper functions make that easier. sub get_default_text { my ( $code, $vars ) = @_; return LJ::Lang::get_text( undef, $code, undef, $vars ); } sub get_default_text_multi { my ($codes) = @_; return LJ::Lang::get_text_multi( undef, undef, $codes ); } # Loads multiple language strings at once. These strings # cannot however contain variables, if you have variables # you wouldn't be calling this anyway! # args: $lang, $dmid, array ref of lang codes sub get_text_multi { my ( $lang, $dmid, $codes ) = @_; return {} unless $codes; $dmid = int( $dmid || 1 ); $lang ||= $LJ::DEFAULT_LANG; load_lang_struct() unless $LS_CACHED; ## %strings: code --> text my %strings; ## normalize the codes: all chars must be in lower case ## MySQL string comparison isn't case-sensitive, but memcaches keys are. ## Caller will get %strings with keys in original case. ## ## Final note about case: ## Codes in disk .text files, mysql and bml files may be mixed-cased ## Codes in memcache and %TXT_CACHE are lower-case ## Codes are not case-sensitive ## %lc_code: lower-case code --> original code my %lc_codes = map { lc($_) => $_ } @$codes; ## %memkeys: lower-case code --> memcache key my %memkeys; foreach my $code ( keys %lc_codes ) { my $cache_key = "ml.${lang}.${dmid}.${code}"; my $text = $LJ::NO_ML_CACHE ? undef : $TXT_CACHE{$cache_key}; if ( defined $text ) { $strings{ $lc_codes{$code} } = $text; $LJ::_ML_USED_STRINGS{$code} = $text if $LJ::IS_DEV_SERVER; } else { $memkeys{$cache_key} = $code; } } return \%strings unless %memkeys; my $mem = LJ::MemCache::get_multi( keys %memkeys ) || {}; ## %dbload: lower-case key --> text; text may be empty (but defined) string my %dbload; foreach my $cache_key ( keys %memkeys ) { my $code = $memkeys{$cache_key}; my $text = $mem->{$cache_key}; if ( defined $text ) { $strings{ $lc_codes{$code} } = $text; $LJ::_ML_USED_STRINGS{$code} = $text if $LJ::IS_DEV_SERVER; $TXT_CACHE{$cache_key} = $text; } else { # we need to cache nonexistant/empty strings because otherwise we're running a lot of queries all the time # to cache nonexistant strings, value of %dbload must be defined $dbload{$code} = ''; } } return \%strings unless %dbload; my $l = $LN_CODE{$lang}; # This shouldn't happen! die("Unable to load language code: $lang") unless $l; my $dbr = LJ::get_db_reader(); my $bind = join( ',', map { '?' } keys %dbload ); my $sth = $dbr->prepare( "SELECT i.itcode, t.text, i.visible" . " FROM ml_text t, ml_latest l, ml_items i" . " WHERE t.dmid=? AND t.txtid=l.txtid" . " AND l.dmid=? AND l.lnid=? AND l.itid=i.itid" . " AND i.dmid=? AND i.itcode IN ($bind)" ); $sth->execute( $dmid, $dmid, $l->{lnid}, $dmid, keys %dbload ); # now replace the empty strings with the defined ones that we got back from the database while ( my ( $code, $text, $vis ) = $sth->fetchrow_array ) { # some MySQL codes might be mixed-case $dbload{ lc($code) } = $text; # if not currently visible, then set it unless ($vis) { my $dbh = LJ::get_db_writer(); $dbh->do( 'UPDATE ml_items SET visible = 1 WHERE itcode = ?', undef, $code ); } } while ( my ( $code, $text ) = each %dbload ) { $strings{ $lc_codes{$code} } = $text; $LJ::_ML_USED_STRINGS{$code} = $text if $LJ::IS_DEV_SERVER; my $cache_key = "ml.${lang}.${dmid}.${code}"; $TXT_CACHE{$cache_key} = $text; LJ::MemCache::set( $cache_key, $text ); } return \%strings; } sub get_lang_names { my @langs = @_; push @langs, @LJ::LANGS unless @langs; my @list; foreach my $code (@langs) { my $l = LJ::Lang::get_lang($code); next unless $l; my $item = "langname.$code"; my $namethislang = BML::ml($item); my $namenative = LJ::Lang::get_text( $l->{'lncode'}, $item ); push @list, $code, $namenative; } return \@list; } # FIXME: this isn't used anywhere; just falls through to BML::set_language, # which only affects the BML code package in the active process. Keeping this # as a stub to assist with the gradual transition to non-BML functions. sub set_lang { my $lang = shift; my $l = LJ::Lang::get_lang($lang); # set language through BML so it will apply immediately BML::set_language( $l->{lncode} ); return; } # The translation system now supports the ability to add multiple plural forms of the word # given different rules in a languge. This functionality is much like the plural support # in the S2 styles code. To use this code you must use the BML::ml function and pass # the number of items as one of the variables. To make sure that you are allowing the # utmost compatibility for each language you should not hardcode the placement of the # number of items in relation to the noun. Let the translation string do this for you. # A translation string is in the format of, with num being the variable storing the # number of items. # =[[num]] [[?num|singular|plural1|plural2|pluralx]] sub resolve_plural { my ( $lang, $vars, $varname, $wordlist ) = @_; my $count = $vars->{$varname}; my @wlist = split( /\|/, $wordlist ); my $plural_form = plural_form( $lang, $count ); return $wlist[$plural_form]; } # TODO: make this faster, using AUTOLOAD and symbol tables pointing to dynamically # generated subs which only use $_[0] for $count. sub plural_form { my ( $lang, $count ) = @_; return plural_form_en($count) if $lang =~ /^en/; return plural_form_ru($count) if $lang =~ /^ru/ || $lang =~ /^uk/ || $lang =~ /^be/; return plural_form_fr($count) if $lang =~ /^fr/ || $lang =~ /^pt_BR/; return plural_form_lt($count) if $lang =~ /^lt/; return plural_form_pl($count) if $lang =~ /^pl/; return plural_form_singular() if $lang =~ /^hu/ || $lang =~ /^ja/ || $lang =~ /^tr/; return plural_form_lv($count) if $lang =~ /^lv/; return plural_form_is($count) if $lang =~ /^is/; return plural_form_en($count); # default } # English, Danish, German, Norwegian, Swedish, Estonian, Finnish, Greek, Hebrew, Italian, Portugese, Spanish, Esperanto sub plural_form_en { my $count = $_[0] || 0; return 0 if $count == 1; return 1; } # French, Brazilian Portuguese sub plural_form_fr { my $count = $_[0] || 0; return 1 if $count > 1; return 0; } # Croatian, Czech, Russian, Slovak, Ukrainian, Belarusian sub plural_form_ru { my $count = $_[0] || 0; return 0 if ( $count % 10 == 1 and $count % 100 != 11 ); return 1 if ($count % 10 >= 2 and $count % 10 <= 4 and ( $count % 100 < 10 or $count % 100 >= 20 ) ); return 2; } # Polish sub plural_form_pl { my $count = $_[0] || 0; return 0 if ( $count == 1 ); return 1 if ( $count % 10 >= 2 && $count % 10 <= 4 && ( $count % 100 < 10 || $count % 100 >= 20 ) ); return 2; } # Lithuanian sub plural_form_lt { my $count = $_[0] || 0; return 0 if ( $count % 10 == 1 && $count % 100 != 11 ); return 1 if ( $count % 10 >= 2 && ( $count % 100 < 10 || $count % 100 >= 20 ) ); return 2; } # Hungarian, Japanese, Korean (not supported), Turkish sub plural_form_singular { return 0; } # Latvian sub plural_form_lv { my $count = $_[0] || 0; return 0 if ( $count % 10 == 1 && $count % 100 != 11 ); return 1 if ( $count != 0 ); return 2; } # Icelandic sub plural_form_is { my $count = $_[0] || 0; return 0 if ( $count % 10 == 1 and $count % 100 != 11 ); return 1; } 1;